UNPKG

tsvesync

Version:

A TypeScript library for interacting with VeSync smart home devices

330 lines (292 loc) 9.38 kB
/** * Helper functions for VeSync API */ import axios from 'axios'; import crypto from 'crypto'; import { VeSync } from './vesync'; import { logger } from './logger'; // API configuration - Always use US endpoint let _apiBaseUrl = 'https://smartapi.vesync.com'; export function getApiBaseUrl(): string { return _apiBaseUrl; } export function setApiBaseUrl(url: string): void { _apiBaseUrl = url; } export const API_RATE_LIMIT = 30; export const API_TIMEOUT = 15000; export const APP_VERSION = '2.8.6'; export const PHONE_BRAND = 'SM N9005'; export const PHONE_OS = 'Android'; export const USER_TYPE = '1'; export const DEFAULT_TZ = 'America/New_York'; export const DEFAULT_REGION = 'US'; export const MOBILE_ID = '1234567890123456'; export const BYPASS_HEADER_UA = 'okhttp/3.12.1'; export interface RequestBody { acceptLanguage?: string; accountID?: string; appVersion?: string; cid?: string; configModule?: string; debugMode?: boolean; deviceRegion?: string; email?: string; method?: string; password?: string; phoneBrand?: string; phoneOS?: string; timeZone?: string; token?: string; traceId?: string; userType?: string; uuid?: string; status?: string; [key: string]: any; } export class Helpers { static shouldRedact = true; /** * Calculate MD5 hash */ static hashPassword(text: string): string { return crypto.createHash('md5').update(text).digest('hex'); } /** * Build header for legacy api GET requests */ static reqHeaders(manager: VeSync): Record<string, string> { if (!manager.accountId || !manager.token) { throw new Error('Manager accountId and token must be set'); } return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': BYPASS_HEADER_UA, 'accept-language': 'en', 'accountId': manager.accountId, 'appVersion': APP_VERSION, 'content-type': 'application/json', 'tk': manager.token, 'tz': manager.timeZone }; } /** * Build header for api requests on 'bypass' endpoint */ static reqHeaderBypass(): Record<string, string> { return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': BYPASS_HEADER_UA }; } /** * Return universal keys for body of api requests */ static reqBodyBase(manager: VeSync): Record<string, string> { return { 'timeZone': manager.timeZone, 'acceptLanguage': 'en' }; } /** * Keys for authenticating api requests */ static reqBodyAuth(manager: VeSync): Record<string, any> { if (!manager.accountId || !manager.token) { throw new Error('Manager accountId and token must be set'); } return { 'accountID': manager.accountId, 'token': manager.token }; } /** * Detail keys for api requests */ static reqBodyDetails(): Record<string, string> { return { 'appVersion': APP_VERSION, 'phoneBrand': PHONE_BRAND, 'phoneOS': PHONE_OS, 'traceId': Date.now().toString() }; } /** * Builder for body of api requests */ static reqBody(manager: VeSync, type: string): Record<string, any> { const body = { ...this.reqBodyBase(manager) }; if (type === 'login') { return { ...body, ...this.reqBodyDetails(), email: manager.username, password: this.hashPassword(manager.password), devToken: '', userType: USER_TYPE, method: 'login' }; } const authBody = { ...body, ...this.reqBodyAuth(manager) }; if (type === 'devicestatus') { return authBody; } const fullBody = { ...authBody, ...this.reqBodyDetails() }; switch (type) { case 'devicelist': return { ...fullBody, method: 'devices', pageNo: '1', pageSize: '100' }; case 'devicedetail': return { ...fullBody, method: 'devicedetail', mobileId: MOBILE_ID }; case 'bypass': return { ...fullBody, method: 'bypass' }; case 'bypassV2': return { ...fullBody, deviceRegion: DEFAULT_REGION, method: 'bypassV2' }; case 'bypass_config': return { ...fullBody, method: 'firmwareUpdateInfo' }; default: return fullBody; } } /** * Call VeSync API */ static async callApi( endpoint: string, method: string, data: any = null, headers: Record<string, string> = {}, manager: VeSync ): Promise<[any, number]> { try { // Ensure API base URL is properly set if (!_apiBaseUrl || _apiBaseUrl === 'undefined') { logger.error('API base URL is not properly configured. Setting to default US endpoint...'); setApiBaseUrl('https://smartapi.vesync.com'); } const url = _apiBaseUrl + endpoint; logger.debug(`Making API call to: ${url}`); const response = await axios({ method, url, data, headers, timeout: API_TIMEOUT }); return [response.data, response.status]; } catch (error: any) { if (error.response) { const responseData = error.response.data; // Check for token expiration if (responseData?.code === 4001004 || responseData?.msg === "token expired") { logger.debug('Token expired, attempting to re-login...'); // Re-login if (await manager.login()) { // Retry the original request logger.debug('Re-login successful, retrying original request...'); return await this.callApi(endpoint, method, data, headers, manager); } } // Log specific error details for debugging logger.error('API call failed with response:', { status: error.response.status, code: responseData?.code, message: responseData?.msg, url: _apiBaseUrl + endpoint }); return [responseData, error.response.status]; } logger.error('API call failed:', { code: error.code, message: error.message, url: _apiBaseUrl + endpoint }); return [null, 0]; } } /** * Calculate hex value from energy response */ static calculateHex(hexStr: string): string { if (!hexStr || !hexStr.includes(':')) { return '0'; } const [prefix, value] = hexStr.split(':'); if (!prefix || !value) { return '0'; } try { const decoded = Buffer.from(value, 'hex'); return decoded.readFloatBE(0).toString(); } catch (error) { logger.debug('Error decoding hex value:', error); return '0'; } } /** * Build energy dictionary from API response */ static buildEnergyDict(result: any): Record<string, any> { if (!result) { return {}; } return { energy_consumption_of_today: result.energyConsumptionOfToday || 0, cost_per_kwh: result.costPerKWH || 0, max_energy: result.maxEnergy || 0, total_energy: result.totalEnergy || 0, data: result.data || [], energy_consumption: result.energy || 0, start_time: result.startTime || '', end_time: result.endTime || '' }; } /** * Build configuration dictionary from API response */ static buildConfigDict(result: any): Record<string, any> { if (!result) { return {}; } return { configModule: result.configModule || '', firmwareVersion: result.currentFirmVersion || '', deviceRegion: result.deviceRegion || '', debugMode: result.debugMode || false, deviceTimezone: result.deviceTimezone || DEFAULT_TZ, ...result }; } /** * Calculate MD5 hash */ static md5(text: string): string { return crypto.createHash('md5').update(text).digest('hex'); } }