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

422 lines 15.2 kB
"use strict"; /** * Base class for Tapo Smart Bulbs */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseBulb = void 0; const types_1 = require("../../types"); const bulb_1 = require("../../types/bulb"); const auth_1 = require("../../core/auth"); const klap_auth_1 = require("../../core/klap-auth"); class BaseBulb extends types_1.BaseTapoDevice { constructor(ip, credentials) { super(ip, credentials); this.useKlap = false; this.requestQueue = Promise.resolve(); this.lastRequestTime = 0; this.minRequestInterval = 1000; // Minimum interval between requests this.auth = new auth_1.TapoAuth(ip, credentials); this.klapAuth = new klap_auth_1.KlapAuth(ip, credentials); } /** * Get device capabilities */ getCapabilities() { const model = this.getDeviceModel(); const capabilities = bulb_1.BULB_CAPABILITIES[model]; if (!capabilities) { // Fallback to L510 capabilities if model not found return bulb_1.BULB_CAPABILITIES['L510']; } return capabilities; } async checkDeviceConnectivity() { try { const response = await fetch(`http://${this.ip}`, { method: 'HEAD', signal: AbortSignal.timeout(5000) }); return response.status < 500; } catch (error) { console.log('Device connectivity check failed:', error); return false; } } async connect() { console.log(`${this.getDeviceModel()}Bulb.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...'); } const maxRetries = 2; let klapError; let securePassthroughError; for (let attempt = 1; attempt <= maxRetries; attempt++) { console.log(`Connection attempt ${attempt}/${maxRetries}`); // Try KLAP first try { console.log('Trying KLAP authentication...'); await this.klapAuth.authenticate(); this.useKlap = true; console.log('KLAP authentication successful'); return; } catch (error) { klapError = error; console.log('KLAP failed:', error); } // If KLAP fails, try Secure Passthrough if (!this.useKlap) { try { console.log('Trying Secure Passthrough authentication...'); await this.auth.authenticate(); this.useKlap = false; console.log('Secure Passthrough authentication successful'); return; } catch (fallbackError) { securePassthroughError = fallbackError; console.log('Secure Passthrough failed:', fallbackError); } } if (attempt < maxRetries) { console.log(`Both modern protocols failed, retrying in 2 seconds...`); await new Promise(resolve => setTimeout(resolve, 2000)); } } console.error('All authentication attempts failed'); throw new Error(`Failed to connect to bulb after ${maxRetries} attempts. ` + `KLAP: ${klapError === null || klapError === void 0 ? void 0 : klapError.message}; Secure Passthrough: ${securePassthroughError === null || securePassthroughError === void 0 ? void 0 : securePassthroughError.message}`); } async disconnect() { if (this.useKlap) { this.klapAuth.clearSession(); } // Note: Secure Passthrough doesn't require explicit disconnect } /** * Send request with rate limiting and session management */ async sendRequest(request) { return this.requestQueue = this.requestQueue.then(async () => { // Rate limiting const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.minRequestInterval) { await new Promise(resolve => setTimeout(resolve, this.minRequestInterval - timeSinceLastRequest)); } this.lastRequestTime = Date.now(); try { if (this.useKlap) { if (!this.klapAuth.isAuthenticated()) { throw new Error('Device not connected. Call connect() first.'); } const result = await this.klapAuth.secureRequest(request); return { error_code: 0, result }; } else { if (!this.auth.isAuthenticated()) { throw new Error('Device not connected. Call connect() first.'); } const result = await this.auth.secureRequest(request); return { error_code: 0, result }; } } catch (error) { // Check for session errors and attempt re-authentication const errorMessage = error.message.toLowerCase(); if (errorMessage.includes('klap 1002') || errorMessage.includes('session expired')) { console.log('Session error detected, attempting re-authentication...'); try { if (this.useKlap) { this.klapAuth.clearSession(); await this.klapAuth.authenticate(); } else { await this.auth.authenticate(); } console.log('Re-authentication successful, retrying request...'); // Retry the request if (this.useKlap) { const result = await this.klapAuth.secureRequest(request); return { error_code: 0, result }; } else { const result = await this.auth.secureRequest(request); return { error_code: 0, result }; } } catch (reAuthError) { console.log('Re-authentication failed:', reAuthError); throw reAuthError; } } throw error; } }); } // ============================================================================ // Basic Device Control // ============================================================================ /** * Turn bulb on */ async turnOn() { const request = { method: 'set_device_info', params: { device_on: true } }; await this.sendRequest(request); } /** * Turn bulb off */ async turnOff() { const request = { method: 'set_device_info', params: { device_on: false } }; await this.sendRequest(request); } /** * Toggle bulb state */ async toggle() { const deviceInfo = await this.getDeviceInfo(); if (deviceInfo.device_on) { await this.turnOff(); } else { await this.turnOn(); } } /** * Convenience aliases following Python API pattern */ async on() { await this.turnOn(); } async off() { await this.turnOff(); } /** * Check if bulb is on */ async isOn() { const deviceInfo = await this.getDeviceInfo(); return deviceInfo.device_on; } // ============================================================================ // Device Information // ============================================================================ /** * Get device information */ async getDeviceInfo() { const request = { method: 'get_device_info' }; const response = await this.sendRequest(request); return response.result; } // ============================================================================ // Brightness Control // ============================================================================ /** * Set brightness level */ async setBrightness(brightness) { const capabilities = this.getCapabilities(); if (!capabilities.brightness) { throw new Error(`${this.getDeviceModel()} does not support brightness control`); } if (brightness < capabilities.minBrightness || brightness > capabilities.maxBrightness) { throw new Error(`Brightness must be between ${capabilities.minBrightness}-${capabilities.maxBrightness}`); } const request = { method: 'set_device_info', params: { brightness } }; await this.sendRequest(request); } /** * Get current brightness */ async getBrightness() { const deviceInfo = await this.getDeviceInfo(); return deviceInfo.brightness || 0; } // ============================================================================ // Color Control (for color-capable bulbs) // ============================================================================ /** * Set color using HSV values */ async setColor(color) { const capabilities = this.getCapabilities(); if (!capabilities.color) { throw new Error(`${this.getDeviceModel()} does not support color control`); } bulb_1.ColorUtils.validateHSV(color); const request = { method: 'set_device_info', params: { hue: color.hue, saturation: color.saturation, brightness: color.value } }; await this.sendRequest(request); } /** * Set color using RGB values */ async setColorRGB(color) { const hsv = bulb_1.ColorUtils.rgbToHsv(color); await this.setColor(hsv); } /** * Set color using named color */ async setNamedColor(color) { const hsv = bulb_1.ColorUtils.getNamedColor(color); await this.setColor(hsv); } /** * Get current color in HSV format */ async getColor() { const capabilities = this.getCapabilities(); if (!capabilities.color) { return null; } const deviceInfo = await this.getDeviceInfo(); if (deviceInfo.hue !== undefined && deviceInfo.saturation !== undefined) { return { hue: deviceInfo.hue, saturation: deviceInfo.saturation, value: deviceInfo.brightness }; } return null; } // ============================================================================ // Color Temperature Control // ============================================================================ /** * Set color temperature */ async setColorTemperature(temperature, brightness) { const capabilities = this.getCapabilities(); if (!capabilities.colorTemperature) { throw new Error(`${this.getDeviceModel()} does not support color temperature control`); } bulb_1.ColorUtils.validateColorTemperature(temperature); const params = { color_temp: temperature }; if (brightness !== undefined) { if (brightness < capabilities.minBrightness || brightness > capabilities.maxBrightness) { throw new Error(`Brightness must be between ${capabilities.minBrightness}-${capabilities.maxBrightness}`); } params.brightness = brightness; } const request = { method: 'set_device_info', params }; await this.sendRequest(request); } /** * Get current color temperature */ async getColorTemperature() { const capabilities = this.getCapabilities(); if (!capabilities.colorTemperature) { return null; } const deviceInfo = await this.getDeviceInfo(); return deviceInfo.color_temp || null; } // ============================================================================ // Light Effects (for L530) // ============================================================================ /** * Set light effect */ async setLightEffect(config) { const capabilities = this.getCapabilities(); if (!capabilities.effects) { throw new Error(`${this.getDeviceModel()} does not support light effects`); } const params = { lighting_effect: { name: config.effect, enable: config.effect !== 'off' } }; if (config.speed !== undefined) { params.lighting_effect.speed = Math.min(Math.max(config.speed, 1), 10); } if (config.brightness !== undefined) { params.lighting_effect.brightness = Math.min(Math.max(config.brightness, 1), 100); } if (config.colors && config.colors.length > 0) { params.lighting_effect.colors = config.colors.map(color => { bulb_1.ColorUtils.validateHSV(color); return { hue: color.hue, saturation: color.saturation, brightness: color.value }; }); } const request = { method: 'set_lighting_effect', params }; await this.sendRequest(request); } /** * Turn off light effects */ async turnOffEffect() { await this.setLightEffect({ effect: 'off' }); } // ============================================================================ // Utility Methods // ============================================================================ /** * Check if device supports a specific feature */ supportsFeature(feature) { const capabilities = this.getCapabilities(); return Boolean(capabilities[feature]); } /** * Get device capabilities */ async hasColorSupport() { return this.supportsFeature('color'); } async hasColorTemperatureSupport() { return this.supportsFeature('colorTemperature'); } async hasEffectsSupport() { return this.supportsFeature('effects'); } } exports.BaseBulb = BaseBulb; //# sourceMappingURL=base-bulb.js.map