UNPKG

homebridge-levoit-humidifiers

Version:
543 lines 23.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Mode = void 0; const async_lock_1 = __importDefault(require("async-lock")); const deviceTypes_1 = __importStar(require("./deviceTypes")); const VeSync_1 = require("./VeSync"); var Mode; (function (Mode) { Mode["Manual"] = "manual"; Mode["Sleep"] = "sleep"; Mode["Auto"] = "auto"; Mode["AutoPro"] = "autoPro"; Mode["Humidity"] = "humidity"; })(Mode || (exports.Mode = Mode = {})); /** * VeSyncFan represents a single Levoit humidifier device. * Manages device state, API communication, and provides methods to control the device. */ class VeSyncFan { /** * Resets all device state values to their "off" state. * Used when device is turned off or becomes unreachable. */ resetStateToOff() { this._isOn = false; this._humidityLevel = 0; this._targetHumidity = 0; this._displayOn = false; this._mistLevel = 0; this._warmLevel = 0; this._brightnessLevel = 0; this._lightOn = 'off'; } get humidityLevel() { return this._humidityLevel; } get targetHumidity() { return this._targetHumidity; } get displayOn() { return this._displayOn; } get brightnessLevel() { return this._brightnessLevel; } get mistLevel() { return this._mistLevel; } get warmLevel() { return this._warmLevel; } get warmEnabled() { return this._warmEnabled; } get lightOn() { return this._lightOn; } get mode() { return this._mode; } get targetReached() { return this._targetReached; } get isOn() { return this._isOn; } constructor(client, name, _mode, _isOn, _mistLevel, _warmLevel, _warmEnabled, _brightnessLevel, _humidityLevel, _targetHumidity, _targetReached, _lightOn, _lightSpeed, _red, _blue, _green, _colorMode, _colorSliderLocation, configModule, cid, region, model, mac, uuid) { this.client = client; this.name = name; this._mode = _mode; this._isOn = _isOn; this._mistLevel = _mistLevel; this._warmLevel = _warmLevel; this._warmEnabled = _warmEnabled; this._brightnessLevel = _brightnessLevel; this._humidityLevel = _humidityLevel; this._targetHumidity = _targetHumidity; this._targetReached = _targetReached; this._lightOn = _lightOn; this._lightSpeed = _lightSpeed; this._red = _red; this._blue = _blue; this._green = _green; this._colorMode = _colorMode; this._colorSliderLocation = _colorSliderLocation; this.configModule = configModule; this.cid = cid; this.region = region; this.model = model; this.mac = mac; this.uuid = uuid; this.lock = new async_lock_1.default(); /** * Timestamp of the last successful device info update. * Used to implement a 5-second cache to prevent excessive API calls. */ this.lastCheck = 0; this._displayOn = true; this.manufacturer = 'Levoit'; /** * Stores the last non-zero target humidity to restore when device is turned back on. * This preserves user preferences across power cycles. */ this._lastTargetHumidity = 0; this.deviceType = deviceTypes_1.default.find(({ isValid }) => isValid(this.model)); } /** * Sets the device power state (on/off). * When turning off, resets related state values to 0. * When turning on, restores the last known target humidity from memory. * * @param power - true to turn on, false to turn off * @returns true if successful, false otherwise */ async setPower(power) { this.client.log.info('Setting Power to ' + power); let switchJson; if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { switchJson = { powerSwitch: power ? 1 : 0, id: 0, }; } else { switchJson = { enabled: power, id: 0, }; } const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.SWITCH, switchJson); if (success) { this._isOn = power; if (!this._isOn) { // When turning off, save current target and reset all state to match device behavior if (this._targetHumidity > 0) { this._lastTargetHumidity = this._targetHumidity; } this._humidityLevel = 0; this._targetHumidity = 0; this._mistLevel = 0; this._warmLevel = 0; this._warmEnabled = false; this._brightnessLevel = 0; this._lightOn = 'off'; this._displayOn = false; // Note: mode is not reset as the device retains its last mode when powered back on } else { // When turning on, restore last known target from memory // Background polling will sync with device's actual value within 30s if (this._targetHumidity === 0 && this._lastTargetHumidity > 0) { this._targetHumidity = this._lastTargetHumidity; } else if (this._targetHumidity === 0) { // Fallback default if no previous value exists this._targetHumidity = 55; } } } else { this.client.log.error('Failed to setPower due to unreachable device.'); if (this.client.config.options.showOffWhenDisconnected) { this.resetStateToOff(); } else { return false; } } return success; } /** * Sets the target humidity percentage for Auto/Humidity mode. * Handles different JSON field names for new vs old device formats. * * @param level - Target humidity percentage (device-specific range, typically 30-80% or 40-80%) * @returns true if successful, false otherwise */ async setTargetHumidity(level) { this.client.log.info('Setting Target Humidity to ' + level); // Oasis 1000 uses camelcase instead of snakecase let humidityJson; if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { humidityJson = { targetHumidity: level, id: 0, }; } else { humidityJson = { target_humidity: level, id: 0, }; } const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.HUMIDITY, humidityJson); if (success) { this._targetHumidity = level; } return success; } /** * Changes the device operating mode. * Automatically maps Auto mode to the appropriate mode for the device: * - LV600S models use "Humidity" mode instead of "Auto" * - Models with AutoPro support use "AutoPro" mode instead of "Auto" * Skips API call if already in the requested mode. * * @param mode - The mode to switch to * @returns true if successful, false otherwise */ async changeMode(mode) { // LV600s models use "Humidity" mode instead of "Auto" if (this.model.includes(deviceTypes_1.DevicePrefix.LV600S) && mode == Mode.Auto) { mode = Mode.Humidity; } // Some models use "AutoPro" mode instead of "Auto" if (this.deviceType.hasAutoProMode && mode == Mode.Auto) { mode = Mode.AutoPro; } let success; // Oasis 1000 uses camelcase instead of snakecase let modeJson; if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { modeJson = { workMode: mode.toString(), }; } else { modeJson = { mode: mode.toString(), }; } // Don't change the mode if we are already in that mode if (this._mode == mode) { success = true; } else { this.client.log.info('Changing Mode to ' + mode); success = await this.client.sendCommand(this, VeSync_1.BypassMethod.MODE, modeJson); } if (success) { this._mode = mode; } return success; } /** * Sets the night light brightness level. * For non-RGB devices only. RGB devices should use setLightStatus(). * * @param brightness - Brightness level (0-100) * @returns true if successful, false otherwise */ async setBrightness(brightness) { this.client.log.info('Setting Night Light to ' + brightness); const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.NIGHT_LIGHT_BRIGHTNESS, { night_light_brightness: brightness, }); if (success) { this._brightnessLevel = brightness; } return success; } /** * Sets the device display screen state (on/off). * Handles different JSON field names for new vs old device formats. * * @param power - true to turn display on, false to turn off * @returns true if successful, false otherwise */ async setDisplay(power) { this.client.log.info('Setting Display to ' + power); // Oasis 1000 uses camelcase instead of snakecase let displayJson; if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { displayJson = { screenSwitch: power ? 1 : 0, id: 0, }; } else { displayJson = { state: power, id: 0, }; } const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.DISPLAY, displayJson); if (success) { this._displayOn = power; } return success; } /** * Changes the cool mist level. * Validates the level is within device limits (1 to mistLevels). * Handles different JSON field names for new vs old device formats. * * @param mistLevel - Mist level (1 to device-specific maximum, typically 9) * @returns true if successful, false if level is out of range */ async changeMistLevel(mistLevel) { if (mistLevel > this.deviceType.mistLevels || mistLevel < 1) { return false; } this.client.log.info('Setting Mist Level to ' + mistLevel); // New models use different JSON keys let mistJson; const method = VeSync_1.BypassMethod.MIST_LEVEL; if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { mistJson = { virtualLevel: mistLevel, levelType: 'mist', id: 0, }; } else { mistJson = { level: mistLevel, type: 'mist', id: 0, }; } const success = await this.client.sendCommand(this, method, mistJson); if (success) { this._mistLevel = mistLevel; } return success; } /** * Changes the warm mist level. * Only available on devices with warm mist capability. * Validates the level is within device limits (0 to warmMistLevels). * Updates warmEnabled state based on level (0 = disabled, >0 = enabled). * * @param warmMistLevel - Warm mist level (0 to device-specific maximum, typically 3) * @returns true if successful, false if device doesn't support warm mist or level is out of range */ async changeWarmMistLevel(warmMistLevel) { if (!this.deviceType.warmMistLevels) { this.client.log.error('Error: Attempted to set warm level on device without warmMistLevels field.'); return false; } if (warmMistLevel > this.deviceType.warmMistLevels || warmMistLevel < 0) { return false; } this.client.log.info('Setting Warm Level to ' + warmMistLevel); const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.LEVEL, { level: warmMistLevel, type: 'warm', id: 0, }); if (success) { this._warmLevel = warmMistLevel; if (this._warmLevel == 0) { this._warmEnabled = false; } else { this._warmEnabled = true; } } return success; } /** * Sets the RGB night light status and brightness. * Only for RGB-capable devices. Calculates RGB color values proportionally * when brightness changes to maintain color appearance. * * @param action - Light action: 'on' or 'off' * @param brightness - Brightness level (0-100) * @returns true if successful, false otherwise */ async setLightStatus(action, brightness) { // Get the current RGB values and brightness % const red = this._red; const green = this._green; const blue = this._blue; const currentBrightness = this._brightnessLevel; let newRed; let newGreen; let newBlue; // If we're changing brightness, calculate the RGB values to adjust to if (brightness !== this._brightnessLevel) { newRed = Math.round(red * (brightness / currentBrightness)); newGreen = Math.round(green * (brightness / currentBrightness)); newBlue = Math.round(blue * (brightness / currentBrightness)); } const lightJson = { action: action, speed: this._lightSpeed, red: newRed || this._red, green: newGreen || this._green, blue: newBlue || this._blue, brightness: brightness, colorMode: this._colorMode, colorSliderLocation: this._colorSliderLocation, }; this.client.log.debug('Setting Night Light Status to ' + JSON.stringify(lightJson)); const success = await this.client.sendCommand(this, VeSync_1.BypassMethod.LIGHT_STATUS, lightJson); if (success) { this._brightnessLevel = brightness; this._blue = newBlue || this._blue; this._green = newGreen || this._green; this._red = newRed || this._red; this._lightOn = action; } return success; } /** * Updates device state from the VeSync API. * Implements a 15-second cache to prevent excessive API calls. * This cache works in conjunction with background polling (30-second interval) * to ensure fresh data while minimizing API load and respecting quota limits. * * Thread-safe: Uses AsyncLock to prevent concurrent updates. * * @throws Error if device is unreachable and showOffWhenDisconnected is false */ async updateInfo() { return this.lock.acquire('update-info', async () => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9; try { // 15-second cache prevents excessive API calls // Background polling (30s) ensures cache is refreshed regularly // while respecting VeSync API quota limits if (Date.now() - this.lastCheck < 15 * 1000) { return; } const data = await this.client.getDeviceInfo(this); this.lastCheck = Date.now(); const deviceResult = (_a = data === null || data === void 0 ? void 0 : data.result) === null || _a === void 0 ? void 0 : _a.result; if (!deviceResult && ((_b = this.client.config.options) === null || _b === void 0 ? void 0 : _b.showOffWhenDisconnected)) { this.resetStateToOff(); return; } else if (!deviceResult) { return; } const result = deviceResult; this._humidityLevel = (_c = result.humidity) !== null && _c !== void 0 ? _c : 0; // Fields are different on newer models if ((0, deviceTypes_1.isNewFormatDevice)(this.model)) { this._targetHumidity = (_d = result.targetHumidity) !== null && _d !== void 0 ? _d : 0; this._displayOn = (_e = result.screenSwitch) !== null && _e !== void 0 ? _e : false; this._mode = (_f = result.workMode) !== null && _f !== void 0 ? _f : Mode.Auto; this._isOn = result.powerSwitch === 1; this._targetReached = (_g = result.autoStopState) !== null && _g !== void 0 ? _g : false; this._mistLevel = (_h = result.virtualLevel) !== null && _h !== void 0 ? _h : 0; } else { this._targetHumidity = (_k = (_j = result.configuration) === null || _j === void 0 ? void 0 : _j.auto_target_humidity) !== null && _k !== void 0 ? _k : 0; this._displayOn = (_l = result.display) !== null && _l !== void 0 ? _l : false; this._mode = (_m = result.mode) !== null && _m !== void 0 ? _m : Mode.Auto; this._isOn = (_o = result.enabled) !== null && _o !== void 0 ? _o : false; this._targetReached = (_p = result.automatic_stop_reach_target) !== null && _p !== void 0 ? _p : false; this._mistLevel = (_q = result.mist_virtual_level) !== null && _q !== void 0 ? _q : 0; } this._warmLevel = (_r = result.warm_level) !== null && _r !== void 0 ? _r : 0; this._warmEnabled = (_s = result.warm_enabled) !== null && _s !== void 0 ? _s : false; this._brightnessLevel = (_v = ((_t = result.night_light_brightness) !== null && _t !== void 0 ? _t : (_u = result.rgbNightLight) === null || _u === void 0 ? void 0 : _u.brightness)) !== null && _v !== void 0 ? _v : 0; // RGB Light Devices Only: this._lightOn = (_x = (_w = result.rgbNightLight) === null || _w === void 0 ? void 0 : _w.action) !== null && _x !== void 0 ? _x : 'off'; this._blue = (_z = (_y = result.rgbNightLight) === null || _y === void 0 ? void 0 : _y.blue) !== null && _z !== void 0 ? _z : 0; this._green = (_1 = (_0 = result.rgbNightLight) === null || _0 === void 0 ? void 0 : _0.green) !== null && _1 !== void 0 ? _1 : 0; this._red = (_3 = (_2 = result.rgbNightLight) === null || _2 === void 0 ? void 0 : _2.red) !== null && _3 !== void 0 ? _3 : 0; this._colorMode = (_5 = (_4 = result.rgbNightLight) === null || _4 === void 0 ? void 0 : _4.colorMode) !== null && _5 !== void 0 ? _5 : ''; this._lightSpeed = (_7 = (_6 = result.rgbNightLight) === null || _6 === void 0 ? void 0 : _6.speed) !== null && _7 !== void 0 ? _7 : 0; this._colorSliderLocation = (_9 = (_8 = result.rgbNightLight) === null || _8 === void 0 ? void 0 : _8.colorSliderLocation) !== null && _9 !== void 0 ? _9 : 0; if (result.rgbNightLight) { const lightJson = { action: this._lightOn, speed: this._lightSpeed, green: this._green, blue: this._blue, red: this._red, brightness: this._brightnessLevel, colorMode: this._colorMode, colorSliderLocation: this._colorSliderLocation, }; this.client.debugMode.debug('[GET LIGHT JSON]', JSON.stringify(lightJson)); } } catch (err) { const message = err instanceof Error ? err.message : String(err); this.client.log.error('Failed to updateInfo due to unreachable device: ' + message); if (this.client.config.options.showOffWhenDisconnected) { this.resetStateToOff(); } else { throw new Error('Device was unreachable. Ensure it is plugged in and connected to WiFi.'); } } }); } } /** * Factory method to create a VeSyncFan instance from VeSync API response data. * Used during device discovery to instantiate devices from the device list. * * @param client - The VeSync client instance for API communication * @returns A function that takes device data and returns a VeSyncFan instance */ VeSyncFan.fromResponse = (client) => ({ deviceName, mode, deviceStatus, mistLevel, warmLevel, warmEnabled, brightnessLevel, humidity, targetHumidity, targetReached, lightOn, lightSpeed, red, blue, green, colorMode, colorSliderLocation, configModule, cid, deviceRegion, deviceType, macID, uuid, }) => new VeSyncFan(client, deviceName, mode, deviceStatus, mistLevel, warmLevel, warmEnabled, brightnessLevel, humidity, targetHumidity, targetReached, lightOn, lightSpeed, red, blue, green, colorMode, colorSliderLocation, configModule, cid, deviceRegion, deviceType, macID, uuid); exports.default = VeSyncFan; //# sourceMappingURL=VeSyncFan.js.map