homebridge-create-ceiling-fan-enhanced
Version:
Enhanced CREATE Ceiling Fan plugin with beep control for Homebridge
363 lines (362 loc) • 18.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CeilingFanAccessory = void 0;
const tuyapi_1 = __importDefault(require("tuyapi"));
class CeilingFanAccessory {
constructor(platform, accessory) {
this.platform = platform;
this.accessory = accessory;
this.isConnecting = false;
this.isConnectingLater = false;
this.state = {
fanOn: false,
fanRotation: this.platform.Characteristic.RotationDirection.CLOCKWISE,
fanSpeed: 20,
lightOn: false,
lightColorTemperature: 140,
beepOn: true,
};
// Log device configuration for debugging
this.platform.log.info(`${accessory.context.device.name}: Initializing device...`);
this.platform.log.debug(`${accessory.context.device.name}: Device ID: ${accessory.context.device.id}`);
this.platform.log.debug(`${accessory.context.device.name}: Device IP: ${accessory.context.device.ip || 'Not provided'}`);
this.platform.log.debug(`${accessory.context.device.name}: Device Version: ${accessory.context.device.version || 'Not provided'}`);
const deviceConfig = {
id: accessory.context.device.id,
key: accessory.context.device.key,
version: accessory.context.device.version || '3.3',
issueGetOnConnect: false,
nullPayloadOnJSONError: false,
issueRefreshOnConnect: true,
issueRefreshOnPing: false,
};
// Add IP if available for direct connection
if (accessory.context.device.ip) {
deviceConfig.ip = accessory.context.device.ip;
}
const device = new tuyapi_1.default(deviceConfig);
device.on('connected', () => {
this.platform.log.info(`${accessory.context.device.name}: Device connected!`);
this.isConnecting = false;
});
device.on('disconnected', () => {
this.platform.log.info(`${accessory.context.device.name}: Disconnected... Will retry connection`);
this.scheduleReconnect(device);
});
device.on('error', error => {
this.platform.log.warn(`${accessory.context.device.name}: Device error:`, error.message);
this.isConnecting = false;
this.scheduleReconnect(device);
});
device.on('data', (data) => {
this.handleDeviceData(data);
});
// Information
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'CREATE')
.setCharacteristic(this.platform.Characteristic.Model, 'Ceiling Fan')
.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name)
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.context.device.id);
// Fan
this.fanService = this.accessory.getService(this.platform.Service.Fan) || this.accessory.addService(this.platform.Service.Fan);
this.fanService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name);
// Fan state
this.fanService.getCharacteristic(this.platform.Characteristic.On)
.onSet(async (value) => {
this.state.fanOn = value.valueOf();
await device.set({ dps: 60, set: this.state.fanOn, shouldWaitForResponse: false });
})
.onGet(() => this.state.fanOn);
// const stateHook = (data: DPSObject) => {
// const isOn = data.dps['60'] as boolean | undefined;
// if (isOn !== undefined) {
// this.state.fanOn = isOn;
// this.platform.log.info('Update fan on', this.state.fanOn);
// this.fanService.updateCharacteristic(this.platform.Characteristic.On, this.state.fanOn);
// }
// };
// device.on('dp-refresh', stateHook);
// device.on('data', stateHook);
// Fan rotation
this.fanService.getCharacteristic(this.platform.Characteristic.RotationDirection)
.onSet(async (value) => {
this.state.fanRotation = value.valueOf();
await device.set({ dps: 63, set: this.state.fanRotation === 0 ? 'forward' : 'reverse', shouldWaitForResponse: false });
})
.onGet(() => this.state.fanRotation);
// const rotationHook = (data: DPSObject) => {
// const rotation = data.dps['63'] as string | undefined;
// if (rotation !== undefined) {
// this.state.fanRotation = rotation === 'forward'
// ? this.platform.Characteristic.RotationDirection.CLOCKWISE
// : this.platform.Characteristic.RotationDirection.COUNTER_CLOCKWISE;
// this.platform.log.info('Update fan rotation', this.state.fanRotation);
// this.fanService.updateCharacteristic(this.platform.Characteristic.RotationDirection, this.state.fanRotation);
// }
// };
// device.on('dp-refresh', rotationHook);
// device.on('data', rotationHook);
// Fan speed
this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.onSet(async (value) => {
if (value.valueOf() === 0) {
await device.set({ dps: 60, set: false, shouldWaitForResponse: false });
}
else {
this.state.fanSpeed = value.valueOf();
await device.set({ dps: 62, set: this.toStep(this.state.fanSpeed), shouldWaitForResponse: false });
}
})
.onGet(() => this.state.fanSpeed);
// .setProps({});
// const speedHook = (data: DPSObject) => {
// const speed = data.dps['62'] as number | undefined;
// if (speed !== undefined) {
// this.state.fanSpeed = this.toPercent(this.state.fanSpeed, speed);
// this.platform.log.info('Update fan speed', this.state.fanSpeed);
// this.fanService.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.state.fanSpeed);
// }
// };
// device.on('dp-refresh', speedHook);
// device.on('data', speedHook);
// Enable light controls if device has hasLight set to true or if not specified (auto-detect)
const hasLightControls = accessory.context.device.hasLight !== false; // Default to true unless explicitly set to false
if (hasLightControls) {
// Fan Light
this.lightService = this.accessory.getService(this.platform.Service.Lightbulb)
|| this.accessory.addService(this.platform.Service.Lightbulb);
this.lightService.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name);
this.lightService.getCharacteristic(this.platform.Characteristic.On)
.onSet(async (value) => {
this.state.lightOn = value.valueOf();
await device.set({ dps: 20, set: this.state.lightOn, shouldWaitForResponse: false });
})
.onGet(() => this.state.lightOn);
// const lightStateHook = (data: DPSObject) => {
// const isOn = data.dps['20'] as boolean | undefined;
// if (isOn !== undefined) {
// this.state.lightOn = isOn;
// this.platform.log.info('Update light on', this.state.lightOn);
// this.lightService.updateCharacteristic(this.platform.Characteristic.On, this.state.lightOn);
// }
// };
// device.on('dp-refresh', lightStateHook);
// device.on('data', lightStateHook);
// Note: This fan light is not dimmable, so no brightness control is needed
// const lightBrightnessHook = (data: DPSObject) => {
// const brightness = data.dps['22'] as number | undefined;
// if (brightness !== undefined) {
// this.state.lightBrightness = brightness / 10;
// this.platform.log.info('Update brightness', this.state.lightBrightness);
// this.lightService.updateCharacteristic(this.platform.Characteristic.Brightness, this.state.lightBrightness);
// }
// };
// device.on('dp-refresh', lightBrightnessHook);
// device.on('data', lightBrightnessHook);
// Fan Light ColorTemperature
this.lightService.getCharacteristic(this.platform.Characteristic.ColorTemperature)
.onSet(async (value) => {
this.state.lightColorTemperature = value.valueOf();
const tuyaValue = this.convertTemperatureTuya(this.state.lightColorTemperature);
this.platform.log.debug(`${accessory.context.device.name}: Setting color temperature to ${this.state.lightColorTemperature} (Tuya: ${tuyaValue})`);
await device.set({ dps: 23, set: tuyaValue, shouldWaitForResponse: false });
})
.onGet(() => this.state.lightColorTemperature)
.setProps({
minValue: 140, // Coolest (blue-ish)
maxValue: 500, // Warmest (yellow-ish)
});
}
// Beep Control
this.beepService = this.accessory.getService('Beep Control')
|| this.accessory.addService(this.platform.Service.Switch, 'Beep Control', 'beep-control');
this.beepService.setCharacteristic(this.platform.Characteristic.Name, `${accessory.context.device.name} Beep`);
this.beepService.getCharacteristic(this.platform.Characteristic.On)
.onSet(async (value) => {
this.state.beepOn = value.valueOf();
await device.set({ dps: 66, set: this.state.beepOn, shouldWaitForResponse: false });
})
.onGet(() => this.state.beepOn);
this.connect(device);
}
async connect(device) {
if (device.isConnected()) {
this.platform.log.info(`${this.accessory.context.device.name}: Device already connected!`);
return;
}
if (this.isConnecting) {
this.platform.log.debug(`${this.accessory.context.device.name}: Connection already in progress...`);
return;
}
this.isConnecting = true;
this.platform.log.info(`${this.accessory.context.device.name}: Attempting to connect...`);
try {
await this.connectWithFallback(device);
this.platform.log.info(`${this.accessory.context.device.name}: Successfully connected!`);
this.isConnecting = false;
}
catch (error) {
this.platform.log.error(`${this.accessory.context.device.name}: All connection attempts failed:`, error.message);
this.platform.log.error(`${this.accessory.context.device.name}: Please check device power, network connection, and configuration`);
this.isConnecting = false;
this.scheduleReconnect(device);
}
}
handleDeviceData(data) {
this.platform.log.debug(`${this.accessory.context.device.name}: Received data:`, JSON.stringify(data));
if (data.dps) {
// Handle fan switch (DP 60)
if (data.dps['60'] !== undefined) {
const fanActive = data.dps['60'];
if (this.state.fanOn !== fanActive) {
this.state.fanOn = fanActive;
this.fanService.updateCharacteristic(this.platform.Characteristic.On, this.state.fanOn);
this.platform.log.debug(`${this.accessory.context.device.name}: Fan state updated to: ${fanActive ? 'ON' : 'OFF'}`);
}
}
// Handle fan speed (DP 62)
if (data.dps['62'] !== undefined) {
const speed = data.dps['62'];
const newFanSpeed = this.toPercent(this.state.fanSpeed, speed);
if (this.state.fanSpeed !== newFanSpeed) {
this.state.fanSpeed = newFanSpeed;
this.fanService.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.state.fanSpeed);
this.platform.log.debug(`${this.accessory.context.device.name}: Fan speed updated to: ${newFanSpeed}%`);
}
}
// Handle fan direction (DP 63)
if (data.dps['63'] !== undefined) {
const direction = data.dps['63'];
const newRotation = direction === 'forward'
? this.platform.Characteristic.RotationDirection.CLOCKWISE
: this.platform.Characteristic.RotationDirection.COUNTER_CLOCKWISE;
if (this.state.fanRotation !== newRotation) {
this.state.fanRotation = newRotation;
this.fanService.updateCharacteristic(this.platform.Characteristic.RotationDirection, this.state.fanRotation);
this.platform.log.debug(`${this.accessory.context.device.name}: Fan direction updated to: ${direction}`);
}
}
// Handle beep switch (DP 66)
if (data.dps['66'] !== undefined) {
const beepOn = data.dps['66'];
if (this.state.beepOn !== beepOn) {
this.state.beepOn = beepOn;
this.beepService.updateCharacteristic(this.platform.Characteristic.On, this.state.beepOn);
this.platform.log.debug(`${this.accessory.context.device.name}: Beep state updated to: ${beepOn ? 'ON' : 'OFF'}`);
}
}
// Handle light controls if device has light
const hasLightControls = this.accessory.context.device.hasLight !== false;
if (hasLightControls) {
// Handle light switch (DP 20)
if (data.dps['20'] !== undefined) {
const lightOn = data.dps['20'];
if (this.state.lightOn !== lightOn) {
this.state.lightOn = lightOn;
this.lightService.updateCharacteristic(this.platform.Characteristic.On, this.state.lightOn);
this.platform.log.debug(`${this.accessory.context.device.name}: Light state updated to: ${lightOn ? 'ON' : 'OFF'}`);
}
}
// Note: Brightness control removed - this fan light is not dimmable
// Handle color temperature (DP 23)
if (data.dps['23'] !== undefined) {
const colorTemp = data.dps['23'];
const newColorTemp = this.convertTemperatureHomeKit(colorTemp);
if (this.state.lightColorTemperature !== newColorTemp) {
this.state.lightColorTemperature = newColorTemp;
this.lightService.updateCharacteristic(this.platform.Characteristic.ColorTemperature, this.state.lightColorTemperature);
this.platform.log.debug(`${this.accessory.context.device.name}: Color temperature updated to: ${newColorTemp} (Tuya: ${colorTemp})`);
}
}
}
}
}
async connectWithFallback(device) {
try {
// First attempt: direct connection if IP is available
if (this.accessory.context.device.ip) {
this.platform.log.info(`${this.accessory.context.device.name}: Attempting direct connection to ${this.accessory.context.device.ip}`);
await device.connect();
return;
}
}
catch (error) {
this.platform.log.warn(`${this.accessory.context.device.name}: Direct connection failed: ${error.message}`);
}
try {
// Second attempt: device discovery
this.platform.log.info(`${this.accessory.context.device.name}: Attempting device discovery...`);
await device.find({ timeout: 15000 });
await device.connect();
return;
}
catch (error) {
this.platform.log.warn(`${this.accessory.context.device.name}: Discovery connection failed: ${error.message}`);
}
// If both methods fail, throw the last error
throw new Error('All connection methods failed');
}
scheduleReconnect(device) {
if (this.isConnectingLater) {
this.platform.log.debug(`${this.accessory.context.device.name}: Reconnection already scheduled`);
return;
}
this.isConnectingLater = true;
this.platform.log.info(`${this.accessory.context.device.name}: Scheduling reconnection in 60 seconds...`);
setTimeout(() => {
this.platform.log.info(`${this.accessory.context.device.name}: Attempting reconnection...`);
this.isConnectingLater = false;
this.connect(device);
}, 60000); // 60 seconds delay - longer interval to avoid spam
}
toStep(percent) {
const etapes = [1, 2, 3, 4, 5, 6];
const etapeIndex = Math.floor(percent / 16.67); // 100 / 6 = 16.67
return etapes[etapeIndex];
}
toPercent(initialPercentage, step) {
const plagesPourcentage = [0, 15, 30, 50, 65, 80, 100];
const plageMin = plagesPourcentage[step - 1];
const plageMax = plagesPourcentage[step];
if (initialPercentage >= plageMin && initialPercentage <= plageMax) {
return initialPercentage;
}
if (step === 1) {
return 10;
}
if (step === 6) {
return 100;
}
return plageMin;
}
convertTemperatureHomeKit(tuyaValue) {
switch (tuyaValue) {
case 0:
return 140;
case 500:
return 320;
case 1000:
return 500;
default:
return 140;
}
}
convertTemperatureTuya(homekitValue) {
if (homekitValue >= 140 && homekitValue < 230) {
return 0;
}
else if (homekitValue >= 230 && homekitValue < 430) {
return 500;
}
else if (homekitValue >= 430 && homekitValue <= 500) {
return 1000;
}
else {
return 0;
}
}
}
exports.CeilingFanAccessory = CeilingFanAccessory;