tsvesync
Version:
A TypeScript library for interacting with VeSync smart home devices
565 lines (564 loc) • 25.6 kB
JavaScript
"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;