UNPKG

@elshaer/homebridge-lg-thinq

Version:

A Homebridge plugin for controlling/monitoring LG ThinQ device via LG ThinQ platform.

301 lines 13.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThinQ = void 0; const API_1 = require("./API"); const Device_1 = require("./Device"); const constants_1 = require("./constants"); const uuid = __importStar(require("uuid")); const Path = __importStar(require("path")); const forge = __importStar(require("node-forge")); const DeviceModel_1 = require("./DeviceModel"); const helper_1 = __importDefault(require("../v1/helper")); const errors_1 = require("../errors"); const settings_1 = require("../settings"); const aws_iot_device_sdk_1 = require("aws-iot-device-sdk"); const url_1 = require("url"); const Persist_1 = __importDefault(require("./Persist")); class ThinQ { constructor(platform, config, log) { this.platform = platform; this.config = config; this.log = log; this.workIds = {}; this.deviceModel = {}; this.api = new API_1.API(this.config.country, this.config.language); this.api.logger = log; this.api.httpClient.interceptors.response.use(response => { this.log.debug('[request]', response.config.method, response.config.url); return response; }, err => { return Promise.reject(err); }); if (config.refresh_token) { this.api.setRefreshToken(config.refresh_token); } else if (config.username && config.password) { this.api.setUsernamePassword(config.username, config.password); } this.persist = new Persist_1.default(Path.join(this.platform.api.user.storagePath(), settings_1.PLUGIN_NAME, 'persist', 'devices')); } async devices() { const listDevices = await this.api.getListDevices().catch(() => { return []; }); return listDevices.map(device => new Device_1.Device(device)) // skip all device invalid id .filter(device => device.id.match(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/)); } async setup(device) { // load device model device.deviceModel = await this.loadDeviceModel(device); if (device.deviceModel.data.Monitoring === undefined && device.deviceModel.data.MonitoringValue === undefined && device.deviceModel.data.Value === undefined) { this.log.warn('[' + device.name + '] This device may not "smart" device. Ignore it!'); } if (device.platform === constants_1.PlatformType.ThinQ1) { // register work uuid await this.registerWorkId(device); // transform thinq1 device const deviceWithSnapshot = helper_1.default.transform(device, null); device.snapshot = deviceWithSnapshot.snapshot; } return true; } async unregister(device) { if (device.platform === constants_1.PlatformType.ThinQ1 && device.id in this.workIds && this.workIds[device.id] !== null) { try { await this.api.sendMonitorCommand(device.id, 'Stop', this.workIds[device.id]); } catch (err) { //this.log.error(err); } delete this.workIds[device.id]; } } async registerWorkId(device) { return this.workIds[device.id] = await this.api.sendMonitorCommand(device.id, 'Start', uuid.v4()).then(data => { if (data !== undefined && 'workId' in data) { return data.workId; } return null; }); } async loadDeviceModel(device) { var _a, _b; let deviceModel = await this.persist.getItem(device.id); if (!deviceModel) { this.log.debug('[' + device.id + '] Device model cache missed.'); deviceModel = await this.api.httpClient.get(device.data.modelJsonUri).then(res => res.data); await this.persist.setItem(device.id, deviceModel); } const modelVersion = parseFloat((_a = deviceModel.Info) === null || _a === void 0 ? void 0 : _a.version); // new washer model if (device.type === constants_1.DeviceType[constants_1.DeviceType.WASH_TOWER_2] && modelVersion && modelVersion >= 3 && ((_b = deviceModel.Info) === null || _b === void 0 ? void 0 : _b.defaultTargetDeviceRoot) && deviceModel[deviceModel.Info.defaultTargetDeviceRoot]) { deviceModel = deviceModel[deviceModel.Info.defaultTargetDeviceRoot]; } return this.deviceModel[device.id] = device.deviceModel = new DeviceModel_1.DeviceModel(deviceModel); } async pollMonitor(device) { device.deviceModel = await this.loadDeviceModel(device); if (device.platform === constants_1.PlatformType.ThinQ1) { let result = null; // check if work id is registered if (!(device.id in this.workIds) || this.workIds[device.id] === null) { // register work id const workId = await this.registerWorkId(device); if (workId === undefined || workId === null) { // device may not connected return helper_1.default.transform(device, result); } } try { result = await this.api.getMonitorResult(device.id, this.workIds[device.id]); } catch (err) { if (err instanceof errors_1.MonitorError) { // restart monitor and try again await this.unregister(device); await this.registerWorkId(device); // retry 1 times try { result = await this.api.getMonitorResult(device.id, this.workIds[device.id]); } catch (err) { // stop it // await this.stopMonitor(device); } } else if (err instanceof errors_1.NotConnectedError) { // device not online // this.log.debug('Device not connected: ', device.toString()); } else { throw err; } } return helper_1.default.transform(device, result); } return device; } thinq1DeviceControl(device, key, value) { const data = helper_1.default.prepareControlData(device, key, value); return this.api.thinq1PostRequest('rti/rtiControl', data).catch(err => { this.log.error('Unknown Error: ', err); }); } deviceControl(device, values, command = 'Set', ctrlKey = 'basicCtrl', ctrlPath = 'control-sync') { const id = device instanceof Device_1.Device ? device.id : device; return this.api.sendCommandToDevice(id, values, command, ctrlKey, ctrlPath) .then(response => { if (response.resultCode === '0000') { this.log.debug('ThinQ Device Received the Command'); return true; } else { this.log.debug('ThinQ Device Did Not Received the Command'); return false; } }); } async registerMQTTListener(callback) { const delayMs = ms => new Promise(res => setTimeout(res, ms)); let tried = 5; while (tried > 0) { try { await this._registerMQTTListener(callback); return; } catch (err) { tried--; this.log.debug('Cannot start MQTT, retrying in 5s.'); this.log.debug('mqtt err:', err); await delayMs(5000); } } this.log.error('Cannot start MQTT!'); } async _registerMQTTListener(callback) { const route = await this.api.getRequest('https://common.lgthinq.com/route').then(data => data.result); // key-pair const keys = await this.persist.cacheForever('keys', async () => { this.log.debug('Generating 2048-bit key-pair...'); const keys = forge.pki.rsa.generateKeyPair(2048); return { privateKey: forge.pki.privateKeyToPem(keys.privateKey), publicKey: forge.pki.publicKeyToPem(keys.publicKey), }; }); // CSR const csr = await this.persist.cacheForever('csr', async () => { this.log.debug('Creating certification request (CSR)...'); const csr = forge.pki.createCertificationRequest(); csr.publicKey = forge.pki.publicKeyFromPem(keys.publicKey); csr.setSubject([ { shortName: 'CN', value: 'AWS IoT Certificate', }, { shortName: 'O', value: 'Amazon', }, ]); csr.sign(forge.pki.privateKeyFromPem(keys.privateKey), forge.md.sha256.create()); return forge.pki.certificationRequestToPem(csr); }); const submitCSR = async () => { await this.api.postRequest('service/users/client', {}); return await this.api.postRequest('service/users/client/certificate', { csr: csr.replace(/-----(BEGIN|END) CERTIFICATE REQUEST-----/g, '').replace(/(\r\n|\r|\n)/g, ''), }).then(data => data.result); }; const urls = new url_1.URL(route.mqttServer); // get trusted cer root based on hostname let rootCAUrl; if (urls.hostname.match(/^([^.]+)-ats.iot.([^.]+).amazonaws.com$/g)) { // ats endpoint rootCAUrl = 'https://www.amazontrust.com/repository/AmazonRootCA1.pem'; } else if (urls.hostname.match(/^([^.]+).iot.ruic.lgthinq.com$/g)) { // LG owned certificate - Comodo CA rootCAUrl = 'http://www.tbs-x509.com/Comodo_AAA_Certificate_Services.crt'; } else { // use legacy VeriSign cert for other endpoint // eslint-disable-next-line max-len rootCAUrl = 'https://www.websecurity.digicert.com/content/dam/websitesecurity/digitalassets/desktop/pdfs/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem'; } const rootCA = await this.api.getRequest(rootCAUrl); const connectToMqtt = async () => { // submit csr const certificate = await submitCSR(); const connectData = { caCert: Buffer.from(rootCA, 'utf-8'), privateKey: Buffer.from(keys.privateKey, 'utf-8'), clientCert: Buffer.from(certificate.certificatePem, 'utf-8'), clientId: this.api.client_id, host: urls.hostname, }; this.log.debug('open mqtt connection to', route.mqttServer); const device = (0, aws_iot_device_sdk_1.device)(connectData); device.on('error', (err) => { this.log.error('mqtt err:', err); }); device.on('connect', () => { this.log.info('Successfully connected to the MQTT server.'); this.log.debug('mqtt connected:', route.mqttServer); for (const subscription of certificate.subscriptions) { device.subscribe(subscription); } }); device.on('message', (topic, payload) => { callback(JSON.parse(payload.toString())); this.log.debug('mqtt message received:', payload.toString()); }); device.on('offline', () => { device.end(); this.log.info('MQTT disconnected, retrying in 60 seconds!'); setTimeout(async () => { await connectToMqtt(); }, 60000); }); }; // first call await connectToMqtt(); } async isReady() { await this.persist.init(); await this.api.ready(); } } exports.ThinQ = ThinQ; //# sourceMappingURL=ThinQ.js.map