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:
Erik Bautista
2022-08-23 21:24:21 -05:00
committed by GitHub
parent f3c83f57c4
commit dfb80e4930
16 changed files with 2677 additions and 0 deletions

4
plugins/tuya/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
out/
node_modules/
dist/

11
plugins/tuya/.npmignore Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"scrypted.debugHost": "127.0.0.1",
}

20
plugins/tuya/.vscode/tasks.json vendored Normal file
View 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}",
},
]
}

View 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
View 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

File diff suppressed because it is too large Load Diff

49
plugins/tuya/package.json Normal file
View 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
View 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
View 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));

View 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
};
}
}

View 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>;

View 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));
}
}

View 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 '';
}
}

View 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 dIvoire', 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;
};
}