UNPKG

homebridge-create-ceiling-fan-enhanced

Version:

Enhanced CREATE Ceiling Fan plugin with beep control for Homebridge

363 lines (362 loc) 18.6 kB
"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;