@elshaer/homebridge-lg-thinq
Version:
A Homebridge plugin for controlling/monitoring LG ThinQ device via LG ThinQ platform.
301 lines • 13.3 kB
JavaScript
"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