mirror of
https://github.com/koush/scrypted.git
synced 2026-09-19 10:10:39 +01:00
Add Tuya Camera (and Doorbell Cameras) Support (#350)
* Added Tuya support for cameras. * Improve naming scheme, logging in, and some minor bugfixed * Added support for live notifications from Tuya using Pulse * Removed online from TuyaCameraLight and TuyaCamera since they are bugged atm. - improvements on variable motion detection values - deleted mqtt in favor or TuyaPulsar * Updated readme and changed from Tuya Plugin to Tuya Controller * added todos * Created Development.md for building - removed tsconfig.json - fixed motion detection issue - static video stream options
This commit is contained in:
4
plugins/tuya/.gitignore
vendored
Normal file
4
plugins/tuya/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
11
plugins/tuya/.npmignore
Normal file
11
plugins/tuya/.npmignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
dist/*.txt
|
||||
HAP-NodeJS
|
||||
.gitmodules
|
||||
22
plugins/tuya/.vscode/launch.json
vendored
Normal file
22
plugins/tuya/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/tuya/.vscode/settings.json
vendored
Normal file
4
plugins/tuya/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/tuya/.vscode/tasks.json
vendored
Normal file
20
plugins/tuya/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
15
plugins/tuya/Development.md
Normal file
15
plugins/tuya/Development.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Development
|
||||
This document describes how to build and run this plugin.
|
||||
|
||||
## npm commands
|
||||
- npm run scrypted-webpack
|
||||
- npm run scrypted-deploy
|
||||
- npm run scrypted-debug
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
- If using a remote server, edit .vscode/settings.json to specify the IP Address of the Scrypted server.
|
||||
- Launch Scrypted Debugger from the launch menu.
|
||||
17
plugins/tuya/README.md
Normal file
17
plugins/tuya/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Tuya for Scrypted
|
||||
|
||||
This is a Tuya controller that integrates Tuya devices, specifically cameras, into Scrypted.
|
||||
|
||||
The plugin will discover all the cameras within Tuya Cloud IoT project and report them to Scrypted, including motion events, for the ones that are supported.
|
||||
|
||||
## Retrieving Keys
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
## TODOs
|
||||
- Fix 2-way talk for supported platforms (Can only work with WebRTC since we only get one stream with RTSPS)
|
||||
- Add support for camera doorbells (Just need to implement doorbell notification)
|
||||
1101
plugins/tuya/package-lock.json
generated
Normal file
1101
plugins/tuya/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
plugins/tuya/package.json
Normal file
49
plugins/tuya/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@scrypted/tuya",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
"build": "scrypted-webpack",
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"Tuya"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Tuya Controller",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin",
|
||||
"@scrypted/webrtc"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^0.27.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"mqtt": "^4.3.7",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/node": "^16.6.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/ws": "^8.5.3"
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
||||
220
plugins/tuya/src/camera.ts
Normal file
220
plugins/tuya/src/camera.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, ScryptedInterface, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom } from "@scrypted/sdk";
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { TuyaController } from "./main";
|
||||
import { TuyaDeviceConfig } from "./tuya/const";
|
||||
import { TuyaDevice } from "./tuya/device";
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff {
|
||||
constructor(
|
||||
public camera: TuyaCamera,
|
||||
nativeId: string
|
||||
) {
|
||||
super(nativeId);
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
async turnOff(): Promise<void> {
|
||||
await this.setLightSwitch(false);
|
||||
}
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
await this.setLightSwitch(true);
|
||||
}
|
||||
|
||||
private async setLightSwitch(on: boolean) {
|
||||
const camera = this.camera.findCamera();
|
||||
const lightSwitchStatus = TuyaDevice.getLightSwitchStatus(camera);
|
||||
|
||||
if (camera.online && lightSwitchStatus) {
|
||||
await this.camera.controller.api.updateDevice(camera, [
|
||||
{
|
||||
code: lightSwitchStatus.code,
|
||||
value: on
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
updateState(camera?: TuyaDeviceConfig) {
|
||||
camera = camera || this.camera.findCamera();
|
||||
if (!camera)
|
||||
return;
|
||||
|
||||
this.on = TuyaDevice.getLightSwitchStatus(camera)?.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, VideoCamera, BinarySensor, MotionSensor, OnOff {
|
||||
cameraLight?: TuyaCameraLight
|
||||
private previousMotion?: any;
|
||||
private motionTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
public controller: TuyaController,
|
||||
nativeId: string,
|
||||
config: TuyaDeviceConfig
|
||||
) {
|
||||
super(nativeId);
|
||||
|
||||
if (this.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
this.binaryState = false;
|
||||
}
|
||||
|
||||
this.updateState(config);
|
||||
}
|
||||
|
||||
// Camera Light Provider
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
if (!this.cameraLight) {
|
||||
this.cameraLight = new TuyaCameraLight(this, nativeId);
|
||||
}
|
||||
|
||||
return this.cameraLight;
|
||||
}
|
||||
|
||||
// OnOff Status Indicator
|
||||
|
||||
async turnOff(): Promise<void> {
|
||||
this.setStatusIndicator(false);
|
||||
}
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
this.setStatusIndicator(true);
|
||||
}
|
||||
|
||||
private async setStatusIndicator(on: boolean) {
|
||||
const camera = this.findCamera();
|
||||
const statusIndicator = TuyaDevice.getStatusIndicator(camera);
|
||||
|
||||
if (statusIndicator) {
|
||||
await this.controller.api.updateDevice(camera, [
|
||||
{
|
||||
code: statusIndicator.code,
|
||||
value: on
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// VideoCamera
|
||||
|
||||
async getVideoStream(
|
||||
options?: MediaStreamOptions
|
||||
): Promise<MediaObject> {
|
||||
const vso = (await this.getVideoStreamOptions())[0];
|
||||
|
||||
// Always create new rtsp since it can only be used once and we only have 30 seconds before we can
|
||||
// use it.
|
||||
|
||||
const camera = this.findCamera();
|
||||
|
||||
if (!camera) {
|
||||
this.logger.w(`Could not find camera for ${this.name} to show stream.`);
|
||||
throw new Error(`Failed to stream ${this.name}: Camera not found.`);
|
||||
}
|
||||
|
||||
if (!camera.online) {
|
||||
this.logger.w(`${this.name} is currently offline. Will not be able to stream until device is back online.`);
|
||||
throw new Error(`Failed to stream ${this.name}: Camera is offline.`);
|
||||
}
|
||||
|
||||
const rtsps = await this.controller.api.getRTSPS(camera);
|
||||
|
||||
if (!rtsps) {
|
||||
this.logger.w("There was an error retreiving camera's rtsps for streamimg.");
|
||||
throw new Error(`Failed to capture stream for ${this.name}: RTSPS link not found.`);
|
||||
}
|
||||
|
||||
const mediaStreamUrl: MediaStreamUrl = {
|
||||
url: rtsps.url,
|
||||
container: 'rtsp',
|
||||
mediaStreamOptions: vso
|
||||
}
|
||||
return this.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl);
|
||||
}
|
||||
|
||||
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
container: 'rtsp',
|
||||
video: {
|
||||
codec: 'h264',
|
||||
},
|
||||
audio: {
|
||||
codec: 'pcm_ulaw'
|
||||
},
|
||||
source: 'cloud',
|
||||
tool: 'scrypted',
|
||||
userConfigurable: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Motion
|
||||
|
||||
// most cameras have have motion and doorbell press events, but dont notify when the event ends.
|
||||
// so set a timeout ourselves to reset the state.
|
||||
|
||||
triggerBinaryState() {
|
||||
// TODO: Implement doorbell chime
|
||||
// this.binaryState = true;
|
||||
// setTimeout(() => this.binaryState = false, 10000);
|
||||
}
|
||||
|
||||
// This will trigger a motion detected alert if it has no timeout. If there is a timeout, then
|
||||
// it will restart the timeout in order to turn off motion detected
|
||||
|
||||
triggerMotion() {
|
||||
const timeoutCallback = () => {
|
||||
this.motionDetected = false;
|
||||
this.motionTimeout = undefined;
|
||||
}
|
||||
if (!this.motionTimeout) {
|
||||
this.motionTimeout = setTimeout(timeoutCallback, 10 * 1000)
|
||||
this.motionDetected = true;
|
||||
} else {
|
||||
// Cancel the timeout and start again.
|
||||
clearTimeout(this.motionTimeout);
|
||||
this.motionTimeout = setTimeout(timeoutCallback, 10 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
findCamera() {
|
||||
return this.controller.api.cameras.find(device => device.id === this.nativeId);
|
||||
}
|
||||
|
||||
updateState(camera?: TuyaDeviceConfig) {
|
||||
camera = camera || this.findCamera();
|
||||
|
||||
if (!camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.on = TuyaDevice.getStatusIndicator(camera)?.value;
|
||||
|
||||
const hasMotionSwitchStatus = TuyaDevice.getMotionSwitch(camera) !== undefined;
|
||||
if (hasMotionSwitchStatus) {
|
||||
const movementDetectedStatus = TuyaDevice.getMotionDetectionStatus(camera);
|
||||
if (movementDetectedStatus) {
|
||||
if (!this.previousMotion) {
|
||||
this.previousMotion = movementDetectedStatus.value;
|
||||
} else if (this.previousMotion !== movementDetectedStatus.value) {
|
||||
this.previousMotion = movementDetectedStatus.value;
|
||||
this.triggerMotion();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getDevice(this.nativeLightId).updateState(camera);
|
||||
}
|
||||
|
||||
private get nativeLightId(): string {
|
||||
return `${this.nativeId}-light`;
|
||||
}
|
||||
|
||||
private get logger(): Logger {
|
||||
return deviceManager.getDeviceLogger(this.nativeId);
|
||||
}
|
||||
}
|
||||
284
plugins/tuya/src/main.ts
Normal file
284
plugins/tuya/src/main.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Device, DeviceDiscovery, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk';
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { StorageSettings } from '../../../common/src/settings';
|
||||
import { TuyaCloud } from './tuya/cloud';
|
||||
import { TuyaDevice } from './tuya/device';
|
||||
import { createInstanceableProviderPlugin } from '@scrypted/common/src/provider-plugin';
|
||||
import { TuyaCamera } from './camera';
|
||||
import { getTuyaPulsarEndpoint, TUYA_COUNTRIES } from './tuya/utils';
|
||||
import { TuyaPulsar, TuyaPulsarMessage } from './tuya/pulsar';
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export class TuyaController extends ScryptedDeviceBase implements DeviceProvider, DeviceDiscovery, Settings {
|
||||
api: TuyaCloud;
|
||||
pulsar: TuyaPulsar;
|
||||
cameras: Map<string, TuyaCamera> = new Map();
|
||||
|
||||
settingsStorage = new StorageSettings(this, {
|
||||
userId: {
|
||||
title: 'User Id',
|
||||
description: 'Required: You can find this information in Tuya IoT -> Cloud -> Devices -> Linked Devices.',
|
||||
onPut: async () => this.discoverDevices(0),
|
||||
},
|
||||
accessId: {
|
||||
title: 'Access Id',
|
||||
description: 'Requirerd: This is located on the main project.',
|
||||
onPut: async () => this.discoverDevices(0),
|
||||
},
|
||||
accessKey: {
|
||||
title: 'Access Key/Secret',
|
||||
description: 'Requirerd: This is located on the main project.',
|
||||
type: 'password',
|
||||
onPut: async () => this.discoverDevices(0),
|
||||
},
|
||||
country: {
|
||||
title: 'Country',
|
||||
description: 'Required: This is the country where you registered your devices.',
|
||||
type: 'string',
|
||||
choices: TUYA_COUNTRIES.map(value => value.country),
|
||||
onPut: async () => this.discoverDevices(0)
|
||||
}
|
||||
});
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
|
||||
async tryLogin() {
|
||||
const userId = this.settingsStorage.getItem('userId');
|
||||
const accessId = this.settingsStorage.getItem('accessId');
|
||||
const accessKey = this.settingsStorage.getItem('accessKey');
|
||||
const country = TUYA_COUNTRIES.find(value => value.country == this.settingsStorage.getItem('country'));
|
||||
|
||||
if (!userId ||
|
||||
!accessId ||
|
||||
!accessKey ||
|
||||
!country
|
||||
) {
|
||||
this.log.a('Enter your Tuya User Id, access Id, access key, and country to complete the setup.');
|
||||
throw new Error('User Id, access Id, access key, and country info are missing.');
|
||||
}
|
||||
|
||||
this.api = new TuyaCloud(
|
||||
userId,
|
||||
accessId,
|
||||
accessKey,
|
||||
country
|
||||
);
|
||||
|
||||
const success = await this.api.login();
|
||||
|
||||
if (!success) {
|
||||
this.log.e("Failed to log in with credentials.");
|
||||
this.api = undefined;
|
||||
this.pulsar?.stop();
|
||||
this.pulsar = undefined;
|
||||
throw new Error("Failed to log in with credentials, please check if everything is correct.");
|
||||
}
|
||||
|
||||
this.pulsar = new TuyaPulsar({
|
||||
accessId: accessId,
|
||||
accessKey: accessKey,
|
||||
url: getTuyaPulsarEndpoint(country)
|
||||
});
|
||||
|
||||
this.pulsar.open(() => {
|
||||
this.log.i(`TulsaPulse: opening connection.`)
|
||||
});
|
||||
|
||||
this.pulsar.message((ws, message) => {
|
||||
this.pulsar.ackMessage(message.messageId);
|
||||
this.log.i(`TuyaPulse: message received: ${message}`);
|
||||
const tuyaDevice = handleMessage(message);
|
||||
if (!tuyaDevice)
|
||||
return;
|
||||
tuyaDevice.updateState();
|
||||
});
|
||||
|
||||
this.pulsar.reconnect(() => {
|
||||
this.log.i(`TuyaPulse: restarting connection.`);
|
||||
});
|
||||
|
||||
this.pulsar.close((ws, ...args) => {
|
||||
this.log.w(`TuyaPulse: closed connection.`);
|
||||
});
|
||||
|
||||
this.pulsar.error((ws, error) => {
|
||||
this.log.e(`TuyaPulse: ${error}`);
|
||||
});
|
||||
|
||||
this.pulsar.start();
|
||||
|
||||
const handleMessage = (message: TuyaPulsarMessage) => {
|
||||
const data = message.payload.data;
|
||||
const { devId, productKey } = data;
|
||||
|
||||
const device = this.api.cameras?.find(c => c.id === devId);
|
||||
|
||||
let returnDevice = false;
|
||||
|
||||
if (data.bizCode) {
|
||||
if (!device && data.bizCode !== 'add') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.bizCode === 'online' || data.bizCode === 'offline') {
|
||||
// Device status changed
|
||||
const isOnline = data.bizCode === 'online';
|
||||
device.online = isOnline;
|
||||
returnDevice = true;
|
||||
} else if (data.bizCode === 'delete') {
|
||||
// Device needs to be deleted
|
||||
// - devId
|
||||
// - uid
|
||||
|
||||
const { uid } = data.bizData;
|
||||
} else if (data.bizCode === 'add') {
|
||||
// TODO: There is a new device added, refetch
|
||||
}
|
||||
} else {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatus = data.status || [];
|
||||
|
||||
newStatus.forEach(item => {
|
||||
const index = device.status.findIndex(status => status.code == item.code);
|
||||
if (index !== -1) {
|
||||
device.status[index].value = item.value
|
||||
}
|
||||
});
|
||||
|
||||
returnDevice = true;
|
||||
}
|
||||
|
||||
if (returnDevice) {
|
||||
return this.cameras.get(devId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.settingsStorage.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.settingsStorage.putSetting(key, value);
|
||||
}
|
||||
|
||||
async discoverDevices(duration: number) {
|
||||
await this.tryLogin();
|
||||
|
||||
this.log.clearAlerts();
|
||||
this.log.a("Successsfully logged in with credentials! Now discovering devices.");
|
||||
|
||||
if (!await this.api.fetchDevices()) {
|
||||
this.log.e("Could not fetch devices.");
|
||||
throw new Error("There was an error fetching devices.");
|
||||
}
|
||||
|
||||
const devices: Device[] = [];
|
||||
|
||||
// Camera Setup
|
||||
|
||||
for (const camera of this.api.cameras || []) {
|
||||
const nativeId = camera.id;
|
||||
|
||||
const device: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: camera.name,
|
||||
nativeId,
|
||||
info: {
|
||||
manufacturer: 'Tuya',
|
||||
model: camera.model,
|
||||
serialNumber: nativeId
|
||||
},
|
||||
type: TuyaDevice.isDoorbell(camera)
|
||||
? ScryptedDeviceType.Doorbell
|
||||
: ScryptedDeviceType.Camera,
|
||||
interfaces: [
|
||||
ScryptedInterface.VideoCamera,
|
||||
]
|
||||
};
|
||||
|
||||
if (TuyaDevice.isDoorbell(camera)) {
|
||||
device.interfaces.push(ScryptedInterface.BinarySensor);
|
||||
}
|
||||
|
||||
if (TuyaDevice.hasStatusIndicator(camera)) {
|
||||
device.interfaces.push(ScryptedInterface.OnOff);
|
||||
}
|
||||
|
||||
if (TuyaDevice.hasMotionDetection(camera)) {
|
||||
device.interfaces.push(ScryptedInterface.MotionSensor);
|
||||
}
|
||||
|
||||
// Device Provider
|
||||
|
||||
if (TuyaDevice.hasLightSwitch(camera)) {
|
||||
device.interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
}
|
||||
|
||||
devices.push(device);
|
||||
}
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.nativeId,
|
||||
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) {
|
||||
if (!TuyaDevice.hasLightSwitch(camera)) {
|
||||
continue;
|
||||
}
|
||||
const nativeId = camera.id + '-light';
|
||||
const device: Device = {
|
||||
providerNativeId: camera.id,
|
||||
name: camera.name + ' Light',
|
||||
nativeId,
|
||||
info: {
|
||||
manufacturer: 'Tuya',
|
||||
model: camera.model,
|
||||
serialNumber: camera.id,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff,
|
||||
],
|
||||
type: ScryptedDeviceType.Light,
|
||||
}
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: camera.id,
|
||||
devices: [device]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<TuyaCamera> {
|
||||
if (this.cameras.has(nativeId)) {
|
||||
return this.cameras.get(nativeId);
|
||||
}
|
||||
|
||||
const camera = this.api.cameras.find(camera => camera.id === nativeId);
|
||||
if (camera) {
|
||||
const ret = new TuyaCamera(this, nativeId, camera);
|
||||
this.cameras.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
throw new Error('device not found?');
|
||||
}
|
||||
}
|
||||
|
||||
export default createInstanceableProviderPlugin("Tuya", nativeId => new TuyaController(nativeId));
|
||||
240
plugins/tuya/src/tuya/cloud.ts
Normal file
240
plugins/tuya/src/tuya/cloud.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { Axios, Method } from "axios";
|
||||
import { HmacSHA256, SHA256, lib } from 'crypto-js';
|
||||
import { getTuyaCloudEndpoint, TuyaSupportedCountry } from "./utils";
|
||||
import { DeviceFunction, TuyaDeviceStatus, RTSPToken, TuyaDeviceConfig, TuyaResponse } from "./const";
|
||||
|
||||
interface Session {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
tokenExpiresAt: Date;
|
||||
uid: string
|
||||
}
|
||||
|
||||
export class TuyaCloud {
|
||||
|
||||
// Tuya IoT Cloud API
|
||||
|
||||
private readonly nonce: string;
|
||||
private session?: Session = undefined;
|
||||
private client: Axios;
|
||||
|
||||
private _cameras: TuyaDeviceConfig[] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly userId: string,
|
||||
private readonly clientId: string,
|
||||
private readonly secret: string,
|
||||
private readonly country: TuyaSupportedCountry
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.clientId = clientId;
|
||||
this.secret = secret;
|
||||
this.nonce = lib.WordArray.random(16).toString();
|
||||
this.country = country;
|
||||
this.client = new Axios({
|
||||
baseURL: getTuyaCloudEndpoint(this.country),
|
||||
timeout: 5 * 1e3
|
||||
});
|
||||
this._cameras = undefined;
|
||||
}
|
||||
|
||||
public async login(): Promise<boolean> {
|
||||
await this.refreshAccessTokenIfNeeded();
|
||||
|
||||
return this.isLoggedIn();
|
||||
}
|
||||
|
||||
public isLoggedIn(): boolean {
|
||||
return this.session !== undefined && this.session.tokenExpiresAt.getTime() > Date.now();
|
||||
}
|
||||
|
||||
// Set Device Status
|
||||
|
||||
public async updateDevice(
|
||||
device: TuyaDeviceConfig,
|
||||
statuses: TuyaDeviceStatus[]
|
||||
): Promise<boolean> {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await this.post<boolean>(
|
||||
`/v1.0/devices/${device.id}/commands`,
|
||||
{
|
||||
commands: statuses
|
||||
}
|
||||
);
|
||||
|
||||
return result.success && result.result;
|
||||
}
|
||||
|
||||
// Get Devices
|
||||
|
||||
public async fetchDevices(): Promise<boolean> {
|
||||
let response = await this.get<TuyaDeviceConfig[]>(`/v1.0/users/${this.userId}/devices`);
|
||||
|
||||
if (!response.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let devicesState = response.result;
|
||||
|
||||
for (const state of devicesState) {
|
||||
let response = await this.get<DeviceFunction[]>(`/v1.0/devices/${state.id}/functions`);
|
||||
if (!response.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
state.functions = response.result;
|
||||
}
|
||||
|
||||
this._cameras = devicesState.filter(element => element.category === 'sp');
|
||||
return true;
|
||||
}
|
||||
|
||||
public get cameras(): TuyaDeviceConfig[] | undefined {
|
||||
return this._cameras;
|
||||
}
|
||||
|
||||
// Camera Functions
|
||||
|
||||
public async getRTSPS(camera: TuyaDeviceConfig): Promise<RTSPToken | undefined> {
|
||||
interface RTSPResponse {
|
||||
url: string
|
||||
}
|
||||
|
||||
const response = await this.post<RTSPResponse>(
|
||||
`/v1.0/devices/${camera.id}/stream/actions/allocate`,
|
||||
{ type: 'rtsp' }
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
return {
|
||||
url: response.result.url,
|
||||
expires: new Date(response.t + 30 * 1000) // This will expire in 30 seconds.
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public getSessionUserId(): string | undefined {
|
||||
return this.session?.uid;
|
||||
}
|
||||
|
||||
// Tuya IoT Cloud Requests API
|
||||
|
||||
public async get<T>(
|
||||
path: string,
|
||||
query: { [k: string]: any } = {},
|
||||
): Promise<TuyaResponse<T>> {
|
||||
return this.request<T>('GET', path, query);
|
||||
}
|
||||
|
||||
public async post<T>(
|
||||
path: string,
|
||||
body: { [k: string]: any } = {}
|
||||
): Promise<TuyaResponse<T>> {
|
||||
return this.request<T>('POST', path, {}, body);
|
||||
}
|
||||
|
||||
private async request<T = any>(
|
||||
method: Method,
|
||||
path: string,
|
||||
query: { [k: string]: any } = {},
|
||||
body: { [k: string]: any } = {}
|
||||
): Promise<TuyaResponse<T>> {
|
||||
await this.refreshAccessTokenIfNeeded();
|
||||
|
||||
const timestamp = Date.now().toString();
|
||||
const headers = { client_id: this.clientId };
|
||||
|
||||
const stringToSign = this.getStringToSign(method, path, query, headers, body);
|
||||
const sign = HmacSHA256(
|
||||
this.clientId + this.session.accessToken + timestamp + this.nonce + stringToSign,
|
||||
this.secret
|
||||
)
|
||||
.toString()
|
||||
.toUpperCase();
|
||||
|
||||
let requestHeaders = {
|
||||
'client_id': this.clientId,
|
||||
'sign': sign,
|
||||
'sign_method': 'HMAC-SHA256',
|
||||
't': timestamp,
|
||||
'access_token': this.session.accessToken,
|
||||
'Signature-Headers': Object.keys(headers).join(':'),
|
||||
'nonce': this.nonce
|
||||
};
|
||||
|
||||
return this.client.request<TuyaResponse<T>>({
|
||||
method,
|
||||
url: path,
|
||||
data: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
|
||||
params: query,
|
||||
headers: requestHeaders,
|
||||
responseType: 'json',
|
||||
transformResponse: (data) => JSON.parse(data)
|
||||
})
|
||||
.then(value => { return value.data });
|
||||
}
|
||||
|
||||
private getStringToSign(
|
||||
method: Method,
|
||||
path: string,
|
||||
query: { [k: string]: any } = {},
|
||||
headers: { [k: string]: string } = {},
|
||||
body: { [k: string]: any } = {}
|
||||
): string {
|
||||
const isQueryEmpty = Object.keys(query).length == 0;
|
||||
const isHeaderEmpty = Object.keys(headers).length == 0;
|
||||
const isBodyEmpty = Object.keys(body).length == 0;
|
||||
const httpMethod = method.toUpperCase();
|
||||
const url = path + (isQueryEmpty ? '' : '?' + Object.keys(query).map(key => `${key}=${query[key]}`).join('&'));
|
||||
const contentHashed = SHA256(isBodyEmpty ? '' : JSON.stringify(body)).toString();
|
||||
const headersParsed = Object.keys(headers).map(key => `${key}:${headers[key]}`).join('\n');
|
||||
const headersStr = isHeaderEmpty ? '' : headersParsed + '\n'
|
||||
const signStr = [httpMethod, contentHashed, headersStr, url].join('\n');
|
||||
return signStr
|
||||
}
|
||||
|
||||
private async refreshAccessTokenIfNeeded() {
|
||||
if (this.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string
|
||||
|
||||
if (!this.session) {
|
||||
url = '/v1.0/token?grant_type=1'
|
||||
} else {
|
||||
url = `/v1.0/token/${this.session.refreshToken}`
|
||||
}
|
||||
|
||||
const timestamp = new Date().getTime().toString();
|
||||
const stringToSign = this.getStringToSign('GET', url);
|
||||
const signString = HmacSHA256(this.clientId + timestamp + stringToSign, this.secret).toString().toUpperCase();
|
||||
|
||||
const headers = {
|
||||
t: timestamp,
|
||||
sign_method: 'HMAC-SHA256',
|
||||
client_id: this.clientId,
|
||||
sign: signString,
|
||||
};
|
||||
|
||||
let { data } = await this.client.get(url,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
let objData = JSON.parse(data);
|
||||
|
||||
const newExpiration = new Date(Date.now() + objData.result.expire_time * 1000);
|
||||
|
||||
this.session = {
|
||||
accessToken: objData.result.access_token,
|
||||
refreshToken: objData.result.refresh_token,
|
||||
tokenExpiresAt: newExpiration,
|
||||
uid: objData.result.uid
|
||||
};
|
||||
}
|
||||
}
|
||||
74
plugins/tuya/src/tuya/const.ts
Normal file
74
plugins/tuya/src/tuya/const.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type } from "os";
|
||||
|
||||
export interface TuyaResponse<T> {
|
||||
success: boolean
|
||||
t: number
|
||||
result: T
|
||||
}
|
||||
|
||||
export interface TuyaDeviceConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
local_key: string;
|
||||
category: string;
|
||||
product_id: string;
|
||||
product_name: string;
|
||||
sub: boolean;
|
||||
uuid: string;
|
||||
online: boolean;
|
||||
icon: string;
|
||||
ip: string;
|
||||
time_zone: string;
|
||||
active_time: number;
|
||||
create_time: number;
|
||||
update_time: number;
|
||||
status: TuyaDeviceStatus[];
|
||||
functions: DeviceFunction[];
|
||||
|
||||
// Not necessary?
|
||||
|
||||
uid: string;
|
||||
biz_type: number;
|
||||
model: string;
|
||||
owner_id: string;
|
||||
}
|
||||
|
||||
export interface TuyaDeviceStatus {
|
||||
code: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface DeviceFunction {
|
||||
code: string;
|
||||
type: string;
|
||||
values: string;
|
||||
desc: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface RTSPToken {
|
||||
url: string;
|
||||
expires: Date;
|
||||
}
|
||||
|
||||
export interface MQTTConfig {
|
||||
url: string;
|
||||
client_id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
source_topic: string;
|
||||
sink_topic: string;
|
||||
expire_topic: string;
|
||||
}
|
||||
|
||||
// From Unify Protect Api:
|
||||
// This type declaration make all properties optional recursively including nested objects. This should
|
||||
// only be used on JSON objects only. Otherwise...you're going to end up with class methods marked as
|
||||
// optional as well. Credit for this belongs to: https://github.com/joonhocho/tsdef. #Grateful
|
||||
// export type DeepPartial<T> = {
|
||||
// [P in keyof T]?: T[P] extends Array<infer I> ? Array<DeepPartial<I>> : DeepPartial<T[P]>
|
||||
// };
|
||||
|
||||
// export type ProtectTuyaDeviceConfig = Readonly<TuyaDeviceInterface>;
|
||||
// export type ProtectTuyaDeviceConfigPartial = DeepPartial<TuyaDeviceInterface>;
|
||||
// export type ProtectTuyaDeviceStatus = Readonly<TuyaDeviceStatus>;
|
||||
65
plugins/tuya/src/tuya/device.ts
Normal file
65
plugins/tuya/src/tuya/device.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { TuyaDeviceStatus, TuyaDeviceConfig as TuyaDeviceConfig } from "./const";
|
||||
|
||||
export namespace TuyaDevice {
|
||||
|
||||
// MARK: Switch Light
|
||||
|
||||
export function hasLightSwitch(camera: TuyaDeviceConfig): boolean {
|
||||
return getLightSwitchStatus(camera) !== undefined;
|
||||
}
|
||||
|
||||
export function getLightSwitchStatus(camera: TuyaDeviceConfig): TuyaDeviceStatus | undefined {
|
||||
const lightStatusCode = [
|
||||
'floodlight_switch', // Devices with floodlight switch
|
||||
];
|
||||
|
||||
return getStatus(camera, lightStatusCode);
|
||||
}
|
||||
|
||||
// MARK: Status Indicator
|
||||
|
||||
export function hasStatusIndicator(camera: TuyaDeviceConfig): boolean {
|
||||
return getStatusIndicator(camera) !== undefined;
|
||||
}
|
||||
|
||||
export function getStatusIndicator(camera: TuyaDeviceConfig): TuyaDeviceStatus | undefined {
|
||||
return getStatus(camera, ['basic_indicator']);
|
||||
}
|
||||
|
||||
// MARK: Doorbell
|
||||
|
||||
export function isDoorbell(camera: TuyaDeviceConfig): boolean {
|
||||
return getDoorbellStatus(camera) !== undefined;
|
||||
}
|
||||
|
||||
export function getDoorbellStatus(camera) : TuyaDeviceStatus | undefined {
|
||||
const doorbellStatus = getStatus(camera, ['dorbell_chime']);
|
||||
return doorbellStatus?.value !== undefined ? doorbellStatus : undefined;
|
||||
}
|
||||
|
||||
// MARK: Motion Detection
|
||||
|
||||
export function hasMotionDetection(camera: TuyaDeviceConfig): boolean {
|
||||
return getMotionSwitch(camera) !== undefined;
|
||||
}
|
||||
|
||||
export function getMotionSwitch(camera: TuyaDeviceConfig) {
|
||||
const motionSwitchCodes = [
|
||||
'motion_switch',
|
||||
'pir_sensitivity'
|
||||
]
|
||||
return getStatus(camera, motionSwitchCodes);
|
||||
}
|
||||
|
||||
export function getMotionDetectionStatus(camera: TuyaDeviceConfig) {
|
||||
const motionDetectionCodes = [
|
||||
'movement_detect_pic'
|
||||
];
|
||||
|
||||
return getStatus(camera, motionDetectionCodes);
|
||||
}
|
||||
|
||||
function getStatus(camera: TuyaDeviceConfig, statusCode: string[]) : TuyaDeviceStatus | undefined {
|
||||
return camera.status.find(value => statusCode.includes(value.code));
|
||||
}
|
||||
}
|
||||
265
plugins/tuya/src/tuya/pulsar.ts
Normal file
265
plugins/tuya/src/tuya/pulsar.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// From Tuya Pulsar (Node.js)
|
||||
// https://developer.tuya.com/en/docs/iot/Pulsar-SDK-get-message-nodejs?id=Kawfmtxp8yscg
|
||||
|
||||
import Event from 'events';
|
||||
import WebSocket from 'ws';
|
||||
import { MD5, AES, enc, mode, pad } from 'crypto-js';
|
||||
|
||||
export interface TuyaPulsarMessage {
|
||||
payload: {
|
||||
data: {
|
||||
devId: string;
|
||||
productKey: string;
|
||||
bizCode?: string;
|
||||
bizData?: any;
|
||||
status?: StatusItem[]
|
||||
}
|
||||
protocol: number;
|
||||
pv: string;
|
||||
sign: string;
|
||||
t: number;
|
||||
}
|
||||
messageId: string;
|
||||
properties: any;
|
||||
publishTime: string;
|
||||
redeliveryCount: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface StatusItem {
|
||||
code: string;
|
||||
value: any;
|
||||
t: number;
|
||||
// "data point": string
|
||||
}
|
||||
|
||||
interface IConfig {
|
||||
accessId: string;
|
||||
accessKey: string;
|
||||
url: string;
|
||||
|
||||
timeout?: number;
|
||||
maxRetryTimes?: number;
|
||||
retryTimeout?: number;
|
||||
}
|
||||
|
||||
export class TuyaPulsar {
|
||||
static data = 'TUTA_DATA';
|
||||
static error = 'TUYA_ERROR';
|
||||
static open = 'TUYA_OPEN';
|
||||
static close = 'TUYA_CLOSE';
|
||||
static reconnect = 'TUYA_RECONNECT';
|
||||
static ping = 'TUYA_PING';
|
||||
static pong = 'TUYA_PONG';
|
||||
|
||||
private config: IConfig;
|
||||
private server?: WebSocket;
|
||||
private timer: any;
|
||||
private retryTimes: number;
|
||||
private event: Event;
|
||||
|
||||
constructor(config: IConfig) {
|
||||
this.config = Object.assign(
|
||||
{
|
||||
ackTimeoutMillis: 3000,
|
||||
subscriptionType: 'Failover',
|
||||
retryTimeout: 1000,
|
||||
maxRetryTimes: 100,
|
||||
timeout: 30000,
|
||||
logger: console.log,
|
||||
},
|
||||
config
|
||||
);
|
||||
this.event = new Event();
|
||||
this.retryTimes = 0;
|
||||
}
|
||||
|
||||
public start() {
|
||||
this.server = this._connect();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.server?.terminate();
|
||||
}
|
||||
|
||||
public open(cb: (ws: WebSocket) => void) {
|
||||
this.event.on(TuyaPulsar.open, cb);
|
||||
}
|
||||
|
||||
public message(cb: (ws: WebSocket, message: any) => void) {
|
||||
this.event.on(TuyaPulsar.data, cb);
|
||||
}
|
||||
|
||||
public ping(cb: (ws: WebSocket) => void) {
|
||||
this.event.on(TuyaPulsar.ping, cb);
|
||||
}
|
||||
|
||||
public pong(cb: (ws: WebSocket) => void) {
|
||||
this.event.on(TuyaPulsar.pong, cb);
|
||||
}
|
||||
|
||||
public reconnect(cb: (ws: WebSocket) => void) {
|
||||
this.event.on(TuyaPulsar.reconnect, cb);
|
||||
}
|
||||
|
||||
public ackMessage(messageId: string) {
|
||||
this.server && this.server.send(JSON.stringify({ messageId }));
|
||||
}
|
||||
|
||||
public error(cb: (ws: WebSocket, error: any) => void) {
|
||||
this.event.on(TuyaPulsar.error, cb);
|
||||
}
|
||||
|
||||
public close(cb: (ws: WebSocket) => void) {
|
||||
this.event.on(TuyaPulsar.close, cb);
|
||||
}
|
||||
|
||||
private _reconnect() {
|
||||
if (this.config.maxRetryTimes && this.retryTimes < this.config.maxRetryTimes) {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
this.retryTimes++;
|
||||
this._connect(false);
|
||||
}, this.config.retryTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private _connect(isInit = true) {
|
||||
const { accessId, accessKey, url } = this.config;
|
||||
const topicUrl = getTopicUrl(
|
||||
url,
|
||||
accessId,
|
||||
'event',
|
||||
`?${buildQuery({ subscriptionType: 'Failover', ackTimeoutMillis: 30000 })}`,
|
||||
);
|
||||
const password = buildPassword(accessId, accessKey);
|
||||
this.server = new WebSocket(topicUrl, {
|
||||
rejectUnauthorized: false,
|
||||
headers: { username: accessId, password },
|
||||
});
|
||||
this.subOpen(this.server, isInit);
|
||||
this.subMessage(this.server);
|
||||
this.subPing(this.server);
|
||||
this.subPong(this.server);
|
||||
this.subError(this.server);
|
||||
this.subClose(this.server);
|
||||
return this.server;
|
||||
}
|
||||
|
||||
private subOpen(server: WebSocket, isInit = true) {
|
||||
server.on('open', () => {
|
||||
if (server.readyState === server.OPEN) {
|
||||
this.retryTimes = 0;
|
||||
}
|
||||
this.keepAlive(server);
|
||||
this.event.emit(
|
||||
isInit ? TuyaPulsar.open : TuyaPulsar.reconnect,
|
||||
this.server,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private subPing(server: WebSocket) {
|
||||
server.on('ping', () => {
|
||||
this.event.emit(TuyaPulsar.ping, this.server);
|
||||
this.keepAlive(server);
|
||||
server.pong(this.config.accessId);
|
||||
});
|
||||
}
|
||||
|
||||
private subPong(server: WebSocket) {
|
||||
server.on('pong', () => {
|
||||
this.keepAlive(server);
|
||||
this.event.emit(TuyaPulsar.pong, this.server);
|
||||
});
|
||||
}
|
||||
|
||||
private subMessage(server: WebSocket) {
|
||||
server.on('message', (data: any) => {
|
||||
try {
|
||||
this.keepAlive(server);
|
||||
const obj = this.handleMessage(data);
|
||||
this.event.emit(TuyaPulsar.data, this.server, obj);
|
||||
} catch (e) {
|
||||
this.event.emit(TuyaPulsar.error, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private subClose(server: WebSocket) {
|
||||
server.on('close', (...data) => {
|
||||
this._reconnect();
|
||||
this.clearKeepAlive();
|
||||
this.event.emit(TuyaPulsar.close, ...data);
|
||||
});
|
||||
}
|
||||
|
||||
private subError(server: WebSocket) {
|
||||
server.on('error', (e) => {
|
||||
this.event.emit(TuyaPulsar.error, this.server, e);
|
||||
});
|
||||
}
|
||||
|
||||
private clearKeepAlive() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
private keepAlive(server: WebSocket) {
|
||||
this.clearKeepAlive();
|
||||
this.timer = setTimeout(() => {
|
||||
server.ping(this.config.accessId);
|
||||
}, this.config.timeout);
|
||||
}
|
||||
|
||||
private handleMessage(data: string): TuyaPulsarMessage {
|
||||
const { payload, ...others } = JSON.parse(data);
|
||||
const pStr = Buffer.from(payload, 'base64').toString('utf-8');
|
||||
const pJson = JSON.parse(pStr);
|
||||
pJson.data = decrypt(pJson.data, this.config.accessKey);
|
||||
return { payload: pJson, ...others };
|
||||
}
|
||||
}
|
||||
|
||||
function getTopicUrl(websocketUrl: string, accessId: string, env: string, query: string) {
|
||||
return `${websocketUrl}ws/v2/consumer/persistent/${accessId}/out/${env}/${accessId}-sub${query}`;
|
||||
}
|
||||
|
||||
function buildQuery(query: { [key: string]: number | string }) {
|
||||
return Object.keys(query)
|
||||
.map((key) => `${key}=${encodeURIComponent(query[key])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
function buildPassword(accessId: string, accessKey: string) {
|
||||
const key = MD5(accessKey).toString();
|
||||
return MD5(`${accessId}${key}`).toString().substr(8, 16);
|
||||
}
|
||||
|
||||
function decrypt(data: string, accessKey: string): TuyaPulsarMessage | undefined {
|
||||
try {
|
||||
const realKey = enc.Utf8.parse(accessKey.substring(8, 24));
|
||||
const json = AES.decrypt(data, realKey, {
|
||||
mode: mode.ECB,
|
||||
padding: pad.Pkcs7,
|
||||
});
|
||||
const dataStr = enc.Utf8.stringify(json).toString();
|
||||
return JSON.parse(dataStr);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function encrypt(data: any, accessKey: string) {
|
||||
try {
|
||||
const realKey = enc.Utf8.parse(accessKey.substring(8, 24));
|
||||
const realData = JSON.stringify(data);
|
||||
const retData = AES.encrypt(realData, realKey, {
|
||||
mode: mode.ECB,
|
||||
padding: pad.Pkcs7,
|
||||
}).toString();
|
||||
return retData;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
286
plugins/tuya/src/tuya/utils.ts
Normal file
286
plugins/tuya/src/tuya/utils.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
enum EndpointGroup {
|
||||
Europe,
|
||||
America,
|
||||
India,
|
||||
China
|
||||
}
|
||||
|
||||
export interface TuyaSupportedCountry {
|
||||
country: string
|
||||
countryCode: number
|
||||
endpointGroup: EndpointGroup
|
||||
}
|
||||
|
||||
export const TUYA_COUNTRIES: TuyaSupportedCountry[] = [
|
||||
{ country: 'Afghanistan', countryCode: 93, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Albania', countryCode: 355, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Algeria', countryCode: 213, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'American Samoa', countryCode: 1684, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Andorra', countryCode: 376, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Angola', countryCode: 244, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Anguilla', countryCode: 1264, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Antarctica', countryCode: 672, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Antigua and Barbuda', countryCode: 1268, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Argentina', countryCode: 54, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Armenia', countryCode: 374, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Aruba', countryCode: 297, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Australia', countryCode: 61, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Austria', countryCode: 43, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Azerbaijan', countryCode: 994, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bahamas', countryCode: 1242, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bahrain', countryCode: 973, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bangladesh', countryCode: 880, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Barbados', countryCode: 1246, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Belarus', countryCode: 375, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Belgium', countryCode: 32, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Belize', countryCode: 501, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Benin', countryCode: 229, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bermuda', countryCode: 1441, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bhutan', countryCode: 975, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bolivia', countryCode: 591, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Bosnia and Herzegovina', countryCode: 387, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Botswana', countryCode: 267, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Brazil', countryCode: 55, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'British Indian Ocean Territory', countryCode: 246, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'British Virgin Islands', countryCode: 1284, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Brunei', countryCode: 673, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Bulgaria', countryCode: 359, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Burkina Faso', countryCode: 226, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Burundi', countryCode: 257, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Cabo Verde', countryCode: 238, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Cambodia', countryCode: 855, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Cameroon', countryCode: 237, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Canada', countryCode: 1, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Cayman Islands', countryCode: 1345, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Central African Republic', countryCode: 236, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Chad', countryCode: 235, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Chile', countryCode: 56, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'China', countryCode: 86, endpointGroup: EndpointGroup.China },
|
||||
{ country: 'Colombia', countryCode: 57, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Comoros', countryCode: 269, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Cook Islands', countryCode: 682, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Costa Rica', countryCode: 506, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Croatia', countryCode: 385, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Curacao', countryCode: 5999, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Cyprus', countryCode: 357, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Czech Republic', countryCode: 420, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Côte d’Ivoire', countryCode: 225, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Democratic Republic of the Congo', countryCode: 243, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Denmark', countryCode: 45, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Djibouti', countryCode: 253, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Dominica', countryCode: 1767, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Dominican Republic (1-809)', countryCode: 1809, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Dominican Republic (1-829)', countryCode: 1829, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Dominican Republic (1-849)', countryCode: 1849, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'East Timor', countryCode: 670, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Ecuador', countryCode: 593, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Egypt', countryCode: 20, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'El Salvador', countryCode: 503, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Equatorial Guinea', countryCode: 240, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Eritrea', countryCode: 291, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Estonia', countryCode: 372, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Ethiopia', countryCode: 251, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Falkland Islands', countryCode: 500, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Faroe Islands', countryCode: 298, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Fiji', countryCode: 679, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Finland', countryCode: 358, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'France', countryCode: 33, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'French Guiana', countryCode: 594, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'French Polynesia', countryCode: 689, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Gabon', countryCode: 241, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Gambia', countryCode: 220, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Georgia', countryCode: 995, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Germany', countryCode: 49, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Ghana', countryCode: 233, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Gibraltar', countryCode: 350, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Greece', countryCode: 30, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Greenland', countryCode: 299, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Grenada', countryCode: 1473, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Guam', countryCode: 1671, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Guatemala', countryCode: 502, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Guinea', countryCode: 224, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Guinea-Bissau', countryCode: 245, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Guyana', countryCode: 592, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Haiti', countryCode: 509, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Honduras', countryCode: 504, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Hong Kong', countryCode: 852, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Hungary', countryCode: 36, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Iceland', countryCode: 354, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'India', countryCode: 91, endpointGroup: EndpointGroup.India },
|
||||
{ country: 'Indonesia', countryCode: 62, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Iraq', countryCode: 964, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Ireland', countryCode: 353, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Israel', countryCode: 972, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Italy', countryCode: 39, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Jamaica', countryCode: 1876, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Japan', countryCode: 81, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Jordan', countryCode: 962, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Kenya', countryCode: 254, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Kiribati', countryCode: 686, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Kuwait', countryCode: 965, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Kyrgyzstan', countryCode: 996, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Laos', countryCode: 856, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Latvia', countryCode: 371, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Lebanon', countryCode: 961, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Lesotho', countryCode: 266, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Liberia', countryCode: 231, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Libya', countryCode: 218, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Liechtenstein', countryCode: 423, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Lithuania', countryCode: 370, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Luxembourg', countryCode: 352, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Macao', countryCode: 853, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Macedonia', countryCode: 389, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Madagascar', countryCode: 261, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Malawi', countryCode: 265, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Malaysia', countryCode: 60, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Maldives', countryCode: 960, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mali', countryCode: 223, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Malta', countryCode: 356, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Marshall Islands', countryCode: 692, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Martinique', countryCode: 596, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mauritania', countryCode: 222, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mauritius', countryCode: 230, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mayotte', countryCode: 262, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mexico', countryCode: 52, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Micronesia', countryCode: 691, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Moldova', countryCode: 373, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Monaco', countryCode: 377, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mongolia', countryCode: 976, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Montenegro', countryCode: 382, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Montserrat', countryCode: 1664, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Morocco', countryCode: 212, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Mozambique', countryCode: 258, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Myanmar', countryCode: 95, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Namibia', countryCode: 264, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Nauru', countryCode: 674, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Nepal', countryCode: 977, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Netherlands', countryCode: 31, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'New Caledonia', countryCode: 687, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'New Zealand', countryCode: 64, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Nicaragua', countryCode: 505, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Niger', countryCode: 227, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Nigeria', countryCode: 234, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Niue', countryCode: 683, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Northern Mariana Islands', countryCode: 1670, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Norway', countryCode: 47, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Oman', countryCode: 968, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Pakistan', countryCode: 92, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Palau', countryCode: 680, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Palestine', countryCode: 970, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Panama', countryCode: 507, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Papua New Guinea', countryCode: 675, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Paraguay', countryCode: 595, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Peru', countryCode: 51, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Philippines', countryCode: 63, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Poland', countryCode: 48, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Portugal', countryCode: 351, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Puerto Rico', countryCode: 1787, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Qatar', countryCode: 974, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Republic of the Congo', countryCode: 242, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Reunion', countryCode: 262, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Romania', countryCode: 40, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Russia', countryCode: 7, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Rwanda', countryCode: 250, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saint Kitts and Nevis', countryCode: 1869, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saint Lucia', countryCode: 1758, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saint Martin', countryCode: 590, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saint Pierre and Miquelon', countryCode: 508, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saint Vincent and the Grenadines', countryCode: 1784, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Samoa', countryCode: 685, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'San Marino', countryCode: 378, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Saudi Arabia', countryCode: 966, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Sao Tome and Principe', countryCode: 239, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Senegal', countryCode: 221, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Serbia', countryCode: 381, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Seychelles', countryCode: 248, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Sierra Leone', countryCode: 232, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Singapore', countryCode: 65, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Sint Maarten', countryCode: 1721, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Slovakia', countryCode: 421, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Slovenia', countryCode: 386, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Solomon Islands', countryCode: 677, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Somalia', countryCode: 252, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'South Africa', countryCode: 27, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'South Korea', countryCode: 82, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Spain', countryCode: 34, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Sri Lanka', countryCode: 94, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Suriname', countryCode: 597, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Svalbard and Jan Mayen', countryCode: 4779, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Swaziland', countryCode: 268, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Sweden', countryCode: 46, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Switzerland', countryCode: 41, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Taiwan', countryCode: 886, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Tajikistan', countryCode: 992, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Tanzania', countryCode: 255, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Thailand', countryCode: 66, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Togo', countryCode: 228, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Tokelau', countryCode: 690, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Tonga', countryCode: 676, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Trinidad and Tobago', countryCode: 1868, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Tunisia', countryCode: 216, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Turkey', countryCode: 90, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Turkmenistan', countryCode: 993, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Turks and Caicos Islands', countryCode: 1649, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Tuvalu', countryCode: 688, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'United States of America', countryCode: 1, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'U.S. Virgin Islands', countryCode: 1340, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Uganda', countryCode: 256, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Ukraine', countryCode: 380, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'United Arab Emirates', countryCode: 971, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'United Kingdom', countryCode: 44, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Uruguay', countryCode: 598, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Uzbekistan', countryCode: 998, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Vanuatu', countryCode: 678, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Vatican', countryCode: 379, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Venezuela', countryCode: 58, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Vietnam', countryCode: 84, endpointGroup: EndpointGroup.America },
|
||||
{ country: 'Wallis and Futuna', countryCode: 681, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Western Sahara', countryCode: 212, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Yemen', countryCode: 967, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Zambia', countryCode: 260, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Zimbabwe', countryCode: 263, endpointGroup: EndpointGroup.Europe },
|
||||
{ country: 'Åland Islands', countryCode: 35818, endpointGroup: EndpointGroup.America },
|
||||
];
|
||||
|
||||
export function getEndPointWithCountryCode(code: number) {
|
||||
const item = TUYA_COUNTRIES.find(item => {
|
||||
return item.countryCode === code;
|
||||
});
|
||||
return item ? item.endpointGroup : EndpointGroup.Europe;
|
||||
}
|
||||
|
||||
export function getTuyaCloudEndpoint(country: TuyaSupportedCountry): string {
|
||||
const AMERICA = 'https://openapi.tuyaus.com';
|
||||
const EUROPE = 'https://openapi.tuyaeu.com';
|
||||
const INDIA = 'https://openapi.tuyain.com';
|
||||
const CHINA = 'https://openapi.tuyacn.com';
|
||||
|
||||
switch (country.endpointGroup) {
|
||||
case EndpointGroup.America:
|
||||
return AMERICA;
|
||||
case EndpointGroup.Europe:
|
||||
return EUROPE;
|
||||
case EndpointGroup.India:
|
||||
return INDIA;
|
||||
case EndpointGroup.China:
|
||||
return CHINA;
|
||||
};
|
||||
}
|
||||
|
||||
export function getTuyaPulsarEndpoint(country: TuyaSupportedCountry): string {
|
||||
const CHINA = 'wss://mqe.tuyacn.com:8285/';
|
||||
const AMERICA = 'wss://mqe.tuyaus.com:8285/';
|
||||
const EUROPE = 'wss://mqe.tuyaeu.com:8285/';
|
||||
const INDIA = 'wss://mqe.tuyain.com:8285/';
|
||||
|
||||
switch (country.endpointGroup) {
|
||||
case EndpointGroup.America:
|
||||
return AMERICA;
|
||||
case EndpointGroup.Europe:
|
||||
return EUROPE;
|
||||
case EndpointGroup.India:
|
||||
return INDIA;
|
||||
case EndpointGroup.China:
|
||||
return CHINA;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user