UNPKG

node-red-contrib-tplink-tapo-connect-api

Version:

This unofficial node-RED node allows connection to TP-Link Tapo devices. This project has been enhanced with AI support to enable new features. Starting with v0.50, we have added support for the KLAP protocol. To prioritize the operation of this node, we

240 lines 10 kB
"use strict"; /** * Unified device connection and communication manager * Eliminates duplication between plug and bulb implementations */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceManager = void 0; const auth_1 = require("./auth"); const klap_auth_1 = require("./klap-auth"); /** * Centralized device communication manager * Handles authentication, session management, and request queuing */ class DeviceManager { constructor(ip, credentials, deviceType, options = {}) { var _a, _b, _c, _d; this.ip = ip; this.deviceType = deviceType; this.useKlap = false; this.requestQueue = Promise.resolve(); this.lastRequestTime = 0; this.isConnected = false; this.sessionErrorPatterns = { klapSessionErrors: ['klap 1002', 'klap -1012'], generalSessionErrors: ['session expired', 'invalid terminal uuid'], busyErrors: ['device busy', 'command timing issue'] }; this.auth = new auth_1.TapoAuth(ip, credentials); this.klapAuth = new klap_auth_1.KlapAuth(ip, credentials); // Set default options this.options = { maxRetries: (_a = options.maxRetries) !== null && _a !== void 0 ? _a : 2, retryDelay: (_b = options.retryDelay) !== null && _b !== void 0 ? _b : 2000, minRequestInterval: (_c = options.minRequestInterval) !== null && _c !== void 0 ? _c : 1000, enableLegacyFallback: (_d = options.enableLegacyFallback) !== null && _d !== void 0 ? _d : true }; } /** * Check if device is reachable */ async checkDeviceConnectivity() { try { const response = await fetch(`http://${this.ip}`, { method: 'HEAD', signal: AbortSignal.timeout(5000) }); return response.status < 500; } catch (error) { console.log(`${this.deviceType} connectivity check failed:`, error); return false; } } /** * Determine if errors indicate legacy device */ isLikelyLegacyDevice(klapError, securePassthroughError) { if (!this.options.enableLegacyFallback) { return false; } // KLAP connection refused suggests older firmware const klapConnectionRefused = (klapError === null || klapError === void 0 ? void 0 : klapError.message.toLowerCase().includes('connection refused')) || (klapError === null || klapError === void 0 ? void 0 : klapError.message.toLowerCase().includes('econnrefused')); // Secure Passthrough timeout suggests newer firmware with disabled fallback const securePassthroughTimeout = (securePassthroughError === null || securePassthroughError === void 0 ? void 0 : securePassthroughError.message.toLowerCase().includes('timeout')) || (securePassthroughError === null || securePassthroughError === void 0 ? void 0 : securePassthroughError.message.toLowerCase().includes('network error')); return Boolean(klapConnectionRefused && securePassthroughTimeout); } /** * Classify error types for appropriate handling */ classifyError(error) { const errorMessage = error.message.toLowerCase(); if (this.sessionErrorPatterns.klapSessionErrors.some(pattern => errorMessage.includes(pattern)) || this.sessionErrorPatterns.generalSessionErrors.some(pattern => errorMessage.includes(pattern))) { return 'session'; } if (this.sessionErrorPatterns.busyErrors.some(pattern => errorMessage.includes(pattern))) { return 'busy'; } if (errorMessage.includes('network') || errorMessage.includes('timeout') || errorMessage.includes('connection') || errorMessage.includes('econnrefused')) { return 'network'; } return 'unknown'; } /** * Establish connection to device */ async connect() { console.log(`${this.deviceType}.connect() called`); // Check basic connectivity first console.log('Checking device connectivity...'); const isReachable = await this.checkDeviceConnectivity(); if (!isReachable) { console.log('Device connectivity check failed, but proceeding with authentication...'); } else { console.log('Device is reachable, proceeding with authentication...'); } let klapError = null; let securePassthroughError = null; for (let attempt = 1; attempt <= this.options.maxRetries; attempt++) { console.log(`Connection attempt ${attempt}/${this.options.maxRetries}`); // Try KLAP first (modern protocol) try { console.log('Trying KLAP authentication...'); await this.klapAuth.authenticate(); this.useKlap = true; this.isConnected = true; console.log('KLAP authentication successful'); return; } catch (error) { klapError = error; console.log('KLAP failed:', error); } // Fallback to Secure Passthrough try { console.log('Trying Secure Passthrough authentication...'); await this.auth.authenticate(); this.useKlap = false; this.isConnected = true; console.log('Secure Passthrough authentication successful'); return; } catch (error) { securePassthroughError = error; console.log('Secure Passthrough failed:', error); } // Check for legacy device pattern if (this.isLikelyLegacyDevice(klapError, securePassthroughError)) { console.log('Device appears to be legacy firmware - connection methods may be limited'); } if (attempt < this.options.maxRetries) { console.log(`Both protocols failed, retrying in ${this.options.retryDelay}ms...`); await new Promise(resolve => setTimeout(resolve, this.options.retryDelay)); } } console.error('All authentication attempts failed'); throw new Error(`Failed to connect to ${this.deviceType} after ${this.options.maxRetries} attempts. ` + `KLAP: ${klapError === null || klapError === void 0 ? void 0 : klapError.message}; Secure Passthrough: ${securePassthroughError === null || securePassthroughError === void 0 ? void 0 : securePassthroughError.message}`); } /** * Disconnect from device */ async disconnect() { if (this.useKlap) { this.klapAuth.clearSession(); } this.isConnected = false; // Note: Secure Passthrough doesn't require explicit disconnect } /** * Check if currently connected */ isDeviceConnected() { return this.isConnected; } /** * Send request with automatic session management and rate limiting */ async sendRequest(request) { if (!this.isConnected) { throw new Error('Device not connected. Call connect() first.'); } return this.requestQueue = this.requestQueue.then(async () => { // Rate limiting const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.options.minRequestInterval) { await new Promise(resolve => setTimeout(resolve, this.options.minRequestInterval - timeSinceLastRequest)); } this.lastRequestTime = Date.now(); try { const result = await this.executeRequest(request); return { error_code: 0, result }; } catch (error) { const errorType = this.classifyError(error); // Handle session errors with re-authentication if (errorType === 'session') { console.log('Session error detected, attempting re-authentication...'); try { await this.reconnectAfterSessionError(); console.log('Re-authentication successful, retrying request...'); // Retry the request const result = await this.executeRequest(request); return { error_code: 0, result }; } catch (reAuthError) { console.log('Re-authentication failed:', reAuthError); throw reAuthError; } } throw error; } }); } /** * Execute the actual request using the appropriate protocol */ async executeRequest(request) { if (this.useKlap) { if (!this.klapAuth.isAuthenticated()) { throw new Error('KLAP session not authenticated'); } return await this.klapAuth.secureRequest(request); } else { if (!this.auth.isAuthenticated()) { throw new Error('Secure Passthrough session not authenticated'); } return await this.auth.secureRequest(request); } } /** * Handle session error by re-authenticating */ async reconnectAfterSessionError() { if (this.useKlap) { this.klapAuth.clearSession(); await this.klapAuth.authenticate(); } else { await this.auth.authenticate(); } } /** * Get current protocol information */ getConnectionInfo() { return { protocol: this.useKlap ? 'KLAP' : 'SecurePassthrough', connected: this.isConnected }; } } exports.DeviceManager = DeviceManager; //# sourceMappingURL=device-manager.js.map