UNPKG

@palekseii/homebridge-tuya-platform

Version:

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

314 lines 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LOGIN_ERROR_MESSAGES = void 0; /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ const https_1 = __importDefault(require("https")); const crypto_1 = __importDefault(require("crypto")); const uuid_1 = require("uuid"); const async_await_retry_1 = __importDefault(require("async-await-retry")); // eslint-disable-next-line // @ts-ignore const package_json_1 = require("../../package.json"); const Logger_1 = require("../util/Logger"); var Endpoints; (function (Endpoints) { Endpoints["AMERICA"] = "https://openapi.tuyaus.com"; Endpoints["AMERICA_EAST"] = "https://openapi-ueaz.tuyaus.com"; Endpoints["CHINA"] = "https://openapi.tuyacn.com"; Endpoints["EUROPE"] = "https://openapi.tuyaeu.com"; Endpoints["EUROPE_WEST"] = "https://openapi-weaz.tuyaeu.com"; Endpoints["INDIA"] = "https://openapi.tuyain.com"; })(Endpoints || (Endpoints = {})); const DEFAULT_ENDPOINTS = { [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818], [Endpoints.CHINA.toString()]: [86], [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876], [Endpoints.INDIA.toString()]: [91], }; exports.LOGIN_ERROR_MESSAGES = { 1004: 'Please make sure your endpoint, accessId, accessKey is right.', 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', 1114: 'Please make sure your endpoint, accessId, accessKey is right.', 2401: 'Username or password is wrong.', 2406: 'Please make sure you selected the right data center where your app account located, and the app account is linked with cloud project.', }; const API_NOT_SUBSCRIBED_ERROR = ` API not subscribed. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", and Authorize the following APIs before using: - Authorization Token Management - Device Status Notification - IoT Core - Industry Project Client Service (for "Custom" project) `; const API_ERROR_MESSAGES = { 1010: 'Token expired. Tuya Cloud don\'t support running multiple HomeBridge/HomeAssistant instance with same tuya account.', 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', 28841101: API_NOT_SUBSCRIBED_ERROR, 28841105: API_NOT_SUBSCRIBED_ERROR, }; class TuyaOpenAPI { constructor(endpoint, accessId, accessKey, log = console, lang = 'en', debug = false) { this.endpoint = endpoint; this.accessId = accessId; this.accessKey = accessKey; this.log = log; this.lang = lang; this.debug = debug; this.assetIDArr = []; this.deviceArr = []; this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; this.log = new Logger_1.PrefixLogger(log, TuyaOpenAPI.name, debug); } static getDefaultEndpoint(countryCode) { for (const endpoint of Object.keys(DEFAULT_ENDPOINTS)) { const countryCodeList = DEFAULT_ENDPOINTS[endpoint]; if (countryCodeList.includes(countryCode)) { return endpoint; } } return Endpoints.AMERICA; } isLogin() { return this.tokenInfo.access_token.length > 0; } isTokenExpired() { return (this.tokenInfo.expire - 60 * 1000 <= new Date().getTime()); } isTokenManagementAPI(path) { if (path.startsWith('/v1.0/token')) { return true; } return false; } async _refreshAccessTokenIfNeed(path) { if (!this.isLogin()) { return; } if (!this.isTokenExpired()) { return; } if (this.isTokenManagementAPI(path)) { return; } this.log.debug('Refreshing access_token'); const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); if (res.success === false) { this.log.error('Refresh access_token failed. code = %s, msg = %s', res.code, res.msg); return; } const { access_token, refresh_token, uid, expire_time } = res.result; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, expire: expire_time * 1000 + new Date().getTime(), }; } /** * In 'Custom' project, get a token directly. (Login with admin) * Have permission on asset management, user management. * But lost some permission on device management. * @returns */ async getToken() { const res = await this.get('/v1.0/token', { grant_type: 1 }); if (res.success) { const { access_token, refresh_token, uid, expire_time } = res.result; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, expire: expire_time * 1000 + new Date().getTime(), }; } return res; } /** * In 'Smart Home' project, login with App's user. * @param countryCode 2-digit Country Code * @param username Username * @param password Password * @param appSchema App Schema: 'tuyaSmart', 'smartlife' * @returns */ async homeLogin(countryCode, username, password, appSchema) { if (this._isSaltedPassword(password)) { this.log.info('Login with md5 salted password.'); } else { password = crypto_1.default.createHash('md5').update(password).digest('hex'); } this.log.info('Login to: %s', this.endpoint); this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { country_code: countryCode, username: username, password: password, schema: appSchema, }); if (res.success) { const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; this.endpoint = platform_url || this.endpoint; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, expire: expire_time * 1000 + new Date().getTime(), }; } return res; } /** * In 'Custom' project, Search user by username. * @param username Username * @returns */ async customGetUserInfo(username) { const res = await this.get(`/v1.2/iot-02/users/${username}`); return res; } /** * In 'Custom' project, create a user. * @param username Username * @param password Password * @param country_code Country Code (Useless) * @returns */ async customCreateUser(username, password, country_code = 1) { const res = await this.post('/v1.0/iot-02/users', { username, password: crypto_1.default.createHash('sha256').update(password).digest('hex'), country_code, }); return res; } /** * In 'Custom' project, login with user. * @param username Username * @param password Password * @returns */ async customLogin(username, password) { this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-03/users/login', { username: username, password: crypto_1.default.createHash('sha256').update(password).digest('hex'), }); if (res.success) { const { access_token, refresh_token, uid, expire } = res.result; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, expire: expire * 1000 + new Date().getTime(), }; } return res; } async request(method, path, params, body) { await this._refreshAccessTokenIfNeed(path); const now = new Date().getTime(); const nonce = (0, uuid_1.v4)(); const accessToken = this.tokenInfo.access_token || ''; const stringToSign = this._getStringToSign(method, path, params, body); const headers = { 't': `${now}`, 'client_id': this.accessId, 'nonce': nonce, 'Signature-Headers': 'client_id', 'sign': this._getSign(this.accessId, this.accessKey, this.isTokenManagementAPI(path) ? '' : this.tokenInfo.access_token, now, nonce, stringToSign), 'sign_method': 'HMAC-SHA256', 'access_token': accessToken, 'lang': this.lang, 'dev_lang': 'javascript', 'dev_channel': 'homebridge', 'devVersion': package_json_1.version, }; this.log.debug('Request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); if (params) { path += '?' + new URLSearchParams(params).toString(); } const res = await (0, async_await_retry_1.default)(async () => new Promise((resolve, reject) => { const req = https_1.default.request({ host: new URL(this.endpoint).host, method, headers, path, }, res => { if (res.statusCode !== 200) { this.log.warn('Status: %d %s', res.statusCode, res.statusMessage); return; } res.setEncoding('utf8'); let rawData = ''; res.on('data', (chunk) => { rawData += chunk; }); res.on('end', () => { resolve(JSON.parse(rawData)); }); }); if (body) { req.write(JSON.stringify(body)); } req.on('error', e => { this.log.error('Network error: %s. Retrying...', e.message); reject(e); }); req.end(); }), undefined, { retriesMax: 10, interval: 100, exponential: true, factor: 2, jitter: 100 }); this.log.debug('Response:\npath = %s\ndata = %s', path, JSON.stringify(res, null, 2)); if (res && res.success !== true && API_ERROR_MESSAGES[res.code]) { this.log.error(API_ERROR_MESSAGES[res.code]); } return res; } async get(path, params) { return this.request('get', path, params, null); } async post(path, params) { return this.request('post', path, null, params); } async delete(path, params) { return this.request('delete', path, params, null); } _getSign(accessId, accessKey, accessToken = '', timestamp = 0, nonce, stringToSign) { const message = [accessId, accessToken, timestamp, nonce, stringToSign].join(''); const sign = crypto_1.default.createHmac('SHA256', accessKey).update(message).digest('hex').toUpperCase(); return sign; } _getStringToSign(method, path, params, body) { const httpMethod = method.toUpperCase(); const bodyStream = body ? JSON.stringify(body) : ''; const contentSHA256 = crypto_1.default.createHash('sha256').update(bodyStream).digest('hex'); const headers = `client_id:${this.accessId}\n`; const url = this._getSignUrl(path, params); const result = [httpMethod, contentSHA256, headers, url].join('\n'); return result; } _getSignUrl(path, params) { if (!params) { return path; } const sortedKeys = Object.keys(params).sort(); const kv = []; for (const key of sortedKeys) { if (params[key] !== null && params[key] !== undefined) { kv.push(`${key}=${params[key]}`); } } const url = `${path}?${kv.join('&')}`; return url; } _isSaltedPassword(password) { return Buffer.from(password, 'hex').length === 16; } } exports.default = TuyaOpenAPI; TuyaOpenAPI.Endpoints = Endpoints; //# sourceMappingURL=TuyaOpenAPI.js.map