From 78f6e80033c498626d5e28fc4dfc5475e43c4ebb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 12:37:32 +0800 Subject: [PATCH] Rewrite `Custom` project start process. (#11) * Rewrite the `Custom` project start process. User will be created and authorized automatically. * Update README.md --- CHANGELOG.md | 1 + README.md | 2 + config.schema.json | 8 +++- src/config.ts | 2 - src/core/TuyaOpenAPI.ts | 59 ++++++++++++++++++++++++ src/device/TuyaCustomDeviceManager.ts | 13 +++++- src/platform.ts | 64 ++++++++++++++++++++++----- test/custom.test.ts | 8 +++- 8 files changed, 140 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6de9f2..68383a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Remove `lang` option. - Update unit test. - For `Custom` project type, some API has switched to the same as `Smart Home`. +- Rewrite the `Custom` project start process. User will be created and authorized automatically. ### Fixed - 1004 signature error when url query has more than 2 elements. diff --git a/README.md b/README.md index f6c0780f..0b19ed5f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ If beta version works fine for a while, it will be merged into the upstream repo - Smoke Sensor - Contact Sensor - Leak Sensor +- For `Custom` project, `username` and `password` options are no longer need. The plugin will create a default user and authorize to all assets automatically. + ## Supported Tuya Devices diff --git a/config.schema.json b/config.schema.json index 6c887e94..95a9372d 100644 --- a/config.schema.json +++ b/config.schema.json @@ -54,12 +54,16 @@ "username": { "title": "Username", "type": "string", - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } }, "password": { "title": "Password", "type": "string", - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } }, "appSchema": { "title": "App", diff --git a/src/config.ts b/src/config.ts index ca8c67e4..89458d22 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,8 +30,6 @@ export const customOptionsSchema = { endpoint: { type: 'string', format: 'url', required: true }, accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, - username: { type: 'string', required: true }, - password: { type: 'string', required: true }, }, }; diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 39786b6f..17359382 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -126,7 +126,34 @@ export default class TuyaOpenAPI { return; } + /** + * 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: number, username: string, password: string, appSchema: string) { for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { @@ -158,6 +185,38 @@ export default class TuyaOpenAPI { return res; } + /** + * In 'Custom' project, Search user by username. + * @param username Username + * @returns + */ + async customGetUserInfo(username: string) { + 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: string, password: string, country_code = 1) { + const res = await this.post('/v1.0/iot-02/users', { + username, + password: Crypto.SHA256(password).toString().toLowerCase(), + country_code, + }); + return res; + } + + /** + * In 'Custom' project, login with user. + * @param username Username + * @param password Password + * @returns + */ async customLogin(username: string, password: string) { const res = await this.post('/v1.0/iot-03/users/login', { 'username': username, diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index cda345f8..748a2431 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -3,14 +3,23 @@ import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { - async getAssetList() { - const res = await this.api.get('/v1.0/iot-03/users/assets', { + async getAssetList(parent_asset_id = -1) { + // const res = await this.api.get('/v1.0/iot-03/users/assets', { + const res = await this.api.get(`/v1.0/iot-02/assets/${parent_asset_id}/sub-assets`, { 'page_no': 0, 'page_size': 100, }); return res; } + async authorizeAssetList(uid: string, asset_ids: string[] = [], authorized_children = false) { + const res = await this.api.post(`/v1.0/iot-03/users/${uid}/actions/batch-assets-authorized`, { + asset_ids: asset_ids.join(','), + authorized_children, + }); + return res; + } + async getAssetDeviceIDList(assetID: string) { let deviceIDs: string[] = []; const params = { diff --git a/src/platform.ts b/src/platform.ts index 6e774d25..7b5b2497 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -103,6 +103,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } // add accessories + this.log.info(`Got ${devices.length} device(s).`); for (const device of devices) { this.addAccessory(device); } @@ -126,24 +127,44 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return null; } + const DEFAULT_USER = 'homebridge'; + const DEFAULT_PASS = 'homebridge'; + let res; - const { endpoint, accessId, accessKey, username, password } = this.options; + const { endpoint, accessId, accessKey } = this.options; const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); const mq = new TuyaOpenMQ(api, '2.0', this.log); const deviceManager = new TuyaCustomDeviceManager(api, mq); - this.log.info('Log in to Tuya Cloud.'); - res = await api.customLogin(username, password); + this.log.info('Get token.'); + res = await api.getToken(); if (res.success === false) { - this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); - if (LOGIN_ERROR_MESSAGES[res.code]) { - this.log.error(LOGIN_ERROR_MESSAGES[res.code]); - } + this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`); return null; } - this.log.info('Start MQTT connection.'); - mq.start(); + + this.log.info(`Search default user "${DEFAULT_USER}"`); + res = await api.customGetUserInfo(DEFAULT_USER); + if (res.success === false) { + this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`); + return null; + } + + + if (!res.result.user_name) { + this.log.info(`Default user "${DEFAULT_USER}" not exist.`); + this.log.info(`Creating default user "${DEFAULT_USER}".`); + res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS); + if (res.success === false) { + this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`); + return null; + } + } else { + this.log.info(`Default user "${DEFAULT_USER}" exists.`); + } + const uid = res.result.user_id; + this.log.info('Fetching asset list.'); res = await deviceManager.getAssetList(); @@ -151,8 +172,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`); return null; } + const assetIDList: string[] = []; - for (const { asset_id, asset_name } of res.result.assets) { + for (const { asset_id, asset_name } of res.result.list) { this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`); assetIDList.push(asset_id); } @@ -162,6 +184,28 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return null; } + + this.log.info('Authorize asset list.'); + res = await deviceManager.authorizeAssetList(uid, assetIDList, true); + if (res.success === false) { + this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`); + return null; + } + + + this.log.info(`Log in with user "${DEFAULT_USER}".`); + res = await api.customLogin(DEFAULT_USER, DEFAULT_USER); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return null; + } + + this.log.info('Start MQTT connection.'); + mq.start(); + this.log.info('Fetching device list.'); const devices = await deviceManager.updateDevices(assetIDList); diff --git a/test/custom.test.ts b/test/custom.test.ts index 54c68c49..3e691c53 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -16,6 +16,11 @@ if (options.projectType === '1') { const customDeviceManager = new TuyaCustomDeviceManager(api, mq); describe('TuyaOpenAPI', () => { + test('getToken()', async () => { + const res = await api.getToken(); + expectSuccessResponse(res); + }); + test('customLogin()', async () => { await api.customLogin(options.username, options.password); expect(api.isLogin()).toBeTruthy(); @@ -33,7 +38,8 @@ if (options.projectType === '1') { test('getAssetList()', async () => { const res = await customDeviceManager.getAssetList(); expectSuccessResponse(res); - for (const { asset_id } of res.result.assets) { + const assets = res.result.list || res.result.assets; + for (const { asset_id } of assets) { assetIDList.push(asset_id); } });