UNPKG

homebridge-tsvesync

Version:

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

940 lines 49.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AirPurifierAccessory = void 0; const base_accessory_1 = require("./base.accessory"); class AirPurifierAccessory extends base_accessory_1.BaseAccessory { constructor(platform, accessory, device) { super(platform, accessory, device); this.lastSetSpeed = 0; // Track the last speed we set this.lastSetPercentage = 0; // Track the last percentage we set this.skipNextUpdate = false; // Flag to skip the next update this.device = device; this.capabilities = this.getDeviceCapabilities(); // Detect device class based on model number patterns const extendedDevice = this.device; const deviceType = extendedDevice.deviceType || ''; // AirBypass Devices: Core series, LAP-C series, Vital series this.isAirBypassDevice = deviceType.includes('CORE') || deviceType.startsWith('LAP-C') || deviceType.includes('VITAL'); // AirBaseV2 Devices: LAP-V series, LAP-EL series, EverestAir series this.isAirBaseV2Device = deviceType.startsWith('LAP-V') || deviceType.startsWith('LAP-EL') || deviceType.includes('EVERESTAIR'); // Air131 Devices: LV-PUR131S, LV-RH131S this.isAir131Device = deviceType.startsWith('LV-'); // Log device class detection if (this.isAirBypassDevice) { this.platform.log.debug(`Detected AirBypass device: ${this.device.deviceName} (${deviceType})`); } else if (this.isAirBaseV2Device) { this.platform.log.debug(`Detected AirBaseV2 device: ${this.device.deviceName} (${deviceType})`); } else if (this.isAir131Device) { this.platform.log.debug(`Detected Air131 device: ${this.device.deviceName} (${deviceType})`); } else { // Device type not specifically recognized, but will use default handling } } /** * Feature detection system */ hasFeature(feature) { var _a, _b, _c, _d; const extendedDevice = this.device; // Log device type for debugging this.platform.log.debug(`${this.device.deviceName}: Checking hasFeature('${feature}') for device type: ${this.device.deviceType}`); // Use device's native hasFeature method if available if (typeof extendedDevice.hasFeature === 'function') { const result = extendedDevice.hasFeature(feature); this.platform.log.debug(`${this.device.deviceName}: Native hasFeature('${feature}') returned: ${result}`); // For filter_life, be extra defensive - if native method says false but we think it should be true, override if (!result && feature === 'filter_life') { const deviceType = this.device.deviceType; // Check if this looks like a device type that should support filter_life if (deviceType.includes('Core') || deviceType.includes('LAP-') || deviceType.includes('LV-') || deviceType.includes('Vital')) { this.platform.log.warn(`${this.device.deviceName}: Device type ${deviceType} should support filter_life but native hasFeature returned false. Overriding to true.`); return true; } } return result; } // Fallback feature detection based on device type switch (feature) { case 'air_quality': // Enable air quality features for devices that support it // Check if device has air quality data available const airQualityDevice = this.device; const hasAirQuality = !!(((_a = airQualityDevice.details) === null || _a === void 0 ? void 0 : _a.air_quality_value) !== undefined || ((_b = airQualityDevice.details) === null || _b === void 0 ? void 0 : _b.pm25) !== undefined || ((_c = airQualityDevice.details) === null || _c === void 0 ? void 0 : _c.air_quality) !== undefined); this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') returned: ${hasAirQuality}`); return hasAirQuality; case 'child_lock': // Explicitly disable child lock features this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') returned: false (explicitly disabled)`); return false; case 'display': // Explicitly disable display control features this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') returned: false (explicitly disabled)`); return false; case 'filter_life': // For air purifiers, enable filter life features based on device type patterns const deviceType = this.device.deviceType; // Check device type patterns that should support filter_life const supportsFilterByType = deviceType.includes('Core') || deviceType.includes('LAP-') || deviceType.includes('LV-') || deviceType.includes('Vital'); if (supportsFilterByType) { this.platform.log.debug(`${this.device.deviceName}: Device type ${deviceType} supports filter_life by pattern matching`); return true; } // Fallback: Check if filter_life data exists in any format const filterDevice = this.device; const filterLife = (_d = filterDevice.details) === null || _d === void 0 ? void 0 : _d.filter_life; const hasFilterLife = !!(filterLife !== undefined && (typeof filterLife === 'number' || (typeof filterLife === 'object' && filterLife !== null && 'percent' in filterLife))); // Also check for filterLife property from VeSyncFan base class const hasFilterLifeProperty = typeof extendedDevice.filterLife === 'number'; const result = hasFilterLife || hasFilterLifeProperty; this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') - details.filter_life: ${JSON.stringify(filterLife)}, device.filterLife: ${extendedDevice.filterLife}, result: ${result}`); return result; case 'fan_speed': this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') returned: true`); return true; default: this.platform.log.debug(`${this.device.deviceName}: Fallback hasFeature('${feature}') returned: false (unknown feature)`); return false; } } /** * Check if a feature is supported in the current device mode */ isFeatureSupportedInCurrentMode(feature) { const extendedDevice = this.device; // Use device's native method if available if (typeof extendedDevice.isFeatureSupportedInCurrentMode === 'function') { return extendedDevice.isFeatureSupportedInCurrentMode(feature); } // Fallback logic const mode = extendedDevice.mode || 'manual'; // Air131 only supports fan speed in manual mode if (feature === 'fan_speed' && this.isAir131Device && mode !== 'manual') { return false; } return true; } /** * Get the maximum fan speed for the device */ getMaxFanSpeed() { const extendedDevice = this.device; // Use device's native method if available if (typeof extendedDevice.getMaxFanSpeed === 'function') { const maxSpeed = extendedDevice.getMaxFanSpeed(); if (typeof maxSpeed === 'number' && maxSpeed > 0) { return maxSpeed; } } // Use maxSpeed property if available if (typeof extendedDevice.maxSpeed === 'number' && extendedDevice.maxSpeed > 0) { return extendedDevice.maxSpeed; } // Fallback logic based on device type if (this.isAir131Device) { return 3; // LV-series devices have 3 speed levels } else if (this.isAirBypassDevice) { return 3; // All Core series have 3 speed levels } else if (this.isAirBaseV2Device) { if (this.device.deviceType.includes('LAP-EL551S')) { return 3; // LAP-EL551S has 3 speed levels } else { return 4; // LAP-V series have 4 speed levels } } // Default to 3 for unknown devices return 3; } /** * Calculate the appropriate step size for discrete speed levels */ calculateRotationSpeedStep() { const maxSpeed = this.getMaxFanSpeed(); // For devices with discrete speed levels, set appropriate step size if (maxSpeed === 3) { return 33.33; // Creates exact positions: 0%, 33.33%, 66.67%, 100% } else if (maxSpeed === 4) { return 25; // Creates exact positions: 0%, 25%, 50%, 75%, 100% } // Default to continuous slider for other devices return 1; } /** * Convert device speed to percentage */ speedToPercentage(speed) { const maxSpeed = this.getMaxFanSpeed(); // Ensure we have valid numbers if (maxSpeed <= 0 || typeof speed !== 'number' || speed <= 0) { return 0; } // For devices with 3 speed levels if (maxSpeed === 3) { switch (speed) { case 1: return 33.33; // Low -> 33.33% case 2: return 66.67; // Medium -> 66.67% case 3: return 100; // High -> 100% default: return 0; } } // For devices with 4 speed levels else if (maxSpeed === 4) { switch (speed) { case 1: return 25; // Low -> 25% case 2: return 50; // Medium-Low -> 50% case 3: return 75; // Medium-High -> 75% case 4: return 100; // High -> 100% default: return 0; } } // Default calculation return Math.min(100, Math.max(0, Math.round((speed / maxSpeed) * 100))); } /** * Updates the rotation speed characteristic for Air131 devices immediately after turning on. * This ensures the UI shows the restored speed instead of 0, providing better user experience. * * Air131 devices remember their last speed setting when turned back on, but the characteristic * update can be delayed. This method proactively updates the HomeKit characteristic to reflect * the actual device speed. * * @private * @returns {Promise<void>} Promise that resolves when the update is complete * @throws Will log errors through handleDeviceError but won't propagate them to avoid * disrupting the main turn-on operation */ async updateAir131RotationSpeedAfterTurnOn() { var _a; try { // Validate device and service state if (!this.device || !this.service) { this.platform.log.warn(`${((_a = this.device) === null || _a === void 0 ? void 0 : _a.deviceName) || 'Unknown'}: Cannot update rotation speed - device or service not available`); return; } // Check if device has a valid speed value if (typeof this.device.speed !== 'number' || this.device.speed <= 0) { this.platform.log.debug(`${this.device.deviceName}: No valid speed to restore (speed: ${this.device.speed})`); return; } // Convert device speed to percentage with validation const percentage = this.speedToPercentage(this.device.speed); if (typeof percentage !== 'number' || isNaN(percentage) || percentage < 0 || percentage > 100) { this.platform.log.warn(`${this.device.deviceName}: Invalid percentage calculated (${percentage}) from speed ${this.device.speed}`); return; } // Update the HomeKit characteristic this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, percentage); this.platform.log.debug(`${this.device.deviceName}: Updated rotation speed to ${percentage}% (speed: ${this.device.speed}) after turning on`); } catch (error) { this.handleDeviceError('update Air131 rotation speed after turn on', error); } } /** * Convert percentage to device speed */ percentageToSpeed(percentage) { const maxSpeed = this.getMaxFanSpeed(); // Ensure we have valid numbers if (maxSpeed <= 0 || typeof percentage !== 'number') { return 1; // Default to lowest speed } // Ensure percentage is between 0 and 100 percentage = Math.min(100, Math.max(0, percentage)); // For devices with 3 speed levels if (maxSpeed === 3) { if (percentage <= 33.34) { return 1; // Low } else if (percentage <= 66.67) { return 2; // Medium } else { return 3; // High } } // For devices with 4 speed levels else if (maxSpeed === 4) { if (percentage <= 25) { return 1; // Low } else if (percentage <= 50) { return 2; // Medium-Low } else if (percentage <= 75) { return 3; // Medium-High } else { return 4; // High } } // Default calculation return Math.max(1, Math.min(Math.round((percentage / 100) * maxSpeed), maxSpeed)); } /** * Set up the air purifier service */ setupService() { // Get or create the air purifier service this.service = this.accessory.getService(this.platform.Service.AirPurifier) || this.accessory.addService(this.platform.Service.AirPurifier); // 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 const targetStateChar = this.service.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState); // For Core200S, only allow manual mode if (this.device.deviceType.includes('Core200S')) { targetStateChar.setProps({ validValues: [0] // MANUAL only }); } else if (this.device.deviceType.includes('Core300S')) { // Explicitly ensure Core300S has both AUTO and MANUAL modes // Force the mode toggle to be visible for Core300S devices // Enable mode toggle for Core300S device // First remove any existing characteristic to ensure clean setup if (this.service.testCharacteristic(this.platform.Characteristic.TargetAirPurifierState)) { this.service.removeCharacteristic(this.service.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState)); } // Re-add the characteristic with proper properties const newTargetStateChar = this.service.addCharacteristic(this.platform.Characteristic.TargetAirPurifierState); newTargetStateChar.setProps({ validValues: [0, 1], perms: ["pr" /* this.platform.Characteristic.Perms.PAIRED_READ */, "pw" /* this.platform.Characteristic.Perms.PAIRED_WRITE */, "ev" /* this.platform.Characteristic.Perms.NOTIFY */] }); // Set up the characteristic handlers newTargetStateChar.onGet(async () => { try { const value = await this.getTargetState(); return value; } catch (error) { this.platform.log.error(`Error getting target state: ${error}`); throw error; } }); newTargetStateChar.onSet(async (value) => { try { await this.setTargetState(value); } catch (error) { this.platform.log.error(`Error setting target state: ${error}`); throw error; } }); } else { targetStateChar.setProps({ validValues: [0, 1] // MANUAL and AUTO }); // Set up the characteristic handlers for non-Core300S devices this.setupCharacteristic(this.platform.Characteristic.TargetAirPurifierState, this.getTargetState.bind(this), this.setTargetState.bind(this)); } // Set up current state this.setupCharacteristic(this.platform.Characteristic.CurrentAirPurifierState, this.getCurrentState.bind(this)); // Set up speed control with special handling for Core200S and Core300S if (this.device.deviceType.includes('Core200S') || this.device.deviceType.includes('Core300S')) { // For Core200S and Core300S, set up rotation speed characteristic manually // Set up rotation speed for Core200S/Core300S // First remove any existing characteristic to ensure clean setup if (this.service.testCharacteristic(this.platform.Characteristic.RotationSpeed)) { this.service.removeCharacteristic(this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)); } // Re-add the characteristic with proper properties const rotationSpeedChar = this.service.addCharacteristic(this.platform.Characteristic.RotationSpeed); const minStep = this.calculateRotationSpeedStep(); rotationSpeedChar.setProps({ minValue: 0, maxValue: 100, minStep: minStep, perms: ["pr" /* this.platform.Characteristic.Perms.PAIRED_READ */, "pw" /* this.platform.Characteristic.Perms.PAIRED_WRITE */, "ev" /* this.platform.Characteristic.Perms.NOTIFY */] }); // Set up the characteristic handlers rotationSpeedChar.onGet(async () => { try { const value = await this.getRotationSpeed(); this.platform.log.debug(`${this.device.deviceName} getRotationSpeed returned: ${value}`); return value; } catch (error) { this.platform.log.error(`Error getting rotation speed: ${error}`); throw error; } }); rotationSpeedChar.onSet(async (value) => { try { this.platform.log.debug(`${this.device.deviceName} setRotationSpeed called with: ${value}`); await this.setRotationSpeed(value); } catch (error) { this.platform.log.error(`Error setting rotation speed: ${error}`); throw error; } }); } else { // For other devices, use the standard setup with appropriate step size const minStep = this.calculateRotationSpeedStep(); this.setupCharacteristic(this.platform.Characteristic.RotationSpeed, this.getRotationSpeed.bind(this), this.setRotationSpeed.bind(this), { minValue: 0, maxValue: 100, minStep: minStep }); } // Add Name characteristic this.setupCharacteristic(this.platform.Characteristic.Name, async () => this.device.deviceName); // Set up air quality sensor service if supported this.platform.log.debug(`${this.device.deviceName}: Checking air_quality feature...`); if (this.hasFeature('air_quality')) { this.platform.log.debug(`${this.device.deviceName}: Setting up air quality service`); this.setupAirQualityService(); } else { this.platform.log.debug(`${this.device.deviceName}: Air quality not supported, skipping air quality service`); } // Set up filter maintenance service if supported this.platform.log.debug(`${this.device.deviceName}: Checking filter_life feature...`); if (this.hasFeature('filter_life')) { this.platform.log.debug(`${this.device.deviceName}: Setting up filter maintenance service`); this.setupFilterService(); } else { this.platform.log.debug(`${this.device.deviceName}: Filter life not supported, skipping filter maintenance service`); } } /** * Set up the air quality sensor service */ setupAirQualityService() { var _a; this.platform.log.debug(`Setting up air quality service for ${this.device.deviceName}`); // Get or create the air quality sensor service this.airQualityService = this.accessory.getService(this.platform.Service.AirQualitySensor) || this.accessory.addService(this.platform.Service.AirQualitySensor); // Set up required air quality characteristic this.setupCharacteristic(this.platform.Characteristic.AirQuality, this.getAirQuality.bind(this), undefined, {}, this.airQualityService); // Set up optional PM2.5 density characteristic this.setupCharacteristic(this.platform.Characteristic.PM2_5Density, this.getPM25Density.bind(this), undefined, { minValue: 0, maxValue: 1000, minStep: 1 }, this.airQualityService); // Set up optional PM10 density characteristic (if available) const extendedDevice = this.device; if (((_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.pm10) !== undefined) { this.setupCharacteristic(this.platform.Characteristic.PM10Density, this.getPM10Density.bind(this), undefined, { minValue: 0, maxValue: 1000, minStep: 1 }, this.airQualityService); } } /** * Set up the filter maintenance service */ setupFilterService() { var _a; this.platform.log.debug(`Setting up filter maintenance service for ${this.device.deviceName}`); // Log filter life data for debugging const extendedDevice = this.device; const filterLifeData = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.filter_life; this.platform.log.debug(`Filter life data: ${JSON.stringify(filterLifeData)} (type: ${typeof filterLifeData})`); this.platform.log.debug(`Device filterLife property: ${extendedDevice.filterLife} (type: ${typeof extendedDevice.filterLife})`); // Get or create the filter maintenance service (match humidifier implementation) this.filterService = this.accessory.getService(this.platform.Service.FilterMaintenance) || this.accessory.addService(this.platform.Service.FilterMaintenance); this.platform.log.debug(`Filter maintenance service ${this.filterService ? 'created/found' : 'FAILED to create'} for ${this.device.deviceName}`); if (this.filterService) { this.platform.log.debug(`Filter service UUID: ${this.filterService.UUID}, displayName: ${this.filterService.displayName}`); } // Set up required filter change indication characteristic this.setupCharacteristic(this.platform.Characteristic.FilterChangeIndication, this.getFilterChangeIndication.bind(this), undefined, {}, this.filterService); this.platform.log.debug(`Set up FilterChangeIndication characteristic`); // Set up optional filter life level characteristic this.setupCharacteristic(this.platform.Characteristic.FilterLifeLevel, this.getFilterLifeLevel.bind(this), undefined, { minValue: 0, maxValue: 100, minStep: 1 }, this.filterService); this.platform.log.debug(`Set up FilterLifeLevel characteristic`); // Set service name using setupCharacteristic like the humidifier this.setupCharacteristic(this.platform.Characteristic.Name, async () => `${this.device.deviceName} Filter`, undefined, {}, this.filterService); this.platform.log.debug(`Set up Name characteristic for filter service`); // Log all services on the accessory to verify filter service is registered const services = this.accessory.services; if (services && services.length > 0) { this.platform.log.debug(`All services on accessory ${this.device.deviceName}:`); services.forEach(service => { this.platform.log.debug(` - ${service.displayName || 'Unnamed'} (UUID: ${service.UUID})`); }); } else { this.platform.log.debug(`No services found on accessory ${this.device.deviceName}`); } } /** * Get air quality level for HomeKit */ async getAirQuality() { var _a, _b, _c, _d; const extendedDevice = this.device; // Get PM2.5 value from device const pm25 = (_d = (_b = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.air_quality_value) !== null && _b !== void 0 ? _b : (_c = extendedDevice.details) === null || _c === void 0 ? void 0 : _c.pm25) !== null && _d !== void 0 ? _d : 0; // Convert to HomeKit air quality scale using existing method return this.convertAirQualityToHomeKit(pm25); } /** * Get PM2.5 density */ async getPM25Density() { var _a, _b, _c, _d; const extendedDevice = this.device; const pm25 = (_d = (_b = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.air_quality_value) !== null && _b !== void 0 ? _b : (_c = extendedDevice.details) === null || _c === void 0 ? void 0 : _c.pm25) !== null && _d !== void 0 ? _d : 0; // Ensure value is within HomeKit limits (0-1000) return Math.min(1000, Math.max(0, pm25)); } /** * Get PM10 density */ async getPM10Density() { var _a, _b; const extendedDevice = this.device; const pm10 = (_b = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.pm10) !== null && _b !== void 0 ? _b : 0; // Ensure value is within HomeKit limits (0-1000) return Math.min(1000, Math.max(0, pm10)); } /** * Get filter change indication */ async getFilterChangeIndication() { var _a; const extendedDevice = this.device; // Use the filterLife property from the VeSync device, which already handles different formats // Fall back to manual parsing if filterLife property is not available let filterLife = 100; // Default to 100% if no filter life data if (typeof extendedDevice.filterLife === 'number' && !isNaN(extendedDevice.filterLife)) { // Use the built-in filterLife property from VeSyncFan filterLife = extendedDevice.filterLife; } else { // Fallback: manually parse details.filter_life for compatibility const filterLifeData = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.filter_life; if (typeof filterLifeData === 'number' && !isNaN(filterLifeData)) { filterLife = filterLifeData; } else if (typeof filterLifeData === 'object' && filterLifeData !== null && 'percent' in filterLifeData) { const percent = filterLifeData.percent; filterLife = (typeof percent === 'number' && !isNaN(percent)) ? percent : 100; } } // Handle NaN, null, undefined cases if (isNaN(filterLife) || filterLife === null || filterLife === undefined) { filterLife = 100; // Default to 100 (filter OK) for invalid values in change indication } // Indicate filter change needed when life is below 10% return filterLife < 10 ? 1 : 0; // 0=FILTER_OK, 1=CHANGE_FILTER } /** * Get filter life level */ async getFilterLifeLevel() { var _a; const extendedDevice = this.device; // Use the filterLife property from the VeSync device, which already handles different formats // Fall back to manual parsing if filterLife property is not available let filterLife = 100; // Default to 100% if no filter life data if (typeof extendedDevice.filterLife === 'number' && !isNaN(extendedDevice.filterLife)) { // Use the built-in filterLife property from VeSyncFan filterLife = extendedDevice.filterLife; } else { // Fallback: manually parse details.filter_life for compatibility const filterLifeData = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.filter_life; if (typeof filterLifeData === 'number' && !isNaN(filterLifeData)) { filterLife = filterLifeData; } else if (typeof filterLifeData === 'object' && filterLifeData !== null && 'percent' in filterLifeData) { const percent = filterLifeData.percent; filterLife = (typeof percent === 'number' && !isNaN(percent)) ? percent : 100; } } // Handle NaN, null, undefined cases if (isNaN(filterLife) || filterLife === null || filterLife === undefined) { filterLife = 0; // Default to 0 for invalid values } // Ensure value is within HomeKit limits (0-100) and round to nearest integer return Math.round(Math.min(100, Math.max(0, filterLife))); } /** * Get current state (INACTIVE = 0, IDLE = 1, PURIFYING_AIR = 2) */ async getCurrentState() { return this.device.deviceStatus === 'on' ? 2 : 0; } /** * Get device capabilities */ getDeviceCapabilities() { return { hasSpeed: true, hasAirQuality: this.hasFeature('air_quality'), hasChildLock: false, hasBrightness: false, hasColorTemp: false, hasColor: false, hasHumidity: false, hasWaterLevel: false, hasSwingMode: false, }; } /** * Update device states based on the latest details */ async updateDeviceSpecificStates(details) { var _a, _b, _c, _d, _e, _f; // Update power state const isOn = details.enabled || details.deviceStatus === 'on'; this.service.updateCharacteristic(this.platform.Characteristic.Active, isOn ? 1 : 0); // Update current state (INACTIVE = 0, IDLE = 1, PURIFYING_AIR = 2) this.service.updateCharacteristic(this.platform.Characteristic.CurrentAirPurifierState, isOn ? 2 : 0); // Get the extended device to access mode information const extendedDevice = this.device; // Update target state (MANUAL = 0, AUTO = 1) // Special handling for Core200S let targetState = 0; // Default to MANUAL if (this.device.deviceType.includes('Core200S')) { // Core200S always reports MANUAL mode targetState = 0; // MANUAL } else if (extendedDevice.mode) { // For other devices, including Core300S, use the reported mode targetState = extendedDevice.mode === 'auto' ? 1 : 0; } this.service.updateCharacteristic(this.platform.Characteristic.TargetAirPurifierState, targetState); // Update rotation speed if (isOn && details.speed !== undefined && details.speed !== null) { // Check if we should skip this update if (this.skipNextUpdate) { this.platform.log.debug(`Skipping update for ${this.device.deviceName} as requested`); this.skipNextUpdate = false; return; } // If we've recently set the speed, use the percentage we calculated if (this.lastSetSpeed > 0 && this.lastSetPercentage > 0) { this.platform.log.debug(`Using last set percentage for ${this.device.deviceName}: speed=${this.lastSetSpeed}, percentage=${this.lastSetPercentage}`); // Only update if the device speed matches what we set if (this.lastSetSpeed === details.speed) { this.platform.log.debug(`Device speed matches last set speed: ${details.speed}`); // Force update the characteristic to our last set percentage this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.lastSetPercentage); } else { // Device speed doesn't match what we set, so convert from device speed const percentage = this.speedToPercentage(details.speed); this.platform.log.debug(`Device speed doesn't match last set speed. Device: ${details.speed}, Last set: ${this.lastSetSpeed}, Calculated percentage: ${percentage}`); // Update our tracking variables this.lastSetSpeed = details.speed; this.lastSetPercentage = percentage; // Update the characteristic this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, percentage); } } else { // No recent set speed, so convert from device speed const percentage = this.speedToPercentage(details.speed); this.platform.log.debug(`No recent set speed for ${this.device.deviceName}. Device speed: ${details.speed}, Calculated percentage: ${percentage}`); // Update the characteristic this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, percentage); } } else { this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, 0); // Reset tracking variables when device is off this.lastSetSpeed = 0; this.lastSetPercentage = 0; } // Update air quality characteristics if service exists if (this.airQualityService && this.hasFeature('air_quality')) { const extendedDevice = this.device; // Update air quality level const pm25 = (_d = (_b = (_a = extendedDevice.details) === null || _a === void 0 ? void 0 : _a.air_quality_value) !== null && _b !== void 0 ? _b : (_c = extendedDevice.details) === null || _c === void 0 ? void 0 : _c.pm25) !== null && _d !== void 0 ? _d : 0; const airQualityLevel = this.convertAirQualityToHomeKit(pm25); this.airQualityService.updateCharacteristic(this.platform.Characteristic.AirQuality, airQualityLevel); // Update PM2.5 density this.airQualityService.updateCharacteristic(this.platform.Characteristic.PM2_5Density, Math.min(1000, Math.max(0, pm25))); // Update PM10 density if available if (((_e = extendedDevice.details) === null || _e === void 0 ? void 0 : _e.pm10) !== undefined) { this.airQualityService.updateCharacteristic(this.platform.Characteristic.PM10Density, Math.min(1000, Math.max(0, extendedDevice.details.pm10))); } } // Set up filter service if not already created and device supports it if (!this.filterService && this.hasFeature('filter_life')) { this.platform.log.debug(`${this.device.deviceName}: Creating filter service during state update (was not created during setup)`); this.setupFilterService(); } // Update filter characteristics if service exists if (this.filterService && this.hasFeature('filter_life')) { const extendedDevice = this.device; // Use the same logic as the getter methods let filterLife = 100; // Default to 100% if no filter life data if (typeof extendedDevice.filterLife === 'number' && !isNaN(extendedDevice.filterLife)) { // Use the built-in filterLife property from VeSyncFan filterLife = extendedDevice.filterLife; } else { // Fallback: manually parse details.filter_life for compatibility const filterLifeData = (_f = extendedDevice.details) === null || _f === void 0 ? void 0 : _f.filter_life; if (typeof filterLifeData === 'number' && !isNaN(filterLifeData)) { filterLife = filterLifeData; } else if (typeof filterLifeData === 'object' && filterLifeData !== null && 'percent' in filterLifeData) { const percent = filterLifeData.percent; filterLife = (typeof percent === 'number' && !isNaN(percent)) ? percent : 100; } } // Handle NaN, null, undefined cases if (isNaN(filterLife) || filterLife === null || filterLife === undefined) { filterLife = 100; // Default to 100% for invalid values } // Update filter change indication this.filterService.updateCharacteristic(this.platform.Characteristic.FilterChangeIndication, filterLife < 10 ? 1 : 0); // Update filter life level this.filterService.updateCharacteristic(this.platform.Characteristic.FilterLifeLevel, Math.min(100, Math.max(0, filterLife))); } } /** * Get target state (MANUAL = 0, AUTO = 1) */ async getTargetState() { // For Core200S, always return MANUAL if (this.device.deviceType.includes('Core200S')) { return 0; // MANUAL } const extendedDevice = this.device; const targetState = extendedDevice.mode === 'auto' ? 1 : 0; return targetState; } /** * Set target state (AUTO = 0, MANUAL = 1) */ async setTargetState(value) { try { const targetState = value; const extendedDevice = this.device; // For Core200S, don't allow mode changes if (this.device.deviceType.includes('Core200S')) { this.platform.log.warn(`Mode changes not supported on ${this.device.deviceName}, only fan speed control is available`); // Update the characteristic back to MANUAL (0) this.service.updateCharacteristic(this.platform.Characteristic.TargetAirPurifierState, 0); return; } // For all other devices, including Core300S, normal mode handling const mode = targetState === 1 ? 'auto' : 'manual'; this.platform.log.debug(`Setting mode to ${mode} for device: ${this.device.deviceName}`); let success = false; // Use the appropriate method to set the mode if (targetState === 1 && typeof extendedDevice.autoMode === 'function') { success = await extendedDevice.autoMode(); } else if (targetState === 0 && typeof extendedDevice.manualMode === 'function') { success = await extendedDevice.manualMode(); } else if (typeof extendedDevice.setMode === 'function') { success = await extendedDevice.setMode(mode); } else if (typeof this.device.setMode === 'function') { success = await this.device.setMode(mode); } else { throw new Error('Device API does not support mode setting operations'); } if (!success) { throw new Error(`Failed to set mode to ${mode}`); } // Update device state and characteristics await this.updateDeviceSpecificStates(this.device); } catch (error) { this.handleDeviceError('set target state', error); } } /** * Get rotation speed */ async getRotationSpeed() { // If device is off or speed is not defined, return 0 if (this.device.deviceStatus !== 'on' || this.device.speed === undefined || this.device.speed === null) { return 0; } // If we have a last set percentage, use that instead of calculating from device speed if (this.lastSetPercentage > 0 && this.lastSetSpeed === this.device.speed) { this.platform.log.debug(`getRotationSpeed returning last set percentage: ${this.lastSetPercentage} for speed: ${this.device.speed}`); return this.lastSetPercentage; } // Convert device speed to percentage using our consistent conversion method const percentage = this.speedToPercentage(this.device.speed); this.platform.log.debug(`getRotationSpeed calculated percentage: ${percentage} for speed: ${this.device.speed}`); return percentage; } /** * Set rotation speed */ async setRotationSpeed(value) { try { // Ensure value is a valid number let percentage = value; if (isNaN(percentage) || percentage === null || percentage === undefined) { this.platform.log.warn(`Invalid rotation speed value: ${value} for device: ${this.device.deviceName}`); return; } // Ensure percentage is between 0 and 100 percentage = Math.min(100, Math.max(0, percentage)); 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'); } // Reset tracking variables when turning off this.lastSetSpeed = 0; this.lastSetPercentage = 0; return; } const extendedDevice = this.device; // Check if device is off and turn it on first if (this.device.deviceStatus !== 'on') { this.platform.log.debug(`Device ${this.device.deviceName} is off, turning on before setting speed`); const turnOnSuccess = await this.device.turnOn(); if (!turnOnSuccess) { // For LV-PUR131S, this might return false if already on, so check actual state if (this.isAir131Device && this.device.deviceStatus === 'on') { this.platform.log.debug(`${this.device.deviceName}: Device is now on`); } else { throw new Error('Failed to turn on device'); } } // Wait a bit for the device to turn on await new Promise(resolve => setTimeout(resolve, 1000)); // Update device state to get fresh mode information await this.updateDeviceSpecificStates(this.device); } // Check if fan speed control is supported in current mode if (!this.isFeatureSupportedInCurrentMode('fan_speed')) { this.platform.log.warn(`Fan speed control not supported in ${extendedDevice.mode} mode for device: ${this.device.deviceName}`); // For Air131 devices, switch to manual mode first if (this.isAir131Device && extendedDevice.mode !== 'manual') { this.platform.log.debug(`Setting device to manual mode before changing fan speed: ${this.device.deviceName}`); // Use the appropriate method to set manual mode if (typeof extendedDevice.manualMode === 'function') { const modeSuccess = await extendedDevice.manualMode(); this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`); // Wait for mode change to take effect await new Promise(resolve => setTimeout(resolve, 1000)); } else if (typeof extendedDevice.setMode === 'function') { const modeSuccess = await extendedDevice.setMode('manual'); this.platform.log.debug(`Set manual mode result: ${modeSuccess ? 'success' : 'failed'}`); // Wait for mode change to take effect await new Promise(resolve => setTimeout(resolve, 1000)); } } } // Convert percentage to device speed using our consistent conversion method const speed = this.percentageToSpeed(percentage); // Final validation to ensure speed is a valid number if (isNaN(speed) || speed === null || speed === undefined) { this.platform.log.warn(`Calculated invalid speed: ${speed} from percentage: ${percentage} for device: ${this.device.deviceName}`); return; } this.platform.log.debug(`Setting fan speed to ${speed} for device: ${this.device.deviceName}`); // Store the values we're setting before making the API call this.lastSetSpeed = speed; this.lastSetPercentage = percentage; // Set the flag to skip the next update this.skipNextUpdate = true; // Immediately update the characteristic to show the correct percentage // This ensures the slider shows the value we set, not what the device reports this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, percentage); const success = await this.device.changeFanSpeed(speed); if (!success) { // If the API call failed, reset our tracking variables this.lastSetSpeed = 0; this.lastSetPercentage = 0; throw new Error(`Failed to set speed to ${speed}`); } this.platform.log.debug(`Successfully set fan speed to ${speed} (${percentage}%) for device: ${this.device.deviceName}`); // Update device state and characteristics // Skip this to avoid overriding our UI update // await this.updateDeviceSpecificStates(this.device); } catch (error) { this.handleDeviceError('set rotation speed', error); } } async getActive() { return this.device.deviceStatus === 'on' ? 1 : 0; } async setActive(value) { try { const isOn = value === 1; this.platform.log.debug(`Setting device ${this.device.deviceName} to ${isOn ? 'on' : 'off'}`); const success = isOn ? await this.device.turnOn() : await this.device.turnOff(); if (!success) { // For LV-PUR131S, success=false might mean device is already in desired state if (this.isAir131Device) { this.platform.log.debug(`${this.device.deviceName}: turn${isOn ? 'On' : 'Off'}() returned false - device may already be ${isOn ? 'on' : 'off'}`); // Check actual device state const actualState = this.device.deviceStatus === 'on'; if (actualState === isOn) { this.platform.log.debug(`${this.device.deviceName}: Device is already in desired state (${isOn ? 'on' : 'off'})`); return; // Don't throw error if already in desired state } } throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} device`); } // Reset tracking variables when turning off if (!isOn) { this.lastSetSpeed = 0; this.lastSetPercentage = 0; } else if (isOn && this.isAir131Device) { // For Air131 devices, immediately update rotation speed characteristic // to show the restored speed instead of 0 await this.updateAir131RotationSpeedAfterTurnOn(); } // Update device state and characteristics await this.updateDeviceSpecificStates(this.device); await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off'); } catch (error) { this.handleDeviceError('set active state', error); } } } exports.AirPurifierAccessory = AirPurifierAccessory; //# sourceMappingURL=air-purifier.accessory.js.map