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

167 lines 5.5 kB
"use strict"; /** * Retry utilities for Tapo device operations * Separated from wrapper for better separation of concerns */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TapoRetryHandler = void 0; exports.withRetry = withRetry; exports.retryable = retryable; class TapoRetryHandler { constructor(config = TapoRetryHandler.DEFAULT_CONFIG) { this.config = config; } async execute(operation, operationName = 'TapoOperation') { const startTime = Date.now(); const retryReasons = []; let lastError; for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) { try { const data = await operation(); return { success: true, data, metadata: { attempts: attempt, duration: Date.now() - startTime, retryReasons } }; } catch (error) { lastError = error; if (attempt < this.config.maxAttempts && this.shouldRetry(lastError)) { const delay = this.calculateDelay(attempt); const reason = this.getRetryReason(lastError); retryReasons.push(`Attempt ${attempt}: ${reason}`); if (this.config.onRetry) { this.config.onRetry(attempt, lastError, delay); } else { console.log(`${operationName} retry ${attempt}/${this.config.maxAttempts}: ${reason} (waiting ${delay}ms)`); } await new Promise(resolve => setTimeout(resolve, delay)); } else { break; } } } return { success: false, error: lastError, metadata: { attempts: this.config.maxAttempts, duration: Date.now() - startTime, retryReasons } }; } shouldRetry(error) { const errorMessage = error.message.toLowerCase(); // Check if it's a retryable error const allRetryablePatterns = [ ...this.config.busyErrorPatterns, ...this.config.sessionErrorPatterns ]; return allRetryablePatterns.some(pattern => errorMessage.includes(pattern.toLowerCase())); } getRetryReason(error) { const errorMessage = error.message.toLowerCase(); if (this.config.busyErrorPatterns.some(p => errorMessage.includes(p.toLowerCase()))) { return 'Device busy'; } if (this.config.sessionErrorPatterns.some(p => errorMessage.includes(p.toLowerCase()))) { return 'Session error'; } return 'Unknown retryable error'; } calculateDelay(attempt) { switch (this.config.strategy) { case 'exponential': return this.config.baseDelay * Math.pow(2, attempt - 1); case 'linear': return this.config.baseDelay * attempt; case 'fixed': default: return this.config.baseDelay; } } /** * Create a pre-configured retry handler for common scenarios */ static forDeviceControl() { const defaultConfig = TapoRetryHandler.DEFAULT_CONFIG; return new TapoRetryHandler({ ...defaultConfig, maxAttempts: 3, baseDelay: 3000, strategy: 'linear' }); } static forEnergyMonitoring() { const defaultConfig = TapoRetryHandler.DEFAULT_CONFIG; return new TapoRetryHandler({ ...defaultConfig, maxAttempts: 2, baseDelay: 1000, strategy: 'fixed' }); } static forInfoRetrieval() { const defaultConfig = TapoRetryHandler.DEFAULT_CONFIG; return new TapoRetryHandler({ ...defaultConfig, maxAttempts: 2, baseDelay: 500, strategy: 'fixed' }); } } exports.TapoRetryHandler = TapoRetryHandler; TapoRetryHandler.DEFAULT_CONFIG = { maxAttempts: 3, baseDelay: 2000, strategy: 'exponential', busyErrorPatterns: [ 'klap -1012', 'device busy', 'command timing issue', 'persistently busy' ], sessionErrorPatterns: [ 'klap 1002', 'session expired', 'invalid terminal uuid', 'session needs to be re-established' ] }; /** * Utility function to wrap any async operation with retry logic */ async function withRetry(operation, config) { const defaultConfig = TapoRetryHandler.DEFAULT_CONFIG; const handler = new TapoRetryHandler({ ...defaultConfig, ...config }); return handler.execute(operation); } /** * Decorator for automatic retry (for class methods) */ function retryable(config) { return function (_target, _propertyName, descriptor) { const method = descriptor.value; descriptor.value = async function (...args) { const result = await withRetry(() => method.apply(this, args), config); if (result.success) { return result.data; } else { throw result.error; } }; }; } //# sourceMappingURL=retry-utils.js.map