mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-15 18:27:05 +00:00
Add Tuya WebSocket configuration and credentials
This commit is contained in:
@ -1,3 +1,6 @@
|
|||||||
import emailConfig from './email.config';
|
import emailConfig from './email.config';
|
||||||
import superAdminConfig from './super.admin.config';
|
import superAdminConfig from './super.admin.config';
|
||||||
export default [emailConfig, superAdminConfig];
|
import tuyaConfig from './tuya.config';
|
||||||
|
import oneSignalConfig from './onesignal.config';
|
||||||
|
|
||||||
|
export default [emailConfig, superAdminConfig, tuyaConfig, oneSignalConfig];
|
||||||
|
30
libs/common/src/config/tuya-web-socket-config/config.ts
Normal file
30
libs/common/src/config/tuya-web-socket-config/config.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export enum TuyaRegionConfigEnum {
|
||||||
|
CN = 'wss://mqe.tuyacn.com:8285/',
|
||||||
|
US = 'wss://mqe.tuyaus.com:8285/',
|
||||||
|
EU = 'wss://mqe.tuyaeu.com:8285/',
|
||||||
|
IN = 'wss://mqe.tuyain.com:8285/',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TUYA_PASULAR_ENV {
|
||||||
|
PROD = 'prod',
|
||||||
|
TEST = 'test',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TuyaEnvConfig = Object.freeze({
|
||||||
|
[TUYA_PASULAR_ENV.PROD]: {
|
||||||
|
name: TUYA_PASULAR_ENV.PROD,
|
||||||
|
value: 'event',
|
||||||
|
desc: 'online environment',
|
||||||
|
},
|
||||||
|
[TUYA_PASULAR_ENV.TEST]: {
|
||||||
|
name: TUYA_PASULAR_ENV.TEST,
|
||||||
|
value: 'event-test',
|
||||||
|
desc: 'test environment',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
type IEnvConfig = typeof TuyaEnvConfig;
|
||||||
|
export function getTuyaEnvConfig<K extends keyof IEnvConfig>(
|
||||||
|
env: TUYA_PASULAR_ENV,
|
||||||
|
): IEnvConfig[K] {
|
||||||
|
return TuyaEnvConfig[env];
|
||||||
|
}
|
214
libs/common/src/config/tuya-web-socket-config/index.ts
Normal file
214
libs/common/src/config/tuya-web-socket-config/index.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TUYA_PASULAR_ENV,
|
||||||
|
getTuyaEnvConfig,
|
||||||
|
TuyaRegionConfigEnum,
|
||||||
|
} from './config';
|
||||||
|
import { getTopicUrl, buildQuery, buildPassword, decrypt } from './utils';
|
||||||
|
|
||||||
|
type LoggerLevel = 'INFO' | 'ERROR';
|
||||||
|
|
||||||
|
interface IConfig {
|
||||||
|
accessId: string;
|
||||||
|
accessKey: string;
|
||||||
|
env: TUYA_PASULAR_ENV;
|
||||||
|
url: TuyaRegionConfigEnum;
|
||||||
|
|
||||||
|
timeout?: number;
|
||||||
|
maxRetryTimes?: number;
|
||||||
|
retryTimeout?: number;
|
||||||
|
logger?: (level: LoggerLevel, ...args: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TuyaMessageSubscribeWebsocket {
|
||||||
|
static URL = TuyaRegionConfigEnum;
|
||||||
|
static env = TUYA_PASULAR_ENV;
|
||||||
|
|
||||||
|
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: EventEmitter;
|
||||||
|
|
||||||
|
constructor(config: IConfig) {
|
||||||
|
this.config = Object.assign(
|
||||||
|
{
|
||||||
|
ackTimeoutMillis: 3000,
|
||||||
|
subscriptionType: 'Failover',
|
||||||
|
retryTimeout: 1000,
|
||||||
|
maxRetryTimes: 100,
|
||||||
|
timeout: 30000,
|
||||||
|
logger: console.log,
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
this.event = new EventEmitter();
|
||||||
|
this.retryTimes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this.server = this._connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public open(cb: (ws: WebSocket) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.open, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public message(cb: (ws: WebSocket, message: any) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.data, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ping(cb: (ws: WebSocket) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.ping, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public pong(cb: (ws: WebSocket) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.pong, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public reconnect(cb: (ws: WebSocket) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.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(TuyaMessageSubscribeWebsocket.error, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(cb: (ws: WebSocket) => void) {
|
||||||
|
this.event.on(TuyaMessageSubscribeWebsocket.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, env, url } = this.config;
|
||||||
|
const topicUrl = getTopicUrl(
|
||||||
|
url,
|
||||||
|
accessId,
|
||||||
|
getTuyaEnvConfig(env).value,
|
||||||
|
`?${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
|
||||||
|
? TuyaMessageSubscribeWebsocket.open
|
||||||
|
: TuyaMessageSubscribeWebsocket.reconnect,
|
||||||
|
this.server,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private subPing(server: WebSocket) {
|
||||||
|
server.on('ping', () => {
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.ping, this.server);
|
||||||
|
this.keepAlive(server);
|
||||||
|
server.pong(this.config.accessId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private subPong(server: WebSocket) {
|
||||||
|
server.on('pong', () => {
|
||||||
|
this.keepAlive(server);
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.pong, this.server);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private subMessage(server: WebSocket) {
|
||||||
|
server.on('message', (data: any) => {
|
||||||
|
try {
|
||||||
|
this.keepAlive(server);
|
||||||
|
const start = Date.now();
|
||||||
|
const obj = this.handleMessage(data);
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.data, this.server, obj);
|
||||||
|
const end = Date.now();
|
||||||
|
} catch (e) {
|
||||||
|
this.logger('ERROR', e);
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.error, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private subClose(server: WebSocket) {
|
||||||
|
server.on('close', (...data) => {
|
||||||
|
this._reconnect();
|
||||||
|
this.clearKeepAlive();
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.close, ...data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private subError(server: WebSocket) {
|
||||||
|
server.on('error', (e) => {
|
||||||
|
this.event.emit(TuyaMessageSubscribeWebsocket.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) {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
private logger(level: LoggerLevel, ...info: any) {
|
||||||
|
const realInfo = `${Date.now()} `;
|
||||||
|
this.config.logger && this.config.logger(level, realInfo, ...info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TuyaMessageSubscribeWebsocket;
|
51
libs/common/src/config/tuya-web-socket-config/utils.ts
Normal file
51
libs/common/src/config/tuya-web-socket-config/utils.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { MD5, AES, enc, mode, pad } from 'crypto-js';
|
||||||
|
|
||||||
|
export function getTopicUrl(
|
||||||
|
websocketUrl: string,
|
||||||
|
accessId: string,
|
||||||
|
env: string,
|
||||||
|
query: string,
|
||||||
|
) {
|
||||||
|
return `${websocketUrl}ws/v2/consumer/persistent/${accessId}/out/${env}/${accessId}-sub${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildQuery(query: { [key: string]: number | string }) {
|
||||||
|
return Object.keys(query)
|
||||||
|
.map((key) => `${key}=${encodeURIComponent(query[key])}`)
|
||||||
|
.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPassword(accessId: string, accessKey: string) {
|
||||||
|
const key = MD5(accessKey).toString();
|
||||||
|
return MD5(`${accessId}${key}`).toString().substr(8, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(data: string, accessKey: string) {
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 '';
|
||||||
|
}
|
||||||
|
}
|
9
libs/common/src/config/tuya.config.ts
Normal file
9
libs/common/src/config/tuya.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs(
|
||||||
|
'tuya-config',
|
||||||
|
(): Record<string, any> => ({
|
||||||
|
TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID,
|
||||||
|
TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY,
|
||||||
|
}),
|
||||||
|
);
|
Reference in New Issue
Block a user