UNPKG

homebridge-tsvesync

Version:

Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets

1,291 lines (1,100 loc) 48.8 kB
import { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; import { BaseAccessory } from './base.accessory'; import { TSVESyncPlatform } from '../platform'; import { DeviceCapabilities, VeSyncHumidifier } from '../types/device.types'; // Extended interface to include optional methods for humidifiers interface ExtendedVeSyncHumidifier extends VeSyncHumidifier { // Common methods setMode?(mode: 'manual' | 'auto' | 'sleep'): Promise<boolean>; getDetails?(): Promise<boolean>; // Night light properties and methods nightLightBrightness?: number; nightLightStatus?: string; setNightLightBrightness?(brightness: number): Promise<boolean>; setNightLight?(enabled: boolean, brightness?: number): Promise<boolean>; // Common methods across humidifier types setMistLevel?(level: number): Promise<boolean>; setHumidity?(humidity: number): Promise<boolean>; setAutoMode?(): Promise<boolean>; setManualMode?(): Promise<boolean>; setSleepMode?(): Promise<boolean>; setDisplay?(on: boolean): Promise<boolean>; turnOnDisplay?(): Promise<boolean>; turnOffDisplay?(): Promise<boolean>; setIndicatorLightSwitch?(on: boolean): Promise<boolean>; // Automatic stop methods automaticStopOn?(): Promise<boolean>; automaticStopOff?(): Promise<boolean>; setAutomaticStop?(enabled: boolean): Promise<boolean>; // Superior6000S specific methods setDryingModeEnabled?(enabled: boolean): Promise<boolean>; // Common properties mistLevel?: number; // Note: humidity must be non-optional to match VeSyncHumidifier interface humidity: number; currentHumidity?: number; // Actual humidity reading in the room mode?: string; screenStatus?: 'on' | 'off'; enabled?: boolean; // Used by Humid200S for power control powerSwitch?: boolean; // Used by Humid1000S and Superior6000S for power control // Superior6000S specific properties temperature?: number; filterLifePercentage?: number; dryingModeEnabled?: boolean; dryingModeState?: string | null; dryingModeLevel?: string | null; dryingModeSecondsRemaining?: number; // For device-specific details details?: { target_humidity?: number; auto_target_humidity?: number; night_light_brightness?: number; current_humidity?: number; water_lacks?: boolean; water_tank_lifted?: boolean; humidity_high?: boolean; automatic_stop?: boolean; automatic_stop_configured?: boolean; temperature?: number; filter_life?: number; }; // Device configuration object configuration?: { auto_target_humidity?: number; automatic_stop?: boolean; display?: string; // Other configuration properties }; // Feature support check hasFeature?(feature: string): boolean; } export class HumidifierAccessory extends BaseAccessory { protected readonly device: VeSyncHumidifier; private capabilities: DeviceCapabilities; // Removed readonly to allow re-initialization // Device type flags private isHumidDual200S: boolean; private isHumid200S: boolean; private isHumid200300S: boolean; private isHumid1000S: boolean; private isSuperior6000S: boolean; // Services private lightService?: Service; private temperatureService?: Service; private filterService?: Service; // Tracks the last mode we commanded, to protect against stale API responses. // Expires after 30s so out-of-band changes (VeSync app) are not masked. private _lastCommandedMode: string | null = null; private _lastCommandedModeTime: number = 0; constructor( platform: TSVESyncPlatform, accessory: PlatformAccessory, device: VeSyncHumidifier ) { super(platform, accessory, device); this.device = device; // Initialize capabilities with default values to prevent undefined this.capabilities = { hasBrightness: false, hasColorTemp: false, hasColor: false, hasSpeed: true, hasHumidity: true, hasAirQuality: false, hasWaterLevel: true, hasChildLock: true, hasSwingMode: false, }; // Detect device type — order matters: specific types first this.isHumidDual200S = this.detectHumidDual200S(); this.isHumid200S = this.detectHumid200S(); this.isHumid200300S = this.detectHumid200300S(); this.isHumid1000S = this.detectHumid1000S(); this.isSuperior6000S = this.detectSuperior6000S(); // Log detected device type if (this.isHumidDual200S) { this.platform.log.debug(`Detected Dual200S device: ${this.device.deviceName}`); } else if (this.isHumid200S) { this.platform.log.debug(`Detected Humid200S device: ${this.device.deviceName}`); } else if (this.isHumid200300S) { this.platform.log.debug(`Detected Humid200300S device: ${this.device.deviceName}`); } else if (this.isHumid1000S) { this.platform.log.debug(`Detected Humid1000S device: ${this.device.deviceName}`); } else if (this.isSuperior6000S) { this.platform.log.debug(`Detected Superior6000S device: ${this.device.deviceName}`); } else { this.platform.log.debug(`Unknown humidifier type: ${this.device.deviceName}, using default implementation`); } } /** * Detect if the device is a HumidDual200S (or regional variant LUH-D301S) */ private detectHumidDual200S(): boolean { const deviceType = this.device.deviceType.toUpperCase(); return deviceType.includes('DUAL200S') || deviceType.includes('LUH-D301S'); } /** * Detect if the device is a Humid200S */ private detectHumid200S(): boolean { const deviceType = this.device.deviceType.toUpperCase(); // Classic200S is a Humid200S device if (deviceType.includes('CLASSIC200S')) { return true; } // Check for mist level range 1-3 which is specific to Humid200S, // but exclude Dual200S which also has low mist levels if (this.isHumidDual200S) { return false; } const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (extendedDevice.mistLevel !== undefined && extendedDevice.mistLevel <= 3) { return true; } return false; } /** * Detect if the device is a Humid200300S */ private detectHumid200300S(): boolean { // Check if the device has the nightLightBrightness property const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (typeof extendedDevice.nightLightBrightness !== 'undefined' || (extendedDevice.details && typeof extendedDevice.details.night_light_brightness !== 'undefined')) { return true; } // Check device type for Classic300S or Dual200S const deviceType = this.device.deviceType.toUpperCase(); if (deviceType.includes('CLASSIC300S') || this.isHumidDual200S) { return true; } // Check for LUH-A601S, LUH-A602S, LUH-O451S, LUH-O601S series if (deviceType.includes('LUH-A601S') || deviceType.includes('LUH-A602S') || deviceType.includes('LUH-O451S') || deviceType.includes('LUH-O601S')) { return true; } return false; } /** * Detect if the device is a Humid1000S */ private detectHumid1000S(): boolean { const deviceType = this.device.deviceType.toUpperCase(); // Check for LUH-M101S series if (deviceType.includes('LUH-M101S-WUS') || deviceType.includes('LUH-M101S-WEUR')) { return true; } // Check for OasisMist1000S if (deviceType.includes('OASISMIST1000S')) { return true; } // Check for powerSwitch field and night light control const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (extendedDevice.powerSwitch !== undefined && typeof extendedDevice.setNightLight === 'function') { return true; } return false; } /** * Detect if the device is a Superior6000S */ private detectSuperior6000S(): boolean { const deviceType = this.device.deviceType.toUpperCase(); // Check for LEH-S601S series if (deviceType.includes('LEH-S601S-WUS') || deviceType.includes('LEH-S601S-WUSR')) { return true; } // Check for Superior6000S if (deviceType.includes('SUPERIOR6000S')) { return true; } // Check for temperature or drying mode properties const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (extendedDevice.temperature !== undefined || extendedDevice.dryingModeEnabled !== undefined || extendedDevice.filterLifePercentage !== undefined) { return true; } return false; } /** * Convert a VeSync mode string to a HomeKit TargetHumidifierDehumidifierState value. * * For Dual200S the mapping is swapped so that VeSync "auto" (which needs a * humidity target) lands on HomeKit state 1 (HUMIDIFIER / "Humidity" in the * Home app) where Apple shows the humidity slider, and VeSync "manual" * (fixed mist, no target) lands on state 0 ("Auto", no slider). */ private modeToTargetState(mode: string | undefined): number { // Prefer last commanded mode to protect against stale API responses. // Clear the override once the device reports the same mode (caught up). let effectiveMode = mode; if (this._lastCommandedMode !== null) { const elapsed = Date.now() - this._lastCommandedModeTime; if (mode === this._lastCommandedMode || elapsed > 30000) { this._lastCommandedMode = null; } else { this.platform.log.debug(`Using commanded mode '${this._lastCommandedMode}' instead of device-reported '${mode}'`); effectiveMode = this._lastCommandedMode; } } if (this.isHumidDual200S) { return effectiveMode === 'auto' ? 1 : 0; } return effectiveMode === 'auto' ? 0 : 1; } /** * Convert a HomeKit TargetHumidifierDehumidifierState value to a VeSync mode. * Inverse of modeToTargetState. */ private targetStateToMode(value: CharacteristicValue): string { if (this.isHumidDual200S) { return value === 1 ? 'auto' : 'manual'; } return value === 0 ? 'auto' : 'manual'; } protected setupService(): void { // No need to check for capabilities initialization since we set defaults in constructor // Get or create the humidifier service this.service = this.accessory.getService(this.platform.Service.HumidifierDehumidifier) || this.accessory.addService(this.platform.Service.HumidifierDehumidifier); // Initialize characteristics this.service.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, 40); this.service.updateCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold, 60); this.service.updateCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState, 1); this.service.updateCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState, 1); // Register humidity handlers IMMEDIATELY after updateCharacteristic — no separation. // The updateCharacteristic creates the instance; onGet/onSet must be on that same instance. this.service .getCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold) .setProps({ minValue: 30, maxValue: 80, minStep: 1 }) .onGet(this.getTargetHumidity.bind(this)) .onSet(async (value: CharacteristicValue) => { this.platform.log.info(`Setting target humidity to ${value}% for ${this.device.deviceName}`); await this.setTargetHumidity(value); }); // Set up required characteristics this.setupCharacteristic( this.platform.Characteristic.Active, this.getActive.bind(this), this.setActive.bind(this) ); // Set up target state characteristic for mode mapping // Determine which modes this device supports const extendedDevice = this.device as ExtendedVeSyncHumidifier; const supportsAutoMode = typeof extendedDevice.setAutoMode === 'function' || (typeof extendedDevice.setMode === 'function' && extendedDevice.mode === 'auto'); // Get the characteristic and set valid values based on device capabilities const targetStateChar = this.service.getCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState); // Only allow HUMIDIFIER (1) mode if auto is not supported // If auto is supported, allow both HUMIDIFIER_OR_DEHUMIDIFIER (0) and HUMIDIFIER (1) // Never allow DEHUMIDIFIER (2) as it's not supported by any VeSync devices targetStateChar.setProps({ validValues: supportsAutoMode ? [0, 1] : [1] }); this.setupCharacteristic( this.platform.Characteristic.TargetHumidifierDehumidifierState, async () => this.modeToTargetState(extendedDevice.mode), this.setTargetState.bind(this) ); // Set up rotation speed characteristic this.setupCharacteristic( this.platform.Characteristic.RotationSpeed, this.getRotationSpeed.bind(this), this.handleSetRotationSpeed.bind(this) ); // Constrain rotation speed to 2 steps for Dual200S (Low=50%, High=100%) if (this.isHumidDual200S) { const rotationSpeedChar = this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed); rotationSpeedChar.setProps({ minValue: 0, maxValue: 100, minStep: 50, }); } // Add Name characteristic this.setupCharacteristic( this.platform.Characteristic.Name, async () => this.device.deviceName ); // CurrentRelativeHumidity getter if (this.capabilities && this.capabilities.hasHumidity) { this.service .getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) .onGet(async () => { const extDev = this.device as ExtendedVeSyncHumidifier; return extDev.currentHumidity ?? extDev.details?.current_humidity ?? 0; }); } // Add Water Level characteristic if supported (mapping inferred from water_lacks/water_tank_lifted) if (this.capabilities && this.capabilities.hasWaterLevel) { if (!this.service.testCharacteristic(this.platform.Characteristic.WaterLevel)) { this.service.addCharacteristic(this.platform.Characteristic.WaterLevel); } } // Add Lock Physical Controls characteristic if supported if (this.capabilities && this.capabilities.hasChildLock) { this.setupCharacteristic( this.platform.Characteristic.LockPhysicalControls, async () => false, async (value: CharacteristicValue) => { throw new Error('Locking physical controls is not supported by the tsvesync API.'); } ); } // Add night light service for devices that support it if (this.isHumid200300S || this.isHumid1000S) { this.setupNightLightService(); } // Add temperature sensor service for Superior6000S if (this.isSuperior6000S) { this.setupTemperatureService(); this.setupFilterService(); } } /** * Set up night light service for devices that support it */ private setupNightLightService(): void { this.platform.log.debug(`Setting up night light service for device: ${this.device.deviceName}`); // Get or create the lightbulb service this.lightService = this.accessory.getService('Night Light') || this.accessory.addService(this.platform.Service.Lightbulb, 'Night Light', 'night-light'); // Set up on/off characteristic this.setupCharacteristic( this.platform.Characteristic.On, this.getNightLightOn.bind(this), this.setNightLightOn.bind(this), undefined, this.lightService ); // Set up brightness characteristic this.setupCharacteristic( this.platform.Characteristic.Brightness, this.getNightLightBrightness.bind(this), this.setNightLightBrightness.bind(this), undefined, this.lightService ); } /** * Set up temperature sensor service for Superior6000S */ private setupTemperatureService(): void { this.platform.log.debug(`Setting up temperature sensor service for device: ${this.device.deviceName}`); // Get or create the temperature sensor service this.temperatureService = this.accessory.getService(this.platform.Service.TemperatureSensor) || this.accessory.addService(this.platform.Service.TemperatureSensor); // Set up current temperature characteristic this.setupCharacteristic( this.platform.Characteristic.CurrentTemperature, this.getTemperature.bind(this), undefined, { minValue: -50, maxValue: 100, minStep: 0.1 }, this.temperatureService ); // Set name for the temperature sensor this.setupCharacteristic( this.platform.Characteristic.Name, async () => `${this.device.deviceName} Temperature`, undefined, undefined, this.temperatureService ); } /** * Get temperature for Superior6000S */ private async getTemperature(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; // Get temperature from device const temperature = extendedDevice.temperature || (extendedDevice.details && extendedDevice.details.temperature) || 20; // Default to 20°C if not available return temperature; } /** * Set up filter maintenance service for Superior6000S */ private setupFilterService(): void { this.platform.log.debug(`Setting up filter maintenance service for device: ${this.device.deviceName}`); // Get or create the filter maintenance service this.filterService = this.accessory.getService(this.platform.Service.FilterMaintenance) || this.accessory.addService(this.platform.Service.FilterMaintenance); // Set up filter change indication characteristic this.setupCharacteristic( this.platform.Characteristic.FilterChangeIndication, this.getFilterChangeIndication.bind(this), undefined, undefined, this.filterService ); // Set up filter life level characteristic this.setupCharacteristic( this.platform.Characteristic.FilterLifeLevel, this.getFilterLifeLevel.bind(this), undefined, undefined, this.filterService ); // Set name for the filter service this.setupCharacteristic( this.platform.Characteristic.Name, async () => `${this.device.deviceName} Filter`, undefined, undefined, this.filterService ); } /** * Get filter change indication for Superior6000S */ private async getFilterChangeIndication(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; // Get filter life percentage const filterLife = extendedDevice.filterLifePercentage || (extendedDevice.details && extendedDevice.details.filter_life) || 100; // Default to 100% if not available // Return 1 (CHANGE_FILTER) if filter life is below 10%, otherwise 0 (FILTER_OK) return filterLife < 10 ? 1 : 0; } /** * Get filter life level for Superior6000S */ private async getFilterLifeLevel(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; // Get filter life percentage const filterLife = extendedDevice.filterLifePercentage || (extendedDevice.details && extendedDevice.details.filter_life) || 100; // Default to 100% if not available return filterLife; } /** * Get night light on state */ private async getNightLightOn(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; const brightness = extendedDevice.nightLightBrightness || (extendedDevice.details && extendedDevice.details.night_light_brightness) || 0; return brightness > 0; } /** * Set night light on state */ private async setNightLightOn(value: CharacteristicValue): Promise<void> { try { const isOn = value as boolean; const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (typeof extendedDevice.setNightLightBrightness === 'function') { const brightness = isOn ? 100 : 0; const success = await extendedDevice.setNightLightBrightness(brightness); if (!success) { throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} night light`); } } else { throw new Error('Device API does not support night light control'); } } catch (error) { this.handleDeviceError('set night light state', error); } } /** * Get night light brightness */ private async getNightLightBrightness(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; return extendedDevice.nightLightBrightness || (extendedDevice.details && extendedDevice.details.night_light_brightness) || 0; } /** * Set night light brightness */ private async setNightLightBrightness(value: CharacteristicValue): Promise<void> { try { const brightness = value as number; const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (typeof extendedDevice.setNightLightBrightness === 'function') { const success = await extendedDevice.setNightLightBrightness(brightness); if (!success) { throw new Error(`Failed to set night light brightness to ${brightness}`); } } else { throw new Error('Device API does not support night light control'); } } catch (error) { this.handleDeviceError('set night light brightness', error); } } /** * Update device states based on the latest details */ protected async updateDeviceSpecificStates(details: any): Promise<void> { // Log the relevant details for debugging without using JSON.stringify on the entire object this.platform.log.debug(`Updating device states for ${this.device.deviceName}`); // Cast to extended device type const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (details) { // Log only specific properties to avoid circular references this.platform.log.debug( `Device status: ${details.deviceStatus}, ` + `Speed: ${details.speed}, ` + `Mode: ${details.mode}, ` + `Humidity: ${details.humidity}, ` + `MistLevel: ${extendedDevice.mistLevel}` ); } // First, refresh the device details to ensure we have the latest state try { if (typeof extendedDevice.getDetails === 'function') { await extendedDevice.getDetails(); // Log only specific properties to avoid circular references this.platform.log.debug( `Refreshed device status: ${this.device.deviceStatus}, ` + `Speed: ${this.device.speed}, ` + `Mode: ${extendedDevice.mode}, ` + `Humidity: ${extendedDevice.humidity}, ` + `MistLevel: ${extendedDevice.mistLevel}` ); } } catch (error) { this.platform.log.warn(`Failed to refresh device details: ${error}`); } this.applyDeviceStatesToHomeKit(details); } private applyDeviceStatesToHomeKit(details: any): void { const extendedDevice = this.device as ExtendedVeSyncHumidifier; const isActive = this.device.deviceStatus === 'on'; this.platform.log.debug(`Device ${this.device.deviceName} active state: ${isActive}, Status: ${this.device.deviceStatus}`); this.service.updateCharacteristic( this.platform.Characteristic.Active, isActive ? 1 : 0 ); let currentState = 0; if (isActive) { if (extendedDevice.mode === 'manual') { currentState = 2; } else if (extendedDevice.mode === 'auto' || extendedDevice.mode === 'sleep') { const currentHumidity = extendedDevice.currentHumidity !== undefined ? extendedDevice.currentHumidity : extendedDevice.details?.current_humidity || 0; const targetHumidity = extendedDevice.humidity || extendedDevice.configuration?.auto_target_humidity || extendedDevice.details?.target_humidity || 45; if (currentHumidity < targetHumidity) { currentState = 2; this.platform.log.debug(`Auto mode: Current humidity ${currentHumidity}% is below target ${targetHumidity}%, setting state to HUMIDIFYING`); } else { currentState = 1; this.platform.log.debug(`Auto mode: Current humidity ${currentHumidity}% has reached target ${targetHumidity}%, setting state to IDLE`); } } } this.service.updateCharacteristic( this.platform.Characteristic.CurrentHumidifierDehumidifierState, currentState ); this.service.updateCharacteristic( this.platform.Characteristic.TargetHumidifierDehumidifierState, this.modeToTargetState(extendedDevice.mode) ); let rotationSpeed = 0; if (isActive) { if (this.isHumidDual200S && extendedDevice.mistLevel !== undefined) { // Dual200S has 2 levels: 1 (Low) = 50%, 2 (High) = 100% rotationSpeed = extendedDevice.mistLevel === 1 ? 50 : 100; } else if (this.isHumid200300S && extendedDevice.mistLevel !== undefined) { const mistLevel = extendedDevice.mistLevel; this.platform.log.debug(`Device ${this.device.deviceName} mist level: ${mistLevel}`); switch (mistLevel) { case 1: rotationSpeed = 11; break; case 2: rotationSpeed = 22; break; case 3: rotationSpeed = 33; break; case 4: rotationSpeed = 44; break; case 5: rotationSpeed = 55; break; case 6: rotationSpeed = 66; break; case 7: rotationSpeed = 77; break; case 8: rotationSpeed = 88; break; case 9: rotationSpeed = 100; break; } } else if (this.device.speed !== undefined) { this.platform.log.debug(`Device ${this.device.deviceName} speed: ${this.device.speed}`); switch (this.device.speed) { case 1: rotationSpeed = 11; break; case 2: rotationSpeed = 22; break; case 3: rotationSpeed = 33; break; case 4: rotationSpeed = 44; break; case 5: rotationSpeed = 55; break; case 6: rotationSpeed = 66; break; case 7: rotationSpeed = 77; break; case 8: rotationSpeed = 88; break; case 9: rotationSpeed = 100; break; } } } this.platform.log.debug(`Setting rotation speed to ${rotationSpeed}% for device: ${this.device.deviceName}`); this.service.updateCharacteristic( this.platform.Characteristic.RotationSpeed, rotationSpeed ); if (this.capabilities && this.capabilities.hasHumidity) { const currentHumidity = extendedDevice.currentHumidity !== undefined ? extendedDevice.currentHumidity : extendedDevice.details?.current_humidity || 0; if (currentHumidity > 0) { this.service.updateCharacteristic( this.platform.Characteristic.CurrentRelativeHumidity, currentHumidity ); } const targetHumidity = extendedDevice.humidity || extendedDevice.configuration?.auto_target_humidity || extendedDevice.details?.target_humidity || 45; if (this.service.testCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold)) { this.service.updateCharacteristic( this.platform.Characteristic.RelativeHumidityHumidifierThreshold, targetHumidity ); } } if (this.capabilities && this.capabilities.hasWaterLevel) { const waterLacks = details?.water_lacks || (extendedDevice.details && extendedDevice.details.water_lacks); const waterTankLifted = details?.water_tank_lifted || (extendedDevice.details && extendedDevice.details.water_tank_lifted); const waterLow = waterLacks || waterTankLifted; if (this.service.getCharacteristic(this.platform.Characteristic.WaterLevel)) { this.service.updateCharacteristic( this.platform.Characteristic.WaterLevel, waterLow ? 0 : 100 ); this.platform.log.debug(`Water level for ${this.device.deviceName}: ${waterLow ? 'Low (0%)' : 'OK (100%)'}`); } } if ((this.isHumid200300S || this.isHumid1000S) && this.lightService) { const brightness = extendedDevice.nightLightBrightness || (extendedDevice.details && extendedDevice.details.night_light_brightness) || 0; this.lightService.updateCharacteristic( this.platform.Characteristic.On, brightness > 0 ); this.lightService.updateCharacteristic( this.platform.Characteristic.Brightness, brightness ); } if (this.isSuperior6000S && this.temperatureService) { const temperature = extendedDevice.temperature || (extendedDevice.details && extendedDevice.details.temperature) || 20; this.temperatureService.updateCharacteristic( this.platform.Characteristic.CurrentTemperature, temperature ); } if (this.isSuperior6000S && this.filterService) { const filterLife = extendedDevice.filterLifePercentage || (extendedDevice.details && extendedDevice.details.filter_life) || 100; this.filterService.updateCharacteristic( this.platform.Characteristic.FilterLifeLevel, filterLife ); this.filterService.updateCharacteristic( this.platform.Characteristic.FilterChangeIndication, filterLife < 10 ? 1 : 0 ); } } protected getDeviceCapabilities(): DeviceCapabilities { return { hasBrightness: false, hasColorTemp: false, hasColor: false, hasSpeed: true, hasHumidity: true, hasAirQuality: false, hasWaterLevel: true, hasChildLock: true, hasSwingMode: false, }; } private async getActive(): Promise<CharacteristicValue> { // Log the current device status for debugging this.platform.log.debug(`Getting active state for device: ${this.device.deviceName}, current status: ${this.device.deviceStatus}`); // Refresh device status before returning try { const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (typeof extendedDevice.getDetails === 'function') { await extendedDevice.getDetails(); this.platform.log.debug(`Refreshed device status: ${this.device.deviceStatus}, Mode: ${extendedDevice.mode}`); } } catch (error) { this.platform.log.warn(`Failed to refresh device status: ${error}`); } // Check if the device is on based on the deviceStatus property // According to the API documentation, deviceStatus should be 'on' or 'off' // In the test script, they use device.deviceStatus to determine if the device is on or off const isActive = this.device.deviceStatus === 'on'; // Log the active state for debugging with more details this.platform.log.debug(`Device ${this.device.deviceName} is ${isActive ? 'active' : 'inactive'}, Status: ${this.device.deviceStatus}`); // Return the active state without updating characteristics here // This will let HomeKit handle the state update return isActive ? 1 : 0; } private async setActive(value: CharacteristicValue): Promise<void> { try { const isOn = value as number === 1; this.platform.log.debug(`Setting device ${this.device.deviceName} to ${isOn ? 'on' : 'off'}`); // Get the extended device with all potential methods const extendedDevice = this.device as ExtendedVeSyncHumidifier; // Refresh device details to ensure we have the latest state before checking try { if (typeof extendedDevice.getDetails === 'function') { await extendedDevice.getDetails(); this.platform.log.debug(`Current device status before setting: ${this.device.deviceStatus}`); } } catch (error) { this.platform.log.warn(`Failed to refresh device status: ${error}`); } // Check current state after refreshing const currentlyOn = this.device.deviceStatus === 'on'; this.platform.log.debug(`Current device state: ${currentlyOn ? 'on' : 'off'}, requested state: ${isOn ? 'on' : 'off'}`); // If the device is already in the desired state, just update HomeKit if (currentlyOn === isOn) { this.platform.log.debug(`Device ${this.device.deviceName} is already ${isOn ? 'on' : 'off'}, skipping API call`); // Update HomeKit characteristics to match the current device state this.service.updateCharacteristic( this.platform.Characteristic.Active, isOn ? 1 : 0 ); // Update current state based on the active state and mode const currentState = isOn ? (extendedDevice.mode === 'manual' ? 2 : 1) : // 2 = HUMIDIFYING, 1 = IDLE 0; // 0 = INACTIVE this.service.updateCharacteristic( this.platform.Characteristic.CurrentHumidifierDehumidifierState, currentState ); // Update all device states to ensure HomeKit is in sync await this.updateDeviceSpecificStates(this.device); return; } let success = false; // Try to turn the device on/off using the appropriate method based on device type if (isOn) { // Turn on the device this.platform.log.debug(`Attempting to turn ON device: ${this.device.deviceName}`); // Use the appropriate method based on device type success = await this.device.turnOn(); // If successful, set the device to manual mode if (success) { // For devices with specific mode setting methods, use them if (typeof extendedDevice.setManualMode === 'function') { this.platform.log.debug(`Setting device to manual mode using setManualMode: ${this.device.deviceName}`); const modeSuccess = await extendedDevice.setManualMode(); this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`); } else if (typeof extendedDevice.setMode === 'function') { // Fall back to generic setMode for other devices this.platform.log.debug(`Setting device to manual mode using setMode: ${this.device.deviceName}`); const modeSuccess = await extendedDevice.setMode('manual'); this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`); } } } else { // Turn off the device this.platform.log.debug(`Attempting to turn OFF device: ${this.device.deviceName}`); success = await this.device.turnOff(); } if (!success) { throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} device`); } this.applyDeviceStatesToHomeKit(this.device); await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off'); } catch (error) { this.handleDeviceError('set active state', error); } } private async getRotationSpeed(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; if (this.isHumidDual200S) { const level = extendedDevice.mistLevel; if (level === undefined || level === null || level === 0) { return 0; } return level === 1 ? 50 : 100; } if (this.device.speed === undefined || this.device.speed === null) { return 0; } // Convert device speed (1-9) to HomeKit percentage (0-100) switch (this.device.speed) { case 0: return 0; case 1: return 11; case 2: return 22; case 3: return 33; case 4: return 44; case 5: return 55; case 6: return 66; case 7: return 77; case 8: return 88; case 9: return 100; default: return 0; } } private async setTargetState(value: CharacteristicValue): Promise<void> { try { // HomeKit Target State: 0 = HUMIDIFIER_OR_DEHUMIDIFIER (Auto), 1 = HUMIDIFIER, 2 = DEHUMIDIFIER this.platform.log.debug(`Setting target state to ${value} for device: ${this.device.deviceName}`); const device = this.device as ExtendedVeSyncHumidifier; // Check if device is off - if so, turn it on first if (this.device.deviceStatus !== 'on') { this.platform.log.debug(`Device is off, turning on before changing mode: ${this.device.deviceName}`); const turnOnSuccess = await this.device.turnOn(); if (!turnOnSuccess) { throw new Error('Failed to turn on device before changing mode'); } } let mode: string; if (value === 2) { this.platform.log.warn(`Dehumidifier mode not supported for device: ${this.device.deviceName}, using manual mode instead`); mode = 'manual'; } else if (value === 0 || value === 1) { mode = this.targetStateToMode(value); } else { this.platform.log.warn(`Unknown target state value: ${value}, using manual mode as default`); mode = 'manual'; } // Check current mode first if (device.mode === mode) { this.platform.log.debug(`Device ${this.device.deviceName} is already in ${mode} mode, skipping API call`); // Update target state to reflect the current mode const targetState = this.modeToTargetState(mode); this.updateCharacteristicValue( this.platform.Characteristic.TargetHumidifierDehumidifierState, targetState ); return; } let success = false; // For Humid200300S devices, use the specific mode setting methods if available if (this.isHumid200300S) { if (mode === 'auto' && typeof device.setAutoMode === 'function') { this.platform.log.debug(`Setting auto mode for Humid200300S device: ${this.device.deviceName}`); success = await device.setAutoMode(); this.platform.log.debug(`Set auto mode result: ${success ? 'success' : 'failed'}`); } else if (mode === 'manual' && typeof device.setManualMode === 'function') { this.platform.log.debug(`Setting manual mode for Humid200300S device: ${this.device.deviceName}`); success = await device.setManualMode(); this.platform.log.debug(`Set manual mode result: ${success ? 'success' : 'failed'}`); } else if (typeof device.setSleepMode === 'function' && mode === 'sleep') { this.platform.log.debug(`Setting sleep mode for Humid200300S device: ${this.device.deviceName}`); success = await device.setSleepMode(); this.platform.log.debug(`Set sleep mode result: ${success ? 'success' : 'failed'}`); } else if (typeof device.setMode === 'function') { // Fall back to generic setMode if specific method not available this.platform.log.debug(`Setting mode to ${mode} for Humid200300S device using generic method: ${this.device.deviceName}`); success = await device.setMode(mode as 'manual' | 'auto' | 'sleep'); this.platform.log.debug(`Set mode result: ${success ? 'success' : 'failed'}`); } else { throw new Error('Device API does not support mode setting operations'); } } else if (typeof device.setMode === 'function') { // For non-Humid200300S devices, use the generic setMode method this.platform.log.debug(`Setting mode to ${mode} for device: ${this.device.deviceName}`); success = await device.setMode(mode as 'manual' | 'auto' | 'sleep'); this.platform.log.debug(`Set mode result: ${success ? 'success' : 'failed'}`); } else { throw new Error('Device API does not support mode setting operations'); } if (!success) { throw new Error(`Failed to set device mode to ${mode}`); } // Track commanded mode so stale getDetails() responses don't revert HomeKit this._lastCommandedMode = mode; this._lastCommandedModeTime = Date.now(); this.applyDeviceStatesToHomeKit(this.device); } catch (error) { this.handleDeviceError('set target state', error); } } private async handleSetRotationSpeed(value: CharacteristicValue): Promise<void> { try { const percentage = value as number; this.platform.log.debug(`Setting rotation speed to ${percentage}% for device: ${this.device.deviceName}`); if (percentage === 0) { // Turn off the device instead of setting speed to 0 this.platform.log.debug(`Turning off device ${this.device.deviceName} due to 0% rotation speed`); const success = await this.device.turnOff(); if (!success) { throw new Error('Failed to turn off device'); } return; } // Convert percentage to mist level based on device type let speed: number; if (this.isHumidDual200S) { // Dual200S: 2 levels — 1-50% = Low (1), 51-100% = High (2) speed = percentage <= 50 ? 1 : 2; } else if (percentage <= 11) { speed = 1; } else if (percentage <= 22) { speed = 2; } else if (percentage <= 33) { speed = 3; } else if (percentage <= 44) { speed = 4; } else if (percentage <= 55) { speed = 5; } else if (percentage <= 66) { speed = 6; } else if (percentage <= 77) { speed = 7; } else if (percentage <= 88) { speed = 8; } else { speed = 9; } // Adjust speed based on device type const extendedDevice = this.device as ExtendedVeSyncHumidifier; let success = false; // Dual200S: adjusting speed while in auto mode implies the user wants manual control if (this.isHumidDual200S && extendedDevice.mode === 'auto') { this.platform.log.debug(`Switching Dual200S to manual mode before setting mist level: ${this.device.deviceName}`); if (typeof extendedDevice.setManualMode === 'function') { await extendedDevice.setManualMode(); } else if (typeof extendedDevice.setMode === 'function') { await extendedDevice.setMode('manual'); } this._lastCommandedMode = 'manual'; this._lastCommandedModeTime = Date.now(); } // For Humid200S devices, limit mist level to 1-3 if (this.isHumid200S) { // Limit speed to 1-3 for Humid200S devices speed = Math.min(speed, 3); this.platform.log.debug(`Limiting mist level to ${speed} for Humid200S device: ${this.device.deviceName}`); } // Use the appropriate method based on device type if ((this.isHumid200S || this.isHumid200300S || this.isHumid1000S || this.isSuperior6000S) && typeof extendedDevice.setMistLevel === 'function') { // Use setMistLevel for devices that support it this.platform.log.debug(`Setting mist level to ${speed} for device: ${this.device.deviceName}`); success = await extendedDevice.setMistLevel(speed); } else { // Fall back to changeFanSpeed for other humidifier types this.platform.log.debug(`Setting fan speed to ${speed} for device: ${this.device.deviceName}`); success = await this.device.changeFanSpeed(speed); } if (!success) { throw new Error(`Failed to set speed to ${speed}`); } // Refresh device details to get the latest state if (typeof extendedDevice.getDetails === 'function') { await extendedDevice.getDetails(); // Log the appropriate property based on device type if (this.isHumid200300S && extendedDevice.mistLevel !== undefined) { this.platform.log.debug(`Device ${this.device.deviceName} mist level after setting: ${extendedDevice.mistLevel}`); } else { this.platform.log.debug(`Device ${this.device.deviceName} speed after setting: ${this.device.speed}`); } } // Update device state and characteristics await this.updateDeviceSpecificStates(this.device); } catch (error) { this.handleDeviceError('set rotation speed', error); } } /** * Helper method to update a characteristic value for a specific service */ /** * Get target humidity level */ private async getTargetHumidity(): Promise<CharacteristicValue> { const extendedDevice = this.device as ExtendedVeSyncHumidifier; // According to API docs, humidifier.humidity returns auto_target_humidity from configuration if (typeof extendedDevice.humidity !== 'undefined') { return extendedDevice.humidity; } // Fall back to configuration for auto_target_humidity if (extendedDevice.configuration && typeof extendedDevice.configuration.auto_target_humidity !== 'undefined') { return extendedDevice.configuration.auto_target_humidity; } // Try to get target humidity from device details if (extendedDevice.details && typeof extendedDevice.details.target_humidity !== 'undefined') { return extendedDevice.details.target_humidity; } // Default to 45% if no target humidity is available return 45; } /** * Set target humidity level */ private async setTargetHumidity(value: CharacteristicValue): Promise<void> { try { const targetHumidity = value as number; this.platform.log.debug(`Setting target humidity to ${targetHumidity}% for device: ${this.device.deviceName}`); const extendedDevice = this.device as ExtendedVeSyncHumidifier; // Check if device is off - if so, turn it on first if (this.device.deviceStatus !== 'on') { this.platform.log.debug(`Device is off, turning on before setting humidity: ${this.device.deviceName}`); const turnOnSuccess = await this.device.turnOn(); if (!turnOnSuccess) { throw new Error('Failed to turn on device before setting humidity'); } // Sync Active state immediately so HomeKit tile reflects power-on this.service.updateCharacteristic(this.platform.Characteristic.Active, 1); } let success = false; // Try to set humidity using the appropriate method if (typeof extendedDevice.setHumidity === 'function') { // Use setHumidity for devices that support it this.platform.log.debug(`Setting humidity to ${targetHumidity}% using setHumidity: ${this.device.deviceName}`); success = await extendedDevice.setHumidity(targetHumidity); } else if (typeof extendedDevice.setTargetHumidity === 'function') { // Use setTargetHumidity for devices that support it this.platform.log.debug(`Setting humidity to ${targetHumidity}% using setTargetHumidity: ${this.device.deviceName}`); success = await extendedDevice.setTargetHumidity(targetHumidity); } else { throw new Error('Device API does not support setting target humidity'); } if (!success) { throw new Error(`Failed to set target humidity to ${targetHumidity}%`); } // Push the commanded value to HomeKit and update in-memory state. // Do NOT call getDetails/updateDeviceSpecificStates here — the API // returns stale target humidity, causing the slider to snap back. // The next polling cycle will eventually sync the actual value. if (extendedDevice.details) { extendedDevice.details.target_humidity = targetHumidity; // Humid200300S's humidity getter reads auto_target_humidity extendedDevice.details.auto_target_humidity = targetHumidity; } this.service.updateCharacteristic( this.platform.Characteristic.RelativeHumidityHumidifierThreshold, targetHumidity ); await this.persistDeviceState('targetHumidity', targetHumidity); } catch (error) { this.handleDeviceError('set target humidity', error); } } private updateServiceCharacteristicValue( service: Service, characteristic: any, value: CharacteristicValue ): void { service.updateCharacteristic(characteristic, value); this.platform.log.debug( `[${this.device.deviceName}] Updated ${characteristic.name} to ${value}` ); } }