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
JavaScript
"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;