homebridge-levoit-humidifiers
Version:
Homebridge plugin for Levoit Humidifiers
543 lines • 23.2 kB
JavaScript
"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