UNPKG

tsvesync

Version:

A TypeScript library for interacting with VeSync smart home devices

598 lines (535 loc) 21.1 kB
import { VeSyncFan } from '../vesyncFan'; import { VeSync } from '../vesync'; import { Helpers } from '../helpers'; import { logger } from '../logger'; /** * VeSync Air Purifier 131 Series (LV-PUR131S, LV-RH131S) * This class implements the specific API for LV series devices */ export class VeSyncAir131 extends VeSyncFan { protected readonly modes = ['auto', 'manual', 'sleep'] as const; protected readonly displayModes = ['on', 'off'] as const; protected readonly childLockModes = ['on', 'off'] as const; private lastKnownSpeed: number = 1; constructor(details: Record<string, any>, manager: VeSync) { super(details, manager); // Initialize last known speed from device details if available if (details.speed && details.speed > 0) { this.lastKnownSpeed = details.speed; } else if (details.level && details.level > 0) { this.lastKnownSpeed = details.level; } logger.debug(`Initialized VeSyncAir131 device: ${this.deviceName}, lastKnownSpeed: ${this.lastKnownSpeed}`); } /** * Get device details */ async getDetails(): Promise<Boolean> { logger.debug(`Getting details for device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/deviceDetail', 'post', { acceptLanguage: 'en', accountID: this.manager.accountId!, appVersion: '2.8.6', method: 'devicedetail', mobileId: '1234567890123456', phoneBrand: 'SM N9005', phoneOS: 'Android', timeZone: this.manager.timeZone!, token: this.manager.token!, traceId: new Date().getTime().toString(), uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'getDetails'); if (success && response) { // Check if data is in response.result or directly in response const data = response.result || response; this.deviceStatus = data.deviceStatus === 'on' ? 'on' : 'off'; this.details = { mode: data.mode || 'manual', // Default to manual if not specified speed: data.level || 0, filterLife: data.filterLife || 0, screenStatus: data.screenStatus || 'off', childLock: data.childLock || false, airQuality: data.airQuality || 'unknown', active_time: data.activeTime || 0 }; // Store last known speed when device is on and has a valid speed if (this.deviceStatus === 'on' && this.details.speed && this.details.speed > 0) { this.lastKnownSpeed = this.details.speed; } logger.debug(`${this.deviceName}: Updated details - mode: ${this.details.mode}, speed: ${this.details.speed}, status: ${this.deviceStatus}`); return true; } // Log additional details on failure if (!success && response?.code) { logger.error(`${this.deviceName}: getDetails failed with code ${response.code} - ${response.msg || 'Unknown error'}`); if (response.traceId) { logger.error(`${this.deviceName}: TraceId for debugging: ${response.traceId}`); } } return false; } /** * Turn device on */ async turnOn(): Promise<boolean> { // Check if device is already on if (this.deviceStatus === 'on') { logger.debug(`Device ${this.deviceName} is already on`); return false; // Return false to match pyvesync behavior } logger.info(`Turning on device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/deviceStatus', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, status: 'on', timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'turnOn'); if (success) { // Update device status immediately this.deviceStatus = 'on'; // Fetch device details to get current speed and other settings logger.debug(`${this.deviceName}: Fetching device details after turning on`); let detailsSuccess = await this.getDetails(); // If getDetails failed or returned 0 speed, retry once after a short delay if (!detailsSuccess || this.details.speed === 0) { logger.debug(`${this.deviceName}: First getDetails attempt failed or returned 0 speed, retrying in 2 seconds...`); await new Promise(resolve => setTimeout(resolve, 2000)); detailsSuccess = await this.getDetails(); } // If still no speed, use last known speed if (!detailsSuccess || this.details.speed === 0) { logger.debug(`${this.deviceName}: getDetails still failed or returned 0 speed after retry, using last known speed: ${this.lastKnownSpeed}`); this.details.speed = this.lastKnownSpeed; } } else { logger.error(`Failed to turn on device: ${this.deviceName}`); } return success; } /** * Turn device off */ async turnOff(): Promise<boolean> { // Check if device is already off if (this.deviceStatus !== 'on') { logger.debug(`Device ${this.deviceName} is already off`); return true; } logger.info(`Turning off device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/deviceStatus', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, status: 'off', timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'turnOff'); if (success) { // Update device status immediately this.deviceStatus = 'off'; } else { logger.error(`Failed to turn off device: ${this.deviceName}`); } return success; } /** * Change fan speed */ async changeFanSpeed(speed: number): Promise<boolean> { logger.info(`Changing fan speed to ${speed} for device: ${this.deviceName}`); // Check if device is in manual mode if (this.details.mode !== 'manual') { logger.error(`${this.deviceName}: Cannot change fan speed - device is in ${this.details.mode} mode, manual mode required`); return false; } // Validate speed for LV series (1-3) if (speed < 1 || speed > 3) { logger.error(`Invalid fan speed: ${speed}. Must be between 1 and 3 for device: ${this.deviceName}`); return false; } const [response, status] = await this.callApi( '/131airPurifier/v1/device/updateSpeed', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, level: speed, timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'changeFanSpeed'); if (success) { this.details.speed = speed; this.lastKnownSpeed = speed; // Update last known speed return true; } else { logger.error(`Failed to change fan speed to ${speed} for device: ${this.deviceName}`); return false; } } /** * 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 payload: Record<string, any> = { acceptLanguage: 'en', accountID: this.manager.accountId!, mode: mode, timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }; // For manual mode, we need to set the level if (mode === 'manual') { payload.level = this.details.speed || 1; } const [response, status] = await this.callApi( '/131airPurifier/v1/device/updateMode', 'put', payload, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'setMode'); if (success) { this.details.mode = mode; return true; } else { logger.error(`Failed to set mode to ${mode} for device: ${this.deviceName}`); return false; } } /** * Set display status */ async setDisplay(enabled: boolean): Promise<boolean> { if (!this.hasFeature('display')) { const error = 'Display control not supported'; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } // Check if device is in sleep mode - display control may not work in sleep mode if (this.details.mode === 'sleep') { logger.warn(`Device ${this.deviceName} is in sleep mode, display control may not work`); } logger.debug(`Setting display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/updateScreen', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, status: enabled ? 'on' : 'off', timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'setDisplay'); if (success) { this.details.screenStatus = enabled ? 'on' : 'off'; return true; } else { logger.error(`Failed to set display to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); return false; } } /** * Set child lock */ async setChildLock(enabled: boolean): Promise<boolean> { if (!this.hasFeature('child_lock')) { const error = 'Child lock not supported'; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } // Check if device is in sleep mode - child lock may not work in sleep mode if (this.details.mode === 'sleep') { logger.warn(`Device ${this.deviceName} is in sleep mode, child lock control may not work`); } logger.debug(`Setting child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/updateChildLock', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, status: enabled ? 'on' : 'off', timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'setChildLock'); if (success) { this.details.childLock = enabled; return true; } else { // Check for error code 11000000 (feature not supported) if (response?.code === 11000000) { logger.warn(`Child lock control not supported via API for device: ${this.deviceName}`); return false; } logger.error(`Failed to set child lock to ${enabled ? 'on' : 'off'} for device: ${this.deviceName}`); return false; } } /** * Set timer */ async setTimer(hours: number): Promise<boolean> { if (!this.hasFeature('timer')) { const error = 'Timer not supported'; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } logger.debug(`Setting timer to ${hours} hours for device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/updateTimer', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, action: 'off', duration: hours, timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'setTimer'); if (success) { this.timer = { duration: hours * 3600, action: 'off' }; return true; } else { logger.error(`Failed to set timer to ${hours} hours for device: ${this.deviceName}`); return false; } } /** * Clear timer */ async clearTimer(): Promise<boolean> { if (!this.hasFeature('timer')) { const error = 'Timer not supported'; logger.error(`${error} for device: ${this.deviceName}`); throw new Error(error); } // If no timer is set, return success if (!this.timer) { logger.debug(`No timer to clear for device: ${this.deviceName}`); return true; } logger.debug(`Clearing timer for device: ${this.deviceName}`); const [response, status] = await this.callApi( '/131airPurifier/v1/device/cancelTimer', 'put', { acceptLanguage: 'en', accountID: this.manager.accountId!, timeZone: this.manager.timeZone!, token: this.manager.token!, uuid: this.uuid }, { 'accept-language': 'en', 'accountId': this.manager.accountId!, 'appVersion': '2.8.6', 'content-type': 'application/json', 'tk': this.manager.token!, 'tz': this.manager.timeZone! } ); const success = this.checkResponse([response, status], 'clearTimer'); if (success) { this.timer = null; return true; } else { logger.error(`Failed to clear timer for device: ${this.deviceName}`); return false; } } /** * Set auto mode */ async autoMode(): Promise<boolean> { logger.debug(`Setting auto mode for device: ${this.deviceName}`); const success = await this.setMode('auto'); if (!success) { logger.error(`Failed to set auto mode for device: ${this.deviceName}`); } return success; } /** * Set manual mode */ async manualMode(): Promise<boolean> { logger.debug(`Setting manual mode for device: ${this.deviceName}`); const success = await this.setMode('manual'); if (!success) { logger.error(`Failed to set manual mode for device: ${this.deviceName}`); } return success; } /** * Set sleep mode */ async sleepMode(): Promise<boolean> { logger.debug(`Setting sleep mode for device: ${this.deviceName}`); const success = await this.setMode('sleep'); if (!success) { logger.error(`Failed to set sleep mode for device: ${this.deviceName}`); } return success; } /** * Turn off display */ async turnOffDisplay(): Promise<boolean> { logger.debug(`Turning off display for device: ${this.deviceName}`); const success = await this.setDisplay(false); if (!success) { logger.error(`Failed to turn off display for device: ${this.deviceName}`); } return success; } /** * Turn on display */ async turnOnDisplay(): Promise<boolean> { logger.debug(`Turning on display for device: ${this.deviceName}`); const success = await this.setDisplay(true); if (!success) { logger.error(`Failed to turn on display for device: ${this.deviceName}`); } return success; } /** * Get active time in minutes */ get activeTime(): number { return this.details.active_time || 0; } /** * Display device info */ override display(): void { super.display(); const info = [ ['Mode: ', this.mode], ['Speed: ', this.speed], ['Filter Life: ', this.filterLife, '%'], ['Screen Status: ', this.screenStatus], ['Child Lock: ', this.childLock ? 'Enabled' : 'Disabled'], ['Air Quality: ', this.airQuality], ['Active Time: ', this.activeTime, 'minutes'] ]; for (const [key, value, unit = ''] of info) { logger.info(`${key.toString().padEnd(30, '.')} ${value}${unit}`); } } /** * Return JSON details for device */ override displayJSON(): string { const baseInfo = JSON.parse(super.displayJSON()); const details: Record<string, string> = { ...baseInfo, 'Mode': this.mode, 'Speed': this.speed.toString(), 'Filter Life': this.filterLife.toString(), 'Screen Status': this.screenStatus, 'Child Lock': this.childLock ? 'Enabled' : 'Disabled', 'Air Quality': this.airQuality, 'Active Time': this.activeTime.toString() }; return JSON.stringify(details, null, 4); } }