UNPKG

homebridge-lg-thinq-ceiling-fan

Version:

A Homebridge plugin for controlling LG ceiling fans via the LG ThinQ platform with working speed control

211 lines (210 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LGCeilingFanAccessory = void 0; class LGCeilingFanAccessory { constructor(platform, accessory, config, lgApi) { this.platform = platform; this.accessory = accessory; this.config = config; this.lgApi = lgApi; this.fanStatus = { isOn: false, fanSpeed: 0, maxSpeed: 4, }; this.lastSpeedCommand = null; this.lastPowerCommand = null; this.commandThrottleMs = 1000; this.accessory.getService(this.platform.Service.AccessoryInformation) .setCharacteristic(this.platform.Characteristic.Manufacturer, 'LG') .setCharacteristic(this.platform.Characteristic.Model, config.model || 'Ceiling Fan') .setCharacteristic(this.platform.Characteristic.SerialNumber, config.id); this.service = this.accessory.getService(this.platform.Service.Fan) || this.accessory.addService(this.platform.Service.Fan); this.service.setCharacteristic(this.platform.Characteristic.Name, config.name || 'Ceiling Fan'); this.service.getCharacteristic(this.platform.Characteristic.On) .onSet(this.setOn.bind(this)) .onGet(this.getOn.bind(this)); this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed) .setProps({ minValue: 0, maxValue: 100, minStep: 25, }) .onSet(this.setRotationSpeed.bind(this)) .onGet(this.getRotationSpeed.bind(this)); const existingLightService = this.accessory.getService(this.platform.Service.Lightbulb); if (existingLightService) { this.accessory.removeService(existingLightService); } this.platform.log.info(`LG Ceiling Fan accessory initialized: ${config.name} (4 speed steps: 2,4,6,7, no light, no reverse)`); } async setOn(value) { const isOn = value; const now = Date.now(); if (this.lastPowerCommand && this.lastPowerCommand.isOn === isOn && (now - this.lastPowerCommand.timestamp) < this.commandThrottleMs) { console.log(`[Fan Accessory DEBUG] Throttling duplicate power command: ${isOn ? 'ON' : 'OFF'} (within ${this.commandThrottleMs}ms)`); return; } this.lastPowerCommand = { isOn, timestamp: now }; try { console.log(`[Fan Accessory DEBUG] Setting power to: ${isOn ? 'ON' : 'OFF'}`); const isAuthValid = await this.lgApi.validateAuthentication(); if (!isAuthValid) { console.log('[Fan Accessory DEBUG] Authentication validation failed, will attempt refresh during API call'); } await this.platform.executeApiWithAutoRefresh(() => this.lgApi.sendCommand(this.config.id, { dataKey: 'airState.operation', dataValue: isOn ? 1 : 0, })); this.fanStatus.isOn = isOn; console.log(`[Fan Accessory DEBUG] Successfully set power: ${isOn ? 'ON' : 'OFF'}`); this.platform.log.info(`Set fan power: ${isOn ? 'ON' : 'OFF'}`); } catch (error) { console.error('[Fan Accessory ERROR] Failed to set power:', error.message); this.platform.log.error(`Failed to set fan power: ${error}`); throw new this.platform.api.hap.HapStatusError(-70402); } } async getOn() { await this.updateStatus(); return this.fanStatus.isOn; } async setRotationSpeed(value) { const percentage = value; const now = Date.now(); console.log(`[Fan Accessory DEBUG] === START setRotationSpeed(${percentage}%) ===`); if (this.lastSpeedCommand && this.lastSpeedCommand.percentage === percentage && (now - this.lastSpeedCommand.timestamp) < this.commandThrottleMs) { console.log(`[Fan Accessory DEBUG] Throttling duplicate speed command: ${percentage}% (within ${this.commandThrottleMs}ms)`); console.log('[Fan Accessory DEBUG] === END setRotationSpeed (throttled) ==='); return; } this.lastSpeedCommand = { percentage, timestamp: now }; let lgSpeed; let actualPercentage; if (percentage === 0) { lgSpeed = 0; actualPercentage = 0; } else if (percentage <= 25) { lgSpeed = 2; actualPercentage = 25; } else if (percentage <= 50) { lgSpeed = 4; actualPercentage = 50; } else if (percentage <= 75) { lgSpeed = 6; actualPercentage = 75; } else { lgSpeed = 7; actualPercentage = 100; } try { console.log(`[Fan Accessory DEBUG] Setting speed to ${actualPercentage}% (LG Level: ${lgSpeed})`); const isAuthValid = await this.lgApi.validateAuthentication(); if (!isAuthValid) { console.log('[Fan Accessory DEBUG] Authentication validation failed, will attempt refresh during API call'); } this.platform.log.info(`Setting fan speed to ${actualPercentage}% (LG Level: ${lgSpeed})`); const command = { dataKey: 'airState.windStrength', dataValue: lgSpeed, }; console.log('[Fan Accessory DEBUG] About to call platform.executeApiWithAutoRefresh'); await this.platform.executeApiWithAutoRefresh(() => this.lgApi.sendCommand(this.config.id, command)); console.log('[Fan Accessory DEBUG] platform.executeApiWithAutoRefresh completed'); this.fanStatus.fanSpeed = actualPercentage; if (lgSpeed > 0 && !this.fanStatus.isOn) { console.log('[Fan Accessory DEBUG] Speed > 0, turning on fan...'); this.platform.log.info('Fan speed > 0, turning on fan...'); await this.setOn(true); } console.log(`[Fan Accessory DEBUG] Successfully set speed: ${actualPercentage}% (LG Level: ${lgSpeed})`); console.log('[Fan Accessory DEBUG] === END setRotationSpeed (success) ==='); this.platform.log.info(`Successfully set fan speed: ${actualPercentage}% (LG Level: ${lgSpeed})`); } catch (error) { console.error('[Fan Accessory ERROR] Failed to set speed:', error.message); console.log('[Fan Accessory DEBUG] === END setRotationSpeed (error) ==='); this.platform.log.error(`Failed to set fan speed: ${error}`); throw new this.platform.api.hap.HapStatusError(-70402); } } async getRotationSpeed() { await this.updateStatus(); return this.fanStatus.fanSpeed; } async updateStatus() { try { console.log(`[Fan Accessory DEBUG] Starting status update for device: ${this.config.id}`); const isAuthValid = await this.lgApi.validateAuthentication(); if (!isAuthValid) { console.log('[Fan Accessory DEBUG] Authentication validation failed, will attempt refresh during API call'); } const status = await this.platform.executeApiWithAutoRefresh(() => this.lgApi.getDeviceStatus(this.config.id)); const snapshot = status?.snapshot || {}; const airState = snapshot['airState.operation'] || snapshot.operation; const windStrength = snapshot['airState.windStrength'] || snapshot.windStrength; console.log('[Fan Accessory DEBUG] Raw device status:', JSON.stringify({ airState, windStrength, fullSnapshot: snapshot, }, null, 2)); this.fanStatus.isOn = airState === 1 || airState === '1'; if (typeof windStrength === 'number' || typeof windStrength === 'string') { const lgSpeed = parseInt(windStrength.toString()); switch (lgSpeed) { case 0: this.fanStatus.fanSpeed = 0; break; case 2: this.fanStatus.fanSpeed = 25; break; case 4: this.fanStatus.fanSpeed = 50; break; case 6: this.fanStatus.fanSpeed = 75; break; case 7: this.fanStatus.fanSpeed = 100; break; default: this.fanStatus.fanSpeed = 0; this.platform.log.warn(`Unknown LG speed value: ${lgSpeed}, defaulting to off`); } } console.log(`[Fan Accessory DEBUG] Parsed status: Power=${this.fanStatus.isOn}, Speed=${this.fanStatus.fanSpeed}%`); if (this.platform.config.debug) { this.platform.log.debug(`Status updated: Power=${this.fanStatus.isOn}, Speed=${this.fanStatus.fanSpeed}%`); } } catch (error) { console.error('[Fan Accessory ERROR] Status update failed:', error.message); this.platform.log.error(`Failed to update device status: ${error}`); } } startStatusUpdates() { const interval = (this.platform.config.polling_interval || 30) * 1000; setInterval(async () => { try { await this.updateStatus(); this.service.updateCharacteristic(this.platform.Characteristic.On, this.fanStatus.isOn); this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.fanStatus.fanSpeed); } catch (error) { this.platform.log.error(`Status update failed: ${error}`); } }, interval); this.platform.log.info(`Started status updates every ${interval / 1000} seconds`); } } exports.LGCeilingFanAccessory = LGCeilingFanAccessory;