UNPKG

tsvesync

Version:

A TypeScript library for interacting with VeSync smart home devices

565 lines (564 loc) 25.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VeSyncAirBaseV2 = void 0; const airBypass_1 = require("./airBypass"); const logger_1 = require("../logger"); const helpers_1 = require("../helpers"); /** * VeSync Air Purifier with Bypass V2 */ class VeSyncAirBaseV2 extends airBypass_1.VeSyncAirBypass { constructor(details, manager) { super(details, manager); this._lightDetection = false; this._lightDetectionState = false; this.setSpeedLevel = null; this.enabled = false; this._mode = ''; this._speed = 0; this._timer = null; this.autoPreferences = [...this.getConfigAutoPreferences(['default', 'efficient', 'quiet'])]; logger_1.logger.debug(`Initialized VeSyncAirBaseV2 device: ${this.deviceName}`); } buildConfigDict(configDict) { if (configDict) { this.config = { ...this.config, // Preserve existing config ...configDict // Merge in new config }; } } get mode() { return this._mode; } set mode(value) { this._mode = value; } get speed() { return this._speed; } set speed(value) { this._speed = value; } get timer() { return this._timer; } set timer(value) { this._timer = value; } /** * Check response for bypassV2 purifiers that surface nested result codes. */ checkV2Response(response, status, method) { // First check basic response structure if (!this.checkResponse([response, status], method)) { return false; } // Only apply extended handling to bypassV2 purifiers (Vital/Everest) if (!this.requiresPowerSwitchPayload()) { return true; } // These devices frequently succeed with a nested non-zero code if (response.result && response.result.code !== undefined && response.result.code !== 0) { logger_1.logger.debug(`${this.deviceName} returned inner code ${response.result.code} for ${method}`); logger_1.logger.debug('BypassV2 purifier reports non-zero inner code but request still succeeded'); this.connectionStatus = 'online'; return true; } return true; } /** * Get device details */ async getDetails() { var _a; logger_1.logger.debug(`Getting details for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('getPurifierStatus'); const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); const needsV2Handling = this.requiresPowerSwitchPayload(); // First check the outer response if (!response || status !== 200) { logger_1.logger.debug(`Error getting purifier details for ${this.deviceName}: Invalid response`); this.connectionStatus = 'offline'; return false; } // For bypassV2 purifier families the API nests the result and may surface non-zero codes if (needsV2Handling && response.result) { // Check if we have a valid getPurifierStatus response if (response.code === 0 && ((_a = response.result) === null || _a === void 0 ? void 0 : _a.result)) { const statusResponse = response.result.result; // If we have valid status data, use it regardless of inner result code logger_1.logger.debug(`Using nested status data for bypassV2 purifier: ${this.deviceName}`); this.buildPurifierDict(statusResponse); if (statusResponse.configuration) { this.buildConfigDict(statusResponse.configuration); } // Keep device online since we have valid status this.connectionStatus = 'online'; return true; } } // Standard processing for non-bypassV2 devices or when no status data is available if (!this.checkResponse([response, status], 'getDetails') || !(response === null || response === void 0 ? void 0 : response.result)) { logger_1.logger.debug('Error getting purifier details'); this.connectionStatus = 'offline'; return false; } const innerResponse = response.result; if (innerResponse.code !== 0 || !innerResponse.result) { logger_1.logger.debug('Error in inner response from purifier'); this.connectionStatus = 'offline'; return false; } const deviceData = innerResponse.result; this.buildPurifierDict(deviceData); if (deviceData.configuration) { this.buildConfigDict(deviceData.configuration); } return true; } /** * Build purifier dictionary */ buildPurifierDict(devDict) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; this.connectionStatus = 'online'; const powerSwitch = Boolean((_a = devDict.powerSwitch) !== null && _a !== void 0 ? _a : 0); this.enabled = powerSwitch; this.deviceStatus = powerSwitch ? 'on' : 'off'; this.mode = (_b = devDict.workMode) !== null && _b !== void 0 ? _b : 'manual'; const reportedFanSpeed = (_c = devDict.fanSpeedLevel) !== null && _c !== void 0 ? _c : 0; const isAutoSentinel = reportedFanSpeed === 255; this.speed = isAutoSentinel ? 0 : reportedFanSpeed; this.setSpeedLevel = (_d = devDict.manualSpeedLevel) !== null && _d !== void 0 ? _d : 1; // Parse filter life with proper fallback handling and logging let filterLife = 0; if (devDict.filterLifePercent !== undefined) { filterLife = devDict.filterLifePercent; logger_1.logger.debug(`${this.deviceName}: Parsed filter life from filterLifePercent: ${filterLife}%`); } else if (devDict.filter_life !== undefined) { filterLife = devDict.filter_life; logger_1.logger.debug(`${this.deviceName}: Parsed filter life from filter_life fallback: ${filterLife}%`); } else { logger_1.logger.debug(`${this.deviceName}: No filter life data found in API response`); if (logger_1.logger.getLevel && logger_1.logger.getLevel() !== undefined && logger_1.logger.getLevel() <= 0) { logger_1.logger.debug(`${this.deviceName}: Device dict structure:`, JSON.stringify(devDict, null, 2)); } } this.details = { ...this.details, filter_life: filterLife, child_lock: Boolean((_e = devDict.childLockSwitch) !== null && _e !== void 0 ? _e : 0), display: Boolean((_f = devDict.screenState) !== null && _f !== void 0 ? _f : 0), light_detection_switch: Boolean((_g = devDict.lightDetectionSwitch) !== null && _g !== void 0 ? _g : 0), environment_light_state: Boolean((_h = devDict.environmentLightState) !== null && _h !== void 0 ? _h : 0), screen_switch: Boolean((_j = devDict.screenSwitch) !== null && _j !== void 0 ? _j : 0), screenStatus: Boolean((_k = devDict.screenSwitch) !== null && _k !== void 0 ? _k : 0) ? 'on' : 'off', manualSpeedLevel: this.setSpeedLevel, mode: this.mode, speed: this.speed, isAutoSentinel }; if (this.hasFeature('air_quality')) { this.details.air_quality_value = (_l = devDict.PM25) !== null && _l !== void 0 ? _l : 0; this.details.air_quality = (_m = devDict.AQLevel) !== null && _m !== void 0 ? _m : 0; const normalizedAirQuality = helpers_1.Helpers.normalizeAirQuality(devDict.AQLevel); if (normalizedAirQuality.level >= 1) { this.details.air_quality_level = normalizedAirQuality.level; } this.details.air_quality_label = normalizedAirQuality.label; } if ('PM1' in devDict) this.details.pm1 = devDict.PM1; if ('PM10' in devDict) this.details.pm10 = devDict.PM10; if ('AQPercent' in devDict) this.details.aq_percent = devDict.AQPercent; if ('fanRotateAngle' in devDict) this.details.fan_rotate_angle = devDict.fanRotateAngle; if ('filterOpenState' in devDict) this.details.filter_open_state = Boolean(devDict.filterOpenState); if (((_o = devDict.timerRemain) !== null && _o !== void 0 ? _o : 0) > 0) { this.timer = { duration: devDict.timerRemain, action: 'off' }; } if (typeof devDict.autoPreference === 'object' && devDict.autoPreference) { this.details.auto_preference_type = (_q = (_p = devDict.autoPreference) === null || _p === void 0 ? void 0 : _p.autoPreferenceType) !== null && _q !== void 0 ? _q : 'default'; } else { this.details.auto_preference_type = null; } } /** * Determine whether this purifier requires the powerSwitch payload and nested response handling. */ requiresPowerSwitchPayload() { const type = (this.deviceType || '').toUpperCase(); return type.startsWith('LAP-V') || type.startsWith('LAP-EL'); } /** * Override turn on to use the powerSwitch payload required by bypassV2 purifiers. */ async turnOn() { logger_1.logger.info(`Turning on device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setSwitch'); // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation if (this.requiresPowerSwitchPayload()) { logger_1.logger.debug(`Using bypassV2 power payload for ${this.deviceName}`); body.payload.data = { powerSwitch: 1, switchIdx: 0 }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); // Use bypassV2 response checking const success = this.checkV2Response(response, status, 'turnOn'); if (success) { this.deviceStatus = 'on'; // Force a details update to sync state setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); } return success; } else { // Use parent implementation for non-bypassV2 models return super.turnOn(); } } /** * Override turn off to use the powerSwitch payload required by bypassV2 purifiers. */ async turnOff() { logger_1.logger.info(`Turning off device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setSwitch'); // BypassV2 purifiers require powerSwitch toggles; others fall back to the base implementation if (this.requiresPowerSwitchPayload()) { logger_1.logger.debug(`Using bypassV2 power payload for ${this.deviceName}`); body.payload.data = { powerSwitch: 0, switchIdx: 0 }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); // Use bypassV2 response checking const success = this.checkV2Response(response, status, 'turnOff'); if (success) { this.deviceStatus = 'off'; // Force a details update to sync state setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); } return success; } else { // Use parent implementation for non-bypassV2 models return super.turnOff(); } } /** * Override setMode to send workMode payloads for bypassV2 purifiers. */ async setMode(mode) { var _a, _b; const targetMode = mode.toLowerCase(); const supportedModes = this.modes.map(m => m.toLowerCase()); if (!supportedModes.includes(targetMode)) { const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`; logger_1.logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } // Special handling for Core200S stays the same for all models if (this.deviceType.includes('Core200S') && targetMode === 'auto') { logger_1.logger.warn(`Auto mode not supported for ${this.deviceType}, using manual mode instead`); return this.setMode('manual'); } // Manual mode should leverage changeFanSpeed to mirror pyvesync behaviour if (targetMode === 'manual') { const defaultLevel = this.config.levels && this.config.levels.length > 0 ? this.config.levels[0] : 1; const desiredLevel = (_b = (_a = this.details.manualSpeedLevel) !== null && _a !== void 0 ? _a : this.speed) !== null && _b !== void 0 ? _b : defaultLevel; const success = await this.changeFanSpeed(desiredLevel); if (success) { this.details.mode = 'manual'; this.mode = 'manual'; } return success; } // BypassV2 purifiers need the workMode payload; other models use the base implementation if (this.requiresPowerSwitchPayload()) { logger_1.logger.debug(`Using bypassV2 mode payload for ${this.deviceName}`); const [head, body] = this.buildApiDict('setPurifierMode'); body.payload.data = { workMode: targetMode }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); const success = this.checkV2Response(response, status, 'setMode'); if (success) { this.details.mode = targetMode; this.mode = targetMode; // Update cached speed snapshot for special modes if (targetMode === 'sleep') { this.speed = 0; this.details.speed = 0; } else if (targetMode === 'turbo') { const maxSpeed = this.config.levels && this.config.levels.length > 0 ? Math.max(...this.config.levels) : this.speed; if (typeof maxSpeed === 'number' && maxSpeed > 0) { this.speed = maxSpeed; this.details.speed = maxSpeed; } } else if (targetMode === 'auto') { this.speed = 0; this.details.speed = 0; this.details.isAutoSentinel = true; } // Force a details update to sync state setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); return true; } else { logger_1.logger.error(`Failed to set mode to ${targetMode} for device: ${this.deviceName}`); return false; } } else { // Use parent implementation for non-bypassV2 models return super.setMode(mode); } } /** * Convenience helper to enter turbo mode when supported. */ async turboMode() { if (!this.modes.map(m => m.toLowerCase()).includes('turbo')) { logger_1.logger.debug(`Turbo mode not supported for device: ${this.deviceName}`); return false; } return this.setMode('turbo'); } /** * Convenience helper to enter pet mode when supported. */ async petMode() { if (!this.modes.map(m => m.toLowerCase()).includes('pet')) { logger_1.logger.debug(`Pet mode not supported for device: ${this.deviceName}`); return false; } return this.setMode('pet'); } /** * Override changeFanSpeed to use manualSpeedLevel for bypassV2 purifiers. */ async changeFanSpeed(speed) { var _a; const speeds = (_a = this.config.levels) !== null && _a !== void 0 ? _a : []; if (speeds.length > 0 && !speeds.includes(speed)) { logger_1.logger.error(`Invalid speed: ${speed}. Must be one of: ${speeds.join(', ')} for device: ${this.deviceName}`); return false; } logger_1.logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setLevel'); // Specific handling for bypassV2 devices if (this.requiresPowerSwitchPayload()) { logger_1.logger.debug(`Using bypassV2 speed payload for ${this.deviceName}`); // For these devices, use levelIdx, levelType, and manualSpeedLevel per YAML spec body.payload.data = { levelIdx: 0, levelType: 'wind', manualSpeedLevel: speed }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); const success = this.checkV2Response(response, status, 'changeFanSpeed'); if (success) { this.speed = speed; this.details.manualSpeedLevel = speed; this.details.speed = speed; this.details.mode = 'manual'; this.mode = 'manual'; this.details.isAutoSentinel = false; // Force a details update to sync state setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); return true; } else { logger_1.logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`); return false; } } else { // For non-bypassV2 models, use id, level, mode, and type body.payload.data = { id: 0, level: speed, mode: 'manual', type: 'wind' }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); const success = this.checkResponse([response, status], 'changeFanSpeed'); if (success) { this.speed = speed; this.details.manualSpeedLevel = speed; this.details.speed = speed; this.details.mode = 'manual'; this.mode = 'manual'; this.details.isAutoSentinel = false; return true; } else { logger_1.logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`); return false; } } } /** * Set auto preference */ async setAutoPreference(preference = 'default', roomSize = 600) { if (!this.autoPreferences.includes(preference)) { logger_1.logger.debug(`Invalid preference: ${preference} - valid preferences are ${this.autoPreferences.join(', ')}`); return false; } const [head, body] = this.buildApiDict('setAutoPreference'); body.payload.data = { autoPreference: preference, roomSize: roomSize }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); // Use the appropriate response checking method based on device type const success = this.requiresPowerSwitchPayload() ? this.checkV2Response(response, status, 'setAutoPreference') : this.checkResponse([response, status], 'setAutoPreference'); if (success) { this.details.auto_preference = preference; this.details.auto_preference_type = preference; // Force a details update to sync state for bypassV2 purifiers if (this.requiresPowerSwitchPayload()) { setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); } } return success; } /** * Set light detection */ async setLightDetection(enabled) { const [head, body] = this.buildApiDict('setLightDetection'); body.payload.data = { lightDetectionSwitch: enabled ? 1 : 0 }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); // Use the appropriate response checking method based on device type const success = this.requiresPowerSwitchPayload() ? this.checkV2Response(response, status, 'setLightDetection') : this.checkResponse([response, status], 'setLightDetection'); if (success) { this.details.light_detection_switch = enabled; // Force a details update to sync state for bypassV2 purifiers if (this.requiresPowerSwitchPayload()) { setTimeout(() => { this.getDetails().catch(e => { logger_1.logger.debug(`Background state refresh failed: ${e}`); }); }, 1000); } } return success; } /** * Get light detection status */ get lightDetection() { return Boolean(this.details.light_detection_switch); } /** * Get light detection state */ get lightDetectionState() { return Boolean(this.details.environment_light_state); } /** * Get auto preference type */ get autoPreferenceType() { return this.details.auto_preference_type; } /** * Display JSON details */ displayJSON() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; const baseInfo = JSON.parse(super.displayJSON()); const details = { ...baseInfo, 'Mode': this.mode, 'Filter Life': ((_a = this.details.filter_life) !== null && _a !== void 0 ? _a : 0).toString(), 'Fan Level': this.speed.toString(), 'Display On': (_c = (_b = this.details.display) === null || _b === void 0 ? void 0 : _b.toString()) !== null && _c !== void 0 ? _c : 'false', 'Child Lock': ((_d = this.details.child_lock) !== null && _d !== void 0 ? _d : false).toString(), 'Night Light': ((_e = this.details.night_light) !== null && _e !== void 0 ? _e : '').toString(), 'Display Set On': ((_f = this.details.screen_switch) !== null && _f !== void 0 ? _f : false).toString(), 'Light Detection Enabled': ((_g = this.details.light_detection_switch) !== null && _g !== void 0 ? _g : false).toString(), 'Environment Light State': ((_h = this.details.environment_light_state) !== null && _h !== void 0 ? _h : false).toString() }; if (this.hasFeature('air_quality')) { details['Air Quality Level'] = ((_j = this.details.air_quality) !== null && _j !== void 0 ? _j : '').toString(); details['Air Quality Value'] = ((_k = this.details.air_quality_value) !== null && _k !== void 0 ? _k : '').toString(); } const everestKeys = { 'pm1': 'PM1', 'pm10': 'PM10', 'fan_rotate_angle': 'Fan Rotate Angle', 'filter_open_state': 'Filter Open State' }; for (const [key, value] of Object.entries(everestKeys)) { if (key in this.details) { details[value] = (_m = (_l = this.details[key]) === null || _l === void 0 ? void 0 : _l.toString()) !== null && _m !== void 0 ? _m : ''; } } return JSON.stringify(details, null, 4); } /** * Set oscillation state */ async setOscillation(toggle) { logger_1.logger.debug(`Setting oscillation to ${toggle ? 'on' : 'off'} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setSwitch'); body.payload.data = { oscillationSwitch: toggle ? 1 : 0, switchIdx: 0 }; const [response, status] = await this.callApi('/cloud/v2/deviceManaged/bypassV2', 'post', body, head); // Use the appropriate response checking method based on device type const success = this.requiresPowerSwitchPayload() ? this.checkV2Response(response, status, 'setOscillation') : this.checkResponse([response, status], 'setOscillation'); if (success) { this.details.oscillationState = toggle; this.details.oscillationSwitch = toggle ? 1 : 0; } return success; } } exports.VeSyncAirBaseV2 = VeSyncAirBaseV2;