UNPKG

@felixgeelhaar/govee-api-client

Version:

Enterprise-grade TypeScript client library for the Govee Developer REST API

301 lines 11.8 kB
import { CommandFactory } from '../domain/entities/Command'; import { SlidingWindowRateLimiter } from '../infrastructure'; import { RetryableRepository, RetryExecutor, RetryPolicy, RetryConfigPresets, } from '../infrastructure/retry'; export class GoveeControlService { constructor(config) { this.validateConfig(config); // Set up retry functionality if enabled this.enableRetries = config.enableRetries ?? false; if (this.enableRetries) { this.repository = this.createRetryableRepository(config); } else { this.repository = config.repository; } this.logger = config.logger; // Default to 95 requests per minute (5 request buffer under Govee API limit) const requestsPerMinute = config.rateLimit ?? 95; // Initialize the sliding window rate limiter if (config.rateLimiterConfig) { // Use custom configuration this.rateLimiter = new SlidingWindowRateLimiter({ maxRequests: requestsPerMinute, windowMs: 60 * 1000, // 1 minute logger: this.logger, ...config.rateLimiterConfig, }); } else if (requestsPerMinute === 95) { // Use optimized factory for Govee API this.rateLimiter = SlidingWindowRateLimiter.forGoveeApi(this.logger); } else { // Use custom rate limit this.rateLimiter = SlidingWindowRateLimiter.custom(requestsPerMinute, this.logger); } this.logger?.info({ requestsPerMinute, rateLimiterType: 'SlidingWindow', enableRetries: this.enableRetries, retryPolicy: this.enableRetries ? config.retryPolicy || 'production' : 'disabled', stats: this.rateLimiter.getStats(), }, 'Initialized GoveeControlService with sliding window rate limiting'); } /** * Creates a retry-enabled repository wrapper */ createRetryableRepository(config) { let retryPolicy; // Determine retry policy based on configuration if (config.retryPolicy instanceof RetryPolicy) { retryPolicy = config.retryPolicy; } else { const policyType = config.retryPolicy || 'production'; switch (policyType) { case 'development': retryPolicy = new RetryPolicy(RetryConfigPresets.development(this.logger)); break; case 'testing': retryPolicy = new RetryPolicy(RetryConfigPresets.testing(this.logger)); break; case 'production': retryPolicy = new RetryPolicy(RetryConfigPresets.production(this.logger)); break; case 'custom': // Use Govee-optimized defaults for custom retryPolicy = RetryPolicy.createGoveeOptimized(this.logger); break; default: retryPolicy = new RetryPolicy(RetryConfigPresets.production(this.logger)); } } const retryExecutor = new RetryExecutor(retryPolicy, { logger: this.logger, enableRequestLogging: true, enablePerformanceTracking: true, }); const retryableRepository = new RetryableRepository({ repository: config.repository, retryExecutor, logger: this.logger, enableRequestIds: true, }); this.logger?.info({ retryPolicy: retryPolicy.getMetrics().circuitBreakerState ? 'enabled_with_circuit_breaker' : 'enabled', maxAttempts: retryPolicy instanceof RetryPolicy ? 'configured' : 'default', }, 'Created retry-enabled repository wrapper'); return retryableRepository; } validateConfig(config) { if (!config.repository) { throw new Error('Repository is required'); } if (config.rateLimit !== undefined && (!Number.isInteger(config.rateLimit) || config.rateLimit <= 0)) { throw new Error('Rate limit must be a positive integer'); } } /** * Retrieves all devices associated with the configured API key */ async getDevices() { this.logger?.info('Getting all devices'); return this.rateLimiter.execute(async () => { const devices = await this.repository.findAll(); this.logger?.info(`Retrieved ${devices.length} devices`); return devices; }); } /** * Retrieves the current state of a specific device */ async getDeviceState(deviceId, model) { this.validateDeviceParams(deviceId, model); this.logger?.info({ deviceId, model }, 'Getting device state'); return this.rateLimiter.execute(async () => { const state = await this.repository.findState(deviceId, model); this.logger?.info({ deviceId, model, online: state.online }, 'Retrieved device state'); return state; }); } /** * Sends a command to control a specific device */ async sendCommand(deviceId, model, command) { this.validateDeviceParams(deviceId, model); this.logger?.info({ deviceId, model, command: command.toObject() }, 'Sending command'); return this.rateLimiter.execute(async () => { await this.repository.sendCommand(deviceId, model, command); this.logger?.info({ deviceId, model }, 'Command sent successfully'); }); } /** * Turns a device on */ async turnOn(deviceId, model) { this.logger?.info({ deviceId, model }, 'Turning device on'); await this.sendCommand(deviceId, model, CommandFactory.powerOn()); } /** * Turns a device off */ async turnOff(deviceId, model) { this.logger?.info({ deviceId, model }, 'Turning device off'); await this.sendCommand(deviceId, model, CommandFactory.powerOff()); } /** * Sets the brightness of a device */ async setBrightness(deviceId, model, brightness) { this.logger?.info({ deviceId, model, brightness: brightness.level }, 'Setting device brightness'); await this.sendCommand(deviceId, model, CommandFactory.brightness(brightness)); } /** * Sets the color of a device */ async setColor(deviceId, model, color) { this.logger?.info({ deviceId, model, color: color.toObject() }, 'Setting device color'); await this.sendCommand(deviceId, model, CommandFactory.color(color)); } /** * Sets the color temperature of a device */ async setColorTemperature(deviceId, model, colorTemperature) { this.logger?.info({ deviceId, model, colorTemperature: colorTemperature.kelvin }, 'Setting device color temperature'); await this.sendCommand(deviceId, model, CommandFactory.colorTemperature(colorTemperature)); } /** * Convenience method to turn on a device and set its brightness */ async turnOnWithBrightness(deviceId, model, brightness) { this.logger?.info({ deviceId, model, brightness: brightness.level }, 'Turning device on with brightness'); await this.turnOn(deviceId, model); await this.setBrightness(deviceId, model, brightness); } /** * Convenience method to turn on a device and set its color */ async turnOnWithColor(deviceId, model, color, brightness) { this.logger?.info({ deviceId, model, color: color.toObject(), brightness: brightness?.level }, 'Turning device on with color'); await this.turnOn(deviceId, model); await this.setColor(deviceId, model, color); if (brightness) { await this.setBrightness(deviceId, model, brightness); } } /** * Convenience method to turn on a device and set its color temperature */ async turnOnWithColorTemperature(deviceId, model, colorTemperature, brightness) { this.logger?.info({ deviceId, model, colorTemperature: colorTemperature.kelvin, brightness: brightness?.level }, 'Turning device on with color temperature'); await this.turnOn(deviceId, model); await this.setColorTemperature(deviceId, model, colorTemperature); if (brightness) { await this.setBrightness(deviceId, model, brightness); } } /** * Convenience method to check if a device is online */ async isDeviceOnline(deviceId, model) { const state = await this.getDeviceState(deviceId, model); return state.isOnline(); } /** * Convenience method to check if a device is powered on */ async isDevicePoweredOn(deviceId, model) { const state = await this.getDeviceState(deviceId, model); return state.isPoweredOn(); } /** * Convenience method to get all controllable devices */ async getControllableDevices() { const devices = await this.getDevices(); return devices.filter(device => device.canControl()); } /** * Convenience method to get all retrievable devices */ async getRetrievableDevices() { const devices = await this.getDevices(); return devices.filter(device => device.canRetrieve()); } /** * Convenience method to find a device by name (case-insensitive) */ async findDeviceByName(deviceName) { const devices = await this.getDevices(); return devices.find(device => device.deviceName.toLowerCase().includes(deviceName.toLowerCase())); } /** * Convenience method to find devices by model */ async findDevicesByModel(model) { const devices = await this.getDevices(); return devices.filter(device => device.model === model); } /** * Gets current rate limiter statistics for monitoring and debugging */ getRateLimiterStats() { return this.rateLimiter.getStats(); } /** * Gets retry metrics if retry functionality is enabled */ getRetryMetrics() { if (!this.enableRetries) { return null; } if (this.repository instanceof RetryableRepository) { return this.repository.getRetryMetrics(); } return null; } /** * Resets retry metrics if retry functionality is enabled */ resetRetryMetrics() { if (!this.enableRetries) { return; } if (this.repository instanceof RetryableRepository) { this.repository.resetRetryMetrics(); this.logger?.info('Retry metrics reset'); } } /** * Gets comprehensive service statistics including rate limiter and retry metrics */ getServiceStats() { const stats = { rateLimiter: this.getRateLimiterStats(), retries: this.getRetryMetrics(), configuration: { enableRetries: this.enableRetries, rateLimit: this.rateLimiter.getStats().maxRequests, }, }; return stats; } /** * Checks if retry functionality is enabled */ isRetryEnabled() { return this.enableRetries; } validateDeviceParams(deviceId, model) { if (!deviceId || typeof deviceId !== 'string' || deviceId.trim().length === 0) { throw new Error('Device ID must be a non-empty string'); } if (!model || typeof model !== 'string' || model.trim().length === 0) { throw new Error('Model must be a non-empty string'); } } } //# sourceMappingURL=GoveeControlService.js.map