From c613067df44d7eb31b9de99df1b7c4acf53a0a44 Mon Sep 17 00:00:00 2001 From: Erik Bautista Date: Wed, 24 Aug 2022 23:07:09 -0500 Subject: [PATCH] Fix race condition for Tuya Devices (#351) * fix README.md * fix race condition * increase version to 0.0.4 --- plugins/tuya/README.md | 2 +- plugins/tuya/package.json | 2 +- plugins/tuya/src/camera.ts | 24 ++++++++----- plugins/tuya/src/main.ts | 62 +++++++++++++++------------------- plugins/tuya/src/tuya/cloud.ts | 3 ++ 5 files changed, 48 insertions(+), 45 deletions(-) diff --git a/plugins/tuya/README.md b/plugins/tuya/README.md index 371aafc80..80cf04f6f 100644 --- a/plugins/tuya/README.md +++ b/plugins/tuya/README.md @@ -8,7 +8,7 @@ The plugin will discover all the cameras within Tuya Cloud IoT project and repor In order to retrieve `Access Id` and `Access Key`, you must follow the guide below: - [Using Smart Home PaaS (TuyaSmart, SmartLife, ect...)](https://developer.tuya.com/en/docs/iot/Platform_Configuration_smarthome?id=Kamcgamwoevrx&_source=6435717a3be1bc67fdd1f6699a1a59ac) -Follow this [guide](https://developer.tuya.com/en/docs/iot/Configuration_Guide_custom?id=Kamcfx6g5uyot&_source=bdc927ff355af92156074d47e00d6191) +- [If you're using custom development](https://developer.tuya.com/en/docs/iot/Configuration_Guide_custom?id=Kamcfx6g5uyot&_source=bdc927ff355af92156074d47e00d6191) Once you have retreived both the `Access Id` and `Access Key` from the project, you can get the `User Id` by going to Tuya Cloud IoT -> Select the Project -> Devices -> Link Tuya App Account -> and then get the UID. diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json index ead3bec66..7b17be51d 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -43,5 +43,5 @@ "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3" }, - "version": "0.0.3" + "version": "0.0.4" } diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index afb790b34..8527893dc 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -24,10 +24,16 @@ export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff { private async setLightSwitch(on: boolean) { const camera = this.camera.findCamera(); + + if (!camera) { + this.log.w(`Camera was not found for ${this.name}`); + return; + } + const lightSwitchStatus = TuyaDevice.getLightSwitchStatus(camera); if (camera.online && lightSwitchStatus) { - await this.camera.controller.api.updateDevice(camera, [ + await this.camera.controller.cloud?.updateDevice(camera, [ { code: lightSwitchStatus.code, value: on @@ -56,11 +62,6 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi config: TuyaDeviceConfig ) { super(nativeId); - - if (this.interfaces.includes(ScryptedInterface.BinarySensor)) { - this.binaryState = false; - } - this.updateState(config); } @@ -86,10 +87,15 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi private async setStatusIndicator(on: boolean) { const camera = this.findCamera(); + if (!camera) { + this.log.w(`Camera was not found for ${this.name}`); + return; + } + const statusIndicator = TuyaDevice.getStatusIndicator(camera); if (statusIndicator) { - await this.controller.api.updateDevice(camera, [ + await this.controller.cloud?.updateDevice(camera, [ { code: statusIndicator.code, value: on @@ -120,7 +126,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi throw new Error(`Failed to stream ${this.name}: Camera is offline.`); } - const rtsps = await this.controller.api.getRTSPS(camera); + const rtsps = await this.controller.cloud?.getRTSPS(camera); if (!rtsps) { this.logger.w("There was an error retreiving camera's rtsps for streamimg."); @@ -183,7 +189,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi } findCamera() { - return this.controller.api.cameras.find(device => device.id === this.nativeId); + return this.controller.cloud?.cameras?.find(device => device.id === this.nativeId); } updateState(camera?: TuyaDeviceConfig) { diff --git a/plugins/tuya/src/main.ts b/plugins/tuya/src/main.ts index c89bcdf05..f753a49ac 100644 --- a/plugins/tuya/src/main.ts +++ b/plugins/tuya/src/main.ts @@ -11,8 +11,8 @@ import { TuyaPulsar, TuyaPulsarMessage } from './tuya/pulsar'; const { deviceManager } = sdk; export class TuyaController extends ScryptedDeviceBase implements DeviceProvider, DeviceDiscovery, Settings { - api: TuyaCloud; - pulsar: TuyaPulsar; + cloud?: TuyaCloud; + pulsar?: TuyaPulsar; cameras: Map = new Map(); settingsStorage = new StorageSettings(this, { @@ -61,20 +61,18 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider throw new Error('User Id, access Id, access key, and country info are missing.'); } - this.api = new TuyaCloud( + this.cloud = new TuyaCloud( userId, accessId, accessKey, country ); - const success = await this.api.login(); + const success = await this.cloud.login(); if (!success) { this.log.e("Failed to log in with credentials."); - this.api = undefined; - this.pulsar?.stop(); - this.pulsar = undefined; + this.cloud = undefined; throw new Error("Failed to log in with credentials, please check if everything is correct."); } @@ -89,7 +87,7 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider }); this.pulsar.message((ws, message) => { - this.pulsar.ackMessage(message.messageId); + this.pulsar?.ackMessage(message.messageId); this.log.i(`TuyaPulse: message received: ${message}`); const tuyaDevice = handleMessage(message); if (!tuyaDevice) @@ -115,21 +113,15 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider const data = message.payload.data; const { devId, productKey } = data; - const device = this.api.cameras?.find(c => c.id === devId); - - let returnDevice = false; + const device = this.cloud?.cameras?.find(c => c.id === devId); if (data.bizCode) { - if (!device && data.bizCode !== 'add') { - return; - } - - if (data.bizCode === 'online' || data.bizCode === 'offline') { + if (device && (data.bizCode === 'online' || data.bizCode === 'offline')) { // Device status changed const isOnline = data.bizCode === 'online'; device.online = isOnline; - returnDevice = true; - } else if (data.bizCode === 'delete') { + return this.cameras.get(devId); + } else if (device && data.bizCode === 'delete') { // Device needs to be deleted // - devId // - uid @@ -152,10 +144,6 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider } }); - returnDevice = true; - } - - if (returnDevice) { return this.cameras.get(devId); } } @@ -175,7 +163,13 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider this.log.clearAlerts(); this.log.a("Successsfully logged in with credentials! Now discovering devices."); - if (!await this.api.fetchDevices()) { + const cloud = this.cloud; + + if (!cloud) { + throw new Error("There was an error: TuyaCloud not initialized"); + } + + if (!await cloud.fetchDevices()) { this.log.e("Could not fetch devices."); throw new Error("There was an error fetching devices."); } @@ -184,7 +178,7 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider // Camera Setup - for (const camera of this.api.cameras || []) { + for (const camera of cloud.cameras || []) { const nativeId = camera.id; const device: Device = { @@ -200,7 +194,7 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera, interfaces: [ - ScryptedInterface.VideoCamera, + ScryptedInterface.VideoCamera ] }; @@ -230,15 +224,9 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider devices }); - // Update devices with new state - - for (const device of devices) { - this.getDevice(device.nativeId).then(device => device?.updateState()); - } - // Handle any camera device that have a light switch - for (const camera of this.api.cameras) { + for (const camera of cloud.cameras || []) { if (!TuyaDevice.hasLightSwitch(camera)) { continue; } @@ -263,14 +251,20 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider devices: [device] }); } + + // Update devices with new state + + for (const device of devices) { + await this.getDevice(device.nativeId).then(device => device?.updateState()); + } } - async getDevice(nativeId: string): Promise { + async getDevice(nativeId: string) { if (this.cameras.has(nativeId)) { return this.cameras.get(nativeId); } - const camera = this.api.cameras.find(camera => camera.id === nativeId); + const camera = this.cloud?.cameras?.find(camera => camera.id === nativeId); if (camera) { const ret = new TuyaCamera(this, nativeId, camera); this.cameras.set(nativeId, ret); diff --git a/plugins/tuya/src/tuya/cloud.ts b/plugins/tuya/src/tuya/cloud.ts index 15f85c95b..e7bc0ce99 100644 --- a/plugins/tuya/src/tuya/cloud.ts +++ b/plugins/tuya/src/tuya/cloud.ts @@ -145,6 +145,9 @@ export class TuyaCloud { body: { [k: string]: any } = {} ): Promise> { await this.refreshAccessTokenIfNeeded(); + if (!this.session) { + throw new Error(`Token session not available for TuyaCloud.`); + } const timestamp = Date.now().toString(); const headers = { client_id: this.clientId };