UNPKG

tsvesync

Version:

A TypeScript library for interacting with VeSync smart home devices

467 lines (407 loc) 14.6 kB
import { VeSyncFan } from '../vesyncFan'; import { VeSync } from '../vesync'; import { Helpers } from '../helpers'; import { logger } from '../logger'; /** * VeSync Superior 6000S Humidifier Class * Implementation based on PyVeSync's VeSyncSuperior6000S class */ export class VeSyncSuperior6000S extends VeSyncFan { protected readonly modes = ['auto', 'manual', 'sleep', 'humidity'] as const; protected readonly mistLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9]; protected readonly humidityRange = { min: 30, max: 80 }; protected readonly _apiModes = [ 'getHumidifierStatus', 'setSwitch', 'setVirtualLevel', 'setTargetHumidity', 'setHumidityMode', 'setDisplay', 'setDryingMode', ]; // Override details with more specific type override details: { mode: string; speed?: number; filter_life?: number; screen_status?: 'on' | 'off'; child_lock?: boolean; humidity: number; target_humidity: number; mist_level: number; mist_virtual_level: number; water_lacks: boolean; water_tank_lifted: boolean; display: boolean; filter_life_percentage: number; temperature: number; drying_mode: { autoDryingSwitch?: number; dryingState?: number; dryingLevel?: number; dryingRemain?: number; }; configuration: Record<string, any>; connection_status: string | null; }; constructor(details: Record<string, any>, manager: VeSync) { super(details, manager); // Initialize details object this.details = { mode: '', speed: 0, filter_life: 0, screen_status: 'off', child_lock: false, humidity: 0, target_humidity: 0, mist_level: 0, mist_virtual_level: 0, water_lacks: false, water_tank_lifted: false, display: false, filter_life_percentage: 0, temperature: 0, drying_mode: {}, configuration: {}, connection_status: null }; // Update config for Superior 6000S this.config = { module: 'VeSyncAirBypass', // This doesn't matter as we're implementing directly features: ['display', 'humidity', 'mist', 'timer', 'auto_mode', 'drying'], levels: this.mistLevels }; logger.debug(`Initialized VeSyncSuperior6000S device: ${this.deviceName}`); } /** * Build API dictionary for humidifier */ protected buildApiDict(method: string): [Record<string, any>, Record<string, any>] { if (!this._apiModes.includes(method)) { logger.debug(`Invalid mode - ${method}`); throw new Error(`Invalid mode - ${method}`); } const head = Helpers.reqHeaderBypass(); const body = { ...Helpers.reqBody(this.manager, 'bypassV2'), cid: this.cid, configModule: this.configModule, payload: { method: method, source: 'APP', data: {} } }; return [head, body]; } /** * Get device details */ async getDetails(): Promise<Boolean> { logger.debug(`Getting details for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('getHumidifierStatus'); const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'getDetails'); if (success && response?.result?.result) { const result = response.result.result; // Update device status based on powerSwitch field this.deviceStatus = result.powerSwitch === 1 ? 'on' : 'off'; // Build details object with Superior 6000S specific fields this.details = { mode: result.workMode === 'autoPro' ? 'auto' : result.workMode || '', humidity: result.humidity || 0, target_humidity: result.targetHumidity || 0, mist_level: result.mistLevel || 0, mist_virtual_level: result.virtualLevel || 0, water_lacks: result.waterLacksState || false, water_tank_lifted: result.waterTankLifted || false, display: result.screenSwitch === 1, filter_life_percentage: result.filterLifePercent || 0, temperature: result.temperature || 0, drying_mode: result.dryingMode || {}, configuration: result.configuration || {}, connection_status: result.connectionStatus || null }; logger.debug(`Successfully got details for device: ${this.deviceName}`); } return success; } /** * Turn device on */ async turnOn(): Promise<boolean> { return this.toggleSwitch(true); } /** * Turn device off */ async turnOff(): Promise<boolean> { return this.toggleSwitch(false); } /** * Toggle device power */ async toggleSwitch(enabled: boolean): Promise<boolean> { logger.info(`Setting device power to ${enabled ? 'on' : 'off'}: ${this.deviceName}`); const [head, body] = this.buildApiDict('setSwitch'); body.payload.data = { powerSwitch: enabled ? 1 : 0, switchIdx: 0 }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'toggleSwitch'); if (success) { this.deviceStatus = enabled ? 'on' : 'off'; return true; } logger.error(`Failed to set device power to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); return false; } /** * Set mist level */ async setMistLevel(level: number): Promise<boolean> { if (!this.mistLevels.includes(level)) { const error = `Invalid mist level: ${level}. Must be one of: ${this.mistLevels.join(', ')}`; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } logger.info(`Setting mist level to ${level} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setVirtualLevel'); body.payload.data = { levelIdx: 0, virtualLevel: level, levelType: 'mist' }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'setMistLevel'); if (!success) { logger.error(`Failed to set mist level to ${level} for device: ${this.deviceName}`); } return success; } /** * Change fan speed - Implemented to satisfy interface but redirects to setMistLevel */ async changeFanSpeed(speed: number): Promise<boolean> { logger.debug(`Redirecting fan speed change to mist level for device: ${this.deviceName}`); return this.setMistLevel(speed); } /** * Set device mode */ async setMode(mode: string): Promise<boolean> { if (!this.modes.includes(mode as any)) { const error = `Invalid mode: ${mode}. Must be one of: ${this.modes.join(', ')}`; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } logger.debug(`Setting mode to ${mode} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setHumidityMode'); body.payload.data = { workMode: mode === 'auto' ? 'autoPro' : mode }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'setMode'); if (success) { this.details.mode = mode; } else { logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`); } return success; } /** * Set target humidity */ async setHumidity(humidity: number): Promise<boolean> { if (humidity < this.humidityRange.min || humidity > this.humidityRange.max) { const error = `Invalid humidity: ${humidity}. Must be between ${this.humidityRange.min} and ${this.humidityRange.max}`; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } logger.debug(`Setting target humidity to ${humidity}% for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setTargetHumidity'); body.payload.data = { targetHumidity: humidity }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'setHumidity'); if (!success) { logger.error(`Failed to set target humidity to ${humidity}% for device: ${this.deviceName}`); } return success; } /** * Set display status */ async setDisplay(enabled: boolean): Promise<boolean> { logger.debug(`Setting display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setDisplay'); body.payload.data = { screenSwitch: enabled ? 1 : 0 }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'setDisplay'); if (!success) { logger.error(`Failed to set display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); } return success; } /** * Set drying mode enabled */ async setDryingModeEnabled(enabled: boolean): Promise<boolean> { logger.debug(`Setting drying mode to ${enabled ? 'enabled' : 'disabled'} for device: ${this.deviceName}`); const [head, body] = this.buildApiDict('setDryingMode'); body.payload.data = { autoDryingSwitch: enabled ? 1 : 0 }; const [response, status] = await this.callApi( '/cloud/v2/deviceManaged/bypassV2', 'post', body, head ); const success = this.checkResponse([response, status], 'setDryingModeEnabled'); if (!success) { logger.error(`Failed to set drying mode to ${enabled ? 'enabled' : 'disabled'} for device: ${this.deviceName}`); } return success; } /** * Turn on display */ async turnOnDisplay(): Promise<boolean> { logger.debug(`Turning on display for device: ${this.deviceName}`); return this.setDisplay(true); } /** * Turn off display */ async turnOffDisplay(): Promise<boolean> { logger.debug(`Turning off display for device: ${this.deviceName}`); return this.setDisplay(false); } /** * Set auto mode */ async setAutoMode(): Promise<boolean> { logger.debug(`Setting auto mode for device: ${this.deviceName}`); return this.setMode('auto'); } /** * Set manual mode */ async setManualMode(): Promise<boolean> { logger.debug(`Setting manual mode for device: ${this.deviceName}`); return this.setMode('manual'); } /** * Get current humidity * Provides access to the current humidity reading */ get currentHumidity(): number { return this.details.humidity || 0; } /** * Get target humidity * Provides access to the target humidity setting */ get targetHumidity(): number { return this.details.target_humidity || 0; } /** * Get current temperature * Provides access to the current temperature reading */ get temperature(): number { return this.details.temperature || 0; } /** * Check if water tank is empty * Returns true if water tank is empty and needs to be refilled */ get waterLacks(): boolean { return this.details.water_lacks || false; } /** * Check if water tank is lifted * Returns true if water tank is lifted/removed from the device */ get waterTankLifted(): boolean { return this.details.water_tank_lifted || false; } /** * Get filter life percentage * Returns the percentage of filter life remaining */ get filterLifePercentage(): number { return this.details.filter_life_percentage || 0; } /** * Check if drying mode is enabled * Returns true if drying mode is enabled */ get dryingModeEnabled(): boolean { const enabled = this.details.drying_mode?.autoDryingSwitch; return enabled === 1; } /** * Get drying mode state * Returns 'on' if drying mode is active, 'off' if inactive, null if unknown */ get dryingModeState(): string | null { const state = this.details.drying_mode?.dryingState; if (state === 1) return 'on'; if (state === 2) return 'off'; return null; } /** * Get drying mode level * Returns 'low' if level is 1, 'high' if level is 2, null if unknown */ get dryingModeLevel(): string | null { const level = this.details.drying_mode?.dryingLevel; if (level === 1) return 'low'; if (level === 2) return 'high'; return null; } /** * Get drying mode seconds remaining * Returns the number of seconds remaining in drying mode */ get dryingModeSecondsRemaining(): number { return this.details.drying_mode?.dryingRemain || 0; } }