├── .vscode ├── settings.json └── launch.json ├── .tool-versions ├── .prettierrc ├── images ├── auto.png ├── logs.png ├── light.png ├── manual.png ├── sleep.png ├── display.png ├── services.png ├── services2.png └── homebridgeUI.png ├── CHANGELOG.md ├── src ├── settings.ts ├── index.ts ├── debugMode.ts ├── characteristics │ ├── TargetState.ts │ ├── Humidity.ts │ ├── DisplayState.ts │ ├── CurrentState.ts │ ├── Active.ts │ ├── AutoProState.ts │ ├── MistLevel.ts │ ├── WarmMistLevel.ts │ ├── SleepState.ts │ ├── WarmActive.ts │ ├── LightBrightness.ts │ ├── TargetHumidity.ts │ └── LightState.ts ├── platform.ts ├── api │ ├── deviceTypes.ts │ ├── VeSyncFan.ts │ └── VeSync.ts └── VeSyncAccessory.ts ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── request-to-add-a-device.md │ ├── new-device-request.md │ ├── feature-request.md │ └── bug-report.md └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── nodemon.json ├── SECURITY.md ├── tsconfig.json ├── .eslintrc ├── package.json ├── .gitignore ├── .npmignore ├── config.schema.json ├── README.md ├── LICENSE └── yarn.lock /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.19.5 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /images/auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/auto.png -------------------------------------------------------------------------------- /images/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/logs.png -------------------------------------------------------------------------------- /images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/light.png -------------------------------------------------------------------------------- /images/manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/manual.png -------------------------------------------------------------------------------- /images/sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/sleep.png -------------------------------------------------------------------------------- /images/display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/display.png -------------------------------------------------------------------------------- /images/services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/services.png -------------------------------------------------------------------------------- /images/services2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/services2.png -------------------------------------------------------------------------------- /images/homebridgeUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pschroeder89/homebridge-levoit-humidifiers/HEAD/images/homebridgeUI.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.15.0] - 2025-Sept-08 2 | 3 | ### Fixed 4 | - #89: Update login implementation for non-US users to use region 5 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export const PLUGIN_NAME = 'homebridge-levoit-humidifiers'; 2 | export const PLATFORM_NAME = 'LevoitHumidifiers'; 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pschroeder89] 4 | custom: [https://www.paypal.me/pschroeder89] 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { API } from 'homebridge'; 2 | 3 | import { PLATFORM_NAME } from './settings'; 4 | import Platform from './platform'; 5 | 6 | export = (api: API) => { 7 | api.registerPlatform(PLATFORM_NAME, Platform); 8 | }; 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # blank_issues_enabled: false 2 | # contact_links: 3 | # - name: Homebridge Discord Community 4 | # url: https://discord.gg/kqNCe2D 5 | # about: Ask your questions in the #YOUR_CHANNEL_HERE channel -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [], 7 | "exec": "tsc && DEBUG=null homebridge -I -D", 8 | "signal": "SIGTERM", 9 | "env": { 10 | "NODE_OPTIONS": "--trace-warnings" 11 | } 12 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.1.0 | :white_check_mark: | 8 | | 1.0.2 | :white_check_mark: | 9 | | < 1.0.2 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Use the issue section to report it 14 | -------------------------------------------------------------------------------- /src/debugMode.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'homebridge'; 2 | 3 | export default class DebugMode { 4 | constructor( 5 | private readonly _debugMode: boolean, 6 | private readonly log: Logger, 7 | ) {} 8 | 9 | public debug(...message: any[]): void { 10 | if (!this._debugMode) { 11 | return; 12 | } 13 | 14 | this.log.info(`[DEBUG]: ${message.join(' ')}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Debugging", 6 | "program": "node_modules/homebridge/bin/homebridge", 7 | "preLaunchTask": "npm: build", 8 | "request": "launch", 9 | "skipFiles": [ 10 | "/**" 11 | ], 12 | "type": "node", 13 | "restart": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request-to-add-a-device.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request to add a device 3 | about: Want this plugin to support your Levoit Humidifier? 4 | title: "[Device Request]" 5 | labels: '' 6 | assignees: pschroeder89 7 | 8 | --- 9 | 10 | **Model number:** 11 | 12 | **Number of mist levels:** 13 | 14 | **Number of warm mist levels:** 15 | 16 | **Auto mode?** 17 | 18 | **Sleep mode?** 19 | 20 | **Night Light?** 21 | 22 | **Any other features not listed above?** 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-device-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Device Request 3 | about: Want this plugin to support your Levoit Humidifier? 4 | title: "[Device Request]" 5 | labels: device request 6 | assignees: pschroeder89 7 | 8 | --- 9 | 10 | **Model number:** 11 | 12 | **Number of mist levels:** 13 | 14 | **Number of warm mist levels:** 15 | 16 | **Auto mode?** 17 | 18 | **Sleep mode?** 19 | 20 | **Night Light?** 21 | 22 | **Any other features not listed above?** 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", // ~node10 4 | "module": "commonjs", 5 | "lib": ["es2015", "es2016", "es2017", "es2018"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "noImplicitAny": false 14 | }, 15 | "include": ["src/"], 16 | "exclude": ["**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /src/characteristics/TargetState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicValue, 4 | Nullable, 5 | } from 'homebridge'; 6 | 7 | import { AccessoryThisType } from '../VeSyncAccessory'; 8 | 9 | const characteristic: { 10 | get: CharacteristicGetHandler; 11 | } & AccessoryThisType = { 12 | get: async function (): Promise> { 13 | const { HUMIDIFIER } = 14 | this.platform.Characteristic.TargetHumidifierDehumidifierState; 15 | 16 | return HUMIDIFIER; 17 | }, 18 | }; 19 | 20 | export default characteristic; 21 | -------------------------------------------------------------------------------- /src/characteristics/Humidity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicValue, 4 | Nullable, 5 | } from 'homebridge'; 6 | 7 | import { AccessoryThisType } from '../VeSyncAccessory'; 8 | 9 | const characteristic: { 10 | get: CharacteristicGetHandler; 11 | } & AccessoryThisType = { 12 | get: async function (): Promise> { 13 | await this.device.updateInfo(); 14 | if (typeof this.device.humidityLevel !== 'number') { 15 | return 0; 16 | } 17 | return this.device.humidityLevel; 18 | }, 19 | }; 20 | 21 | export default characteristic; 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | # the Node.js versions to build on 12 | node-version: [14.x, 16.x, 18.x, 20.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Lint the project 26 | run: npm run lint 27 | 28 | - name: Build the project 29 | run: npm run build 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /src/characteristics/DisplayState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | set: CharacteristicSetHandler; 13 | } & AccessoryThisType = { 14 | get: async function (): Promise> { 15 | await this.device.updateInfo(); 16 | if (this.device.displayOn) { 17 | return true; 18 | } else { 19 | return false; 20 | } 21 | }, 22 | set: async function (value: CharacteristicValue) { 23 | const boolValue = value == 1; 24 | await this.device.setDisplay(boolValue); 25 | }, 26 | }; 27 | 28 | export default characteristic; 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe:** 11 | 12 | 13 | **Describe the solution you'd like:** 14 | 15 | 16 | **Describe alternatives you've considered:** 17 | 18 | 19 | **Additional context:** 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/characteristics/CurrentState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicValue, 4 | Nullable, 5 | } from 'homebridge'; 6 | 7 | import { AccessoryThisType } from '../VeSyncAccessory'; 8 | import { Mode } from '../api/VeSyncFan'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | } & AccessoryThisType = { 13 | get: async function (): Promise> { 14 | await this.device.updateInfo(); 15 | 16 | const { HUMIDIFYING, IDLE } = 17 | this.platform.Characteristic.CurrentHumidifierDehumidifierState; 18 | 19 | if ( 20 | this.device.targetReached || 21 | !this.device.isOn || 22 | this.device.mode == Mode.Manual 23 | ) { 24 | return IDLE; 25 | } else { 26 | return HUMIDIFYING; 27 | } 28 | }, 29 | }; 30 | 31 | export default characteristic; 32 | -------------------------------------------------------------------------------- /src/characteristics/Active.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | set: CharacteristicSetHandler; 13 | } & AccessoryThisType = { 14 | get: async function (): Promise> { 15 | await this.device.updateInfo(); 16 | 17 | if (this.device.isOn) { 18 | return true; 19 | } else { 20 | return false; 21 | } 22 | }, 23 | set: async function (value: CharacteristicValue) { 24 | const boolValue = value == 1; 25 | 26 | if (boolValue !== this.device.isOn) { 27 | await this.device.setPower(boolValue); 28 | } 29 | }, 30 | }; 31 | 32 | export default characteristic; 33 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "dist" 5 | ], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": [ 14 | "./tsconfig.json" 15 | ] 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint", 19 | "prettier", 20 | "unused-imports" 21 | ], 22 | "rules": { 23 | "@typescript-eslint/no-non-null-assertion": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "spaced-comment": [ 26 | "error", 27 | "always" 28 | ], 29 | "@typescript-eslint/no-unused-vars": "error", 30 | "prettier/prettier": "error" 31 | } 32 | } -------------------------------------------------------------------------------- /src/characteristics/AutoProState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | import { Mode } from '../api/VeSyncFan'; 8 | 9 | import { AccessoryThisType } from '../VeSyncAccessory'; 10 | 11 | const characteristic: { 12 | get: CharacteristicGetHandler; 13 | set: CharacteristicSetHandler; 14 | } & AccessoryThisType = { 15 | get: async function (): Promise> { 16 | await this.device.updateInfo(); 17 | 18 | // If device is off, set the mode to null so the switch displays Off 19 | if (!this.device.isOn) { 20 | return false; 21 | } 22 | 23 | return this.device.mode === Mode.AutoPro; 24 | }, 25 | set: async function (value: CharacteristicValue) { 26 | switch (value) { 27 | case true: 28 | await this.device.changeMode(Mode.AutoPro); 29 | break; 30 | case false: 31 | await this.device.changeMode(Mode.Manual); 32 | break; 33 | } 34 | }, 35 | }; 36 | 37 | export default characteristic; 38 | -------------------------------------------------------------------------------- /src/characteristics/MistLevel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | import { Mode } from '../api/VeSyncFan'; 10 | 11 | const characteristic: { 12 | get: CharacteristicGetHandler; 13 | set: CharacteristicSetHandler; 14 | } & AccessoryThisType = { 15 | get: async function (): Promise> { 16 | await this.device.updateInfo(); 17 | return this.device.isOn ? this.device.mistLevel : 0; 18 | }, 19 | 20 | set: async function (value: CharacteristicValue) { 21 | if (value == 0) { 22 | await this.device.setPower(false); 23 | } else { 24 | if ( 25 | !this.device.deviceType.hasWarmMode || 26 | this.device.model.includes('LUH-O601S') // This model has a Warm Mode, but it doesn't support changing mist levels in sleep mode 27 | ) { 28 | await this.device.changeMode(Mode.Manual); 29 | } 30 | await this.device.changeMistLevel(Number(value)); 31 | } 32 | }, 33 | }; 34 | 35 | export default characteristic; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe The Bug:** 13 | 14 | 15 | **To Reproduce:** 16 | 17 | 18 | **Expected behavior:** 19 | 20 | 21 | **Logs:** 22 | 23 | ``` 24 | Show the Homebridge logs here, remove any sensitive information. 25 | ``` 26 | 27 | **Plugin Config:** 28 | 29 | ```json 30 | Show your Homebridge config.json here, remove any sensitive information. 31 | ``` 32 | 33 | **Screenshots:** 34 | 35 | 36 | **Environment:** 37 | 38 | * **Plugin Version**: 39 | * **Homebridge Version**: 40 | * **Node.js Version**: 41 | * **NPM Version**: 42 | * **Operating System**: 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/characteristics/WarmMistLevel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | import VeSyncFan from '../api/VeSyncFan'; 8 | 9 | import { AccessoryThisType } from '../VeSyncAccessory'; 10 | 11 | const calculateWarmMistLevel = (device: VeSyncFan) => { 12 | const currentMistLevel = device.warmLevel; 13 | return device.isOn ? currentMistLevel : 0; 14 | }; 15 | 16 | const characteristic: { 17 | get: CharacteristicGetHandler; 18 | set: CharacteristicSetHandler; 19 | } & AccessoryThisType = { 20 | get: async function (): Promise> { 21 | await this.device.updateInfo(); 22 | return calculateWarmMistLevel(this.device); 23 | }, 24 | 25 | set: async function (value: CharacteristicValue) { 26 | if (!this.device.warmEnabled && value > 0) { 27 | // if from Off state and level is greater than 0, return immediately. 28 | return; 29 | } 30 | if (this.device.warmEnabled && value == 0) { 31 | // if from On state and level is less than 0, return immediately 32 | return; 33 | } 34 | 35 | await this.device.changeWarmMistLevel(Number(value)); 36 | }, 37 | }; 38 | 39 | export default characteristic; 40 | -------------------------------------------------------------------------------- /src/characteristics/SleepState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | import { Mode } from '../api/VeSyncFan'; 8 | 9 | import { AccessoryThisType } from '../VeSyncAccessory'; 10 | import { DeviceName } from '../api/deviceTypes'; 11 | 12 | const characteristic: { 13 | get: CharacteristicGetHandler; 14 | set: CharacteristicSetHandler; 15 | } & AccessoryThisType = { 16 | get: async function (): Promise> { 17 | await this.device.updateInfo(); 18 | 19 | // If device is off, set the mode to null so the switch displays Off 20 | if (!this.device.isOn) { 21 | return false; 22 | } 23 | 24 | return this.device.mode === Mode.Sleep; 25 | }, 26 | set: async function (value: CharacteristicValue) { 27 | switch (value) { 28 | case true: 29 | await this.device.changeMode(Mode.Sleep); 30 | break; 31 | case false: 32 | // LEH_S601S_WUS has an auto and humidity mode, we want to revert to humidity for that model since Auto has its own switch 33 | if ( 34 | [DeviceName.LEH_S601S_WUS, DeviceName.LEH_S601S_WUSR].includes(this.device.model as DeviceName) 35 | ) { 36 | await this.device.changeMode(Mode.Humidity); 37 | break; 38 | } else { 39 | await this.device.changeMode(Mode.Auto); 40 | break; 41 | } 42 | } 43 | }, 44 | }; 45 | 46 | export default characteristic; 47 | -------------------------------------------------------------------------------- /src/characteristics/WarmActive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | set: CharacteristicSetHandler; 13 | } & AccessoryThisType = { 14 | get: async function (): Promise> { 15 | await this.device.updateInfo(); 16 | return this.device.isOn && this.device.warmEnabled; 17 | }, 18 | set: async function (value: CharacteristicValue) { 19 | const boolValue = value == 1; 20 | if (!boolValue) { 21 | await this.device.changeWarmMistLevel(0); 22 | await this.device.updateInfo(); 23 | } else if (!this.device.warmEnabled && this.device.warmLevel == 0) { 24 | /* 25 | If turning on Warm Mode from Off state, we set it to the highest warmMistLevel value. 26 | This is because we can't determine the selected slider number from the WarmMistLevel characteristic. 27 | This appears like a bug (from Off, set to lowest level, but it will set to highest level instead), 28 | but there's not a good way to handle this since VeSync doesn't have an on/off for Warm Mode, just 29 | level selection. 30 | */ 31 | await this.device.changeWarmMistLevel( 32 | Number(this.device.deviceType.warmMistLevels), 33 | ); 34 | await this.device.updateInfo(); 35 | } 36 | }, 37 | }; 38 | export default characteristic; 39 | -------------------------------------------------------------------------------- /src/characteristics/LightBrightness.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | set: CharacteristicSetHandler; 13 | } & AccessoryThisType = { 14 | get: async function (): Promise> { 15 | await this.device.updateInfo(); 16 | return this.device.brightnessLevel; 17 | }, 18 | set: async function (value: CharacteristicValue) { 19 | // Convert value to number 20 | value = Number(value); 21 | 22 | if (this.device.brightnessLevel > 0 && value > 0) { 23 | // If light is on, and we are applying a non-zero value, change brightness to that level. 24 | // Otherwise, LightState will handle on / off switching. 25 | 26 | // We allow 39 as a value so 40 doesn't turn off the device. 27 | // So never set the device to 39, since that's not actually supported 28 | if (value === 39) { 29 | value + 1; 30 | } 31 | let action: string; 32 | if (value >= 40) { 33 | action = 'on'; 34 | } else { 35 | action = 'off'; 36 | } 37 | // Handle Color Mode (RGB) devices 38 | if (this.device.deviceType.hasColorMode) { 39 | await this.device.setLightStatus(action, Number(value)); 40 | } else { 41 | // Other devices 42 | await this.device.setBrightness(Number(value)); 43 | } 44 | } 45 | }, 46 | }; 47 | 48 | export default characteristic; 49 | -------------------------------------------------------------------------------- /src/characteristics/TargetHumidity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | import { Mode } from '../api/VeSyncFan'; 10 | import { DeviceName, NewDevices } from '../api/deviceTypes'; 11 | 12 | const characteristic: { 13 | get: CharacteristicGetHandler; 14 | set: CharacteristicSetHandler; 15 | } & AccessoryThisType = { 16 | get: async function (): Promise> { 17 | await this.device.updateInfo(); 18 | if (this.device.isOn) { 19 | return this.device.targetHumidity; 20 | } else { 21 | return 0; 22 | } 23 | }, 24 | set: async function (humidity: CharacteristicValue) { 25 | if (!this.device.isOn) { 26 | await this.device.setPower(true); 27 | } 28 | if (NewDevices.includes(this.device.name as DeviceName)) { 29 | await this.device.changeMode(Mode.Humidity); 30 | } else if ( 31 | this.device.mode == Mode.Manual || 32 | (this.device.deviceType.hasWarmMode && this.device.mode == Mode.Sleep) 33 | ) { 34 | await this.device.changeMode(Mode.Auto); 35 | } 36 | switch (true) { 37 | case Number(humidity) < this.device.deviceType.minHumidityLevel: 38 | humidity = this.device.deviceType.minHumidityLevel; 39 | break; 40 | case Number(humidity) > this.device.deviceType.maxHumidityLevel: 41 | humidity = this.device.deviceType.maxHumidityLevel; 42 | break; 43 | } 44 | await this.device.setTargetHumidity(Number(humidity)); 45 | }, 46 | }; 47 | 48 | export default characteristic; 49 | -------------------------------------------------------------------------------- /src/characteristics/LightState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacteristicGetHandler, 3 | CharacteristicSetHandler, 4 | CharacteristicValue, 5 | Nullable, 6 | } from 'homebridge'; 7 | 8 | import { AccessoryThisType } from '../VeSyncAccessory'; 9 | 10 | const characteristic: { 11 | get: CharacteristicGetHandler; 12 | set: CharacteristicSetHandler; 13 | } & AccessoryThisType = { 14 | get: async function (): Promise> { 15 | await this.device.updateInfo(); 16 | // If there is a lightOn attribute, that's the source of truth (for RGB models) 17 | // Otherwise, convert brightness to a bool 18 | if (this.device.lightOn) { 19 | if (this.device.lightOn === 'on') { 20 | return true; 21 | } else { 22 | return false; 23 | } 24 | } 25 | return !!this.device.brightnessLevel; 26 | }, 27 | set: async function (bool: CharacteristicValue) { 28 | let action: string; 29 | if (bool) { 30 | action = 'on'; 31 | } else { 32 | action = 'off'; 33 | } 34 | 35 | const lightOnVal = this.device.lightOn; 36 | // If light is off and we are turning it on, turn it on to 50% brightness. 37 | // Note: Turning on the device will always make brightness 50%, even if you slide to 100% when turning it on. 38 | if ( 39 | ((lightOnVal && lightOnVal == 'off') || 40 | (!lightOnVal && this.device.brightnessLevel == 0)) && 41 | bool == 1 42 | ) { 43 | // If device has color mode (RGB), set on / off and set brightness to 50 44 | if (this.device.deviceType.hasColorMode) { 45 | await this.device.setLightStatus(action, 50); 46 | } else { 47 | await this.device.setBrightness(50); 48 | } 49 | } 50 | if ( 51 | ((lightOnVal && lightOnVal == 'on') || 52 | (!lightOnVal && this.device.brightnessLevel > 0)) && 53 | bool == 0 54 | ) 55 | if (this.device.deviceType.hasColorMode) { 56 | await this.device.setLightStatus('off', 50); 57 | } else { 58 | // If light is on and we are turning it off, set to 0 59 | await this.device.setBrightness(0); 60 | } 61 | }, 62 | }; 63 | 64 | export default characteristic; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Homebridge plugin for Levoit Humidifiers", 3 | "name": "homebridge-levoit-humidifiers", 4 | "displayName": "Levoit Humidifiers", 5 | "main": "dist/index.js", 6 | "license": "Apache-2.0", 7 | "version": "1.15.0", 8 | "private": false, 9 | "bugs": { 10 | "url": "https://github.com/pschroeder89/homebridge-levoit-humidifiers/issues" 11 | }, 12 | "repository": { 13 | "url": "git://github.com/pschroeder89/homebridge-levoit-humidifiers.git", 14 | "type": "git" 15 | }, 16 | "scripts": { 17 | "remove-link": "npm -g remove homebridge-levoit-humidifiers", 18 | "prepublishOnly": "npm run lint && npm run build", 19 | "watch": "npm run build && npm link && nodemon", 20 | "lint": "eslint src/**.ts --max-warnings=0", 21 | "build": "rimraf ./dist && tsc" 22 | }, 23 | "devDependencies": { 24 | "@types/async-lock": "^1.1.3", 25 | "@types/node": "^16.11.7", 26 | "@typescript-eslint/eslint-plugin": "^5.3.1", 27 | "@typescript-eslint/parser": "^5.3.1", 28 | "eslint": "^8.56.0", 29 | "eslint-plugin-prettier": "^5.1.2", 30 | "eslint-plugin-unused-imports": "^2.0.0", 31 | "homebridge": "^1.3.5", 32 | "nodemon": "^2.0.13", 33 | "rimraf": "^3.0.2", 34 | "ts-node": "^10.3.0", 35 | "typescript": "^4.4.4" 36 | }, 37 | "engines": { 38 | "homebridge": ">=1.3.5", 39 | "node": ">=18.16.0" 40 | }, 41 | "dependencies": { 42 | "async-lock": "^1.3.0", 43 | "axios": "^0.24.0", 44 | "eslint-config-prettier": "^9.1.0", 45 | "prettier": "^3.1.1", 46 | "uuid": "^9.0.0", 47 | "yarn": "^1.22.22" 48 | }, 49 | "keywords": [ 50 | "homebridge-plugin", 51 | "humidifier", 52 | "classic 300s", 53 | "vesync", 54 | "levoit", 55 | "300s", 56 | "oasis", 57 | "lv600s", 58 | "homebridge", 59 | "classic 300s" 60 | ], 61 | "types": "./dist/index.d.ts", 62 | "homepage": "https://github.com/pschroeder89/homebridge-levoit-humidifiers#readme", 63 | "author": "Paul Schroeder", 64 | "funding": [ 65 | { 66 | "type": "github", 67 | "url": "https://github.com/sponsors/pschroeder89" 68 | }, 69 | { 70 | "type": "paypal", 71 | "url": "https://www.paypal.me/pschroeder89" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore compiled code 2 | dist 3 | 4 | # ------------- Defaults ------------- # 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | 117 | .yarn/cache 118 | .yarn/unplugged 119 | .yarn/build-state.yml 120 | .pnp.* -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '26 12 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore source code 2 | src 3 | 4 | # ------------- Defaults ------------- # 5 | 6 | # gitHub actions 7 | .github 8 | 9 | # eslint 10 | .eslintrc 11 | 12 | # typescript 13 | tsconfig.json 14 | 15 | # vscode 16 | .vscode 17 | 18 | # nodemon 19 | nodemon.json 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Microbundle cache 77 | .rpt2_cache/ 78 | .rts2_cache_cjs/ 79 | .rts2_cache_es/ 80 | .rts2_cache_umd/ 81 | 82 | # Optional REPL history 83 | .node_repl_history 84 | 85 | # Output of 'npm pack' 86 | *.tgz 87 | 88 | # Yarn Integrity file 89 | .yarn-integrity 90 | 91 | # dotenv environment variables file 92 | .env 93 | .env.test 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | .cache 97 | .parcel-cache 98 | 99 | # Next.js build output 100 | .next 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # Serverless directories 116 | .serverless/ 117 | 118 | # FuseBox cache 119 | .fusebox/ 120 | 121 | # DynamoDB Local files 122 | .dynamodb/ 123 | 124 | # TernJS port file 125 | .tern-port 126 | 127 | # Stores VSCode versions used for testing VSCode extensions 128 | .vscode-test 129 | 130 | # yarn v2 131 | 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .pnp.* -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "LevoitHumidifiers", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "schema": { 6 | "type": "object", 7 | "properties": { 8 | "name": { 9 | "title": "Name", 10 | "type": "string", 11 | "required": true, 12 | "default": "Levoit Humidifiers" 13 | }, 14 | "email": { 15 | "title": "Email", 16 | "type": "string", 17 | "required": true, 18 | "description": "VeSync's account email." 19 | }, 20 | "password": { 21 | "title": "Password", 22 | "type": "string", 23 | "required": true, 24 | "description": "VeSync's account password" 25 | }, 26 | "accessories": { 27 | "type": "object", 28 | "properties": { 29 | "humidity_sensor": { 30 | "title": "Humidity Sensor", 31 | "type": "boolean", 32 | "default": true, 33 | "description": "Expose humidity sensor from device(s) to HomeKit" 34 | }, 35 | "mist": { 36 | "title": "Mist Fan", 37 | "type": "boolean", 38 | "default": true, 39 | "description": "Enable fan for the Mist" 40 | }, 41 | "warm_mist": { 42 | "title": "Warm Mist Fan", 43 | "type": "boolean", 44 | "default": true, 45 | "description": "Enable fan for Warm Mist" 46 | }, 47 | "sleep_mode": { 48 | "title": "Sleep Mode Switch", 49 | "type": "boolean", 50 | "default": true, 51 | "description": "Enable switch for Sleep Mode" 52 | }, 53 | "display": { 54 | "title": "Display Switch", 55 | "type": "boolean", 56 | "default": true, 57 | "description": "Enable switch for the Display" 58 | }, 59 | "night_light": { 60 | "title": "Night Light Slider", 61 | "type": "boolean", 62 | "default": true, 63 | "description": "Enable brightness slider / switch for the Night Light" 64 | }, 65 | "auto_pro": { 66 | "title": "Auto Pro Mode Switch", 67 | "type": "boolean", 68 | "default": true, 69 | "description": "Enables switch to turn on / off Auto Pro Mode on LEH_S601S / LUH_O601S models" 70 | } 71 | } 72 | }, 73 | "options": { 74 | "type": "object", 75 | "properties": { 76 | "showOffWhenDisconnected": { 77 | "title": "Show Off When Disconnected", 78 | "type": "boolean", 79 | "default": false, 80 | "description": "When set to true, HomeKit will display unresponsive humidifiers as Off instead of Not Responding. Read the \"Note to Seasonal Humidifier Users\" in the README for more info." 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicPlatformPlugin, 3 | PlatformAccessory, 4 | PlatformConfig, 5 | Characteristic, 6 | Service, 7 | Logger, 8 | API, 9 | } from 'homebridge'; 10 | 11 | import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; 12 | import VeSyncAccessory from './VeSyncAccessory'; 13 | import VeSyncFan from './api/VeSyncFan'; 14 | import DebugMode from './debugMode'; 15 | import VeSync from './api/VeSync'; 16 | 17 | export interface VeSyncContext { 18 | name: string; 19 | device: VeSyncFan; 20 | } 21 | 22 | export type VeSyncPlatformAccessory = PlatformAccessory; 23 | 24 | export default class Platform implements DynamicPlatformPlugin { 25 | public readonly Service: typeof Service = this.api.hap.Service; 26 | public readonly Characteristic: typeof Characteristic = 27 | this.api.hap.Characteristic; 28 | 29 | public readonly cachedAccessories: VeSyncPlatformAccessory[] = []; 30 | public readonly registeredDevices: VeSyncAccessory[] = []; 31 | 32 | public readonly debugger: DebugMode; 33 | private readonly client: VeSync; 34 | 35 | constructor( 36 | public readonly log: Logger, 37 | public readonly config: PlatformConfig, 38 | public readonly api: API, 39 | ) { 40 | const { email, password, enableDebugMode } = this.config ?? {}; 41 | 42 | this.debugger = new DebugMode(!!enableDebugMode, this.log); 43 | this.debugger.debug('[PLATFORM]', 'Debug mode enabled'); 44 | 45 | this.client = new VeSync(email, password, this.config, this.debugger, log); 46 | 47 | this.api.on('didFinishLaunching', () => { 48 | this.discoverDevices(); 49 | }); 50 | } 51 | 52 | configureAccessory(accessory: VeSyncPlatformAccessory) { 53 | this.log.info('Loading accessory from cache:', accessory.displayName); 54 | this.cachedAccessories.push(accessory); 55 | } 56 | 57 | async discoverDevices() { 58 | const { email, password } = this.config ?? {}; 59 | if (!email || !password) { 60 | if (this.cachedAccessories.length > 0) { 61 | this.debugger.debug( 62 | '[PLATFORM]', 63 | 'Removing cached accessories because the email and password are not set (Count:', 64 | `${this.cachedAccessories.length})`, 65 | ); 66 | this.api.unregisterPlatformAccessories( 67 | PLUGIN_NAME, 68 | PLATFORM_NAME, 69 | this.cachedAccessories, 70 | ); 71 | } 72 | 73 | this.log.error('The email and password are not correct!'); 74 | } 75 | 76 | this.log.info('Connecting to the servers...'); 77 | await this.client.startSession(); 78 | this.log.info('Discovering devices...'); 79 | 80 | const devices = await this.client.getDevices(); 81 | await Promise.all(devices.map(this.loadDevice.bind(this))); 82 | 83 | this.checkOldDevices(); 84 | } 85 | 86 | private async loadDevice(device: VeSyncFan) { 87 | try { 88 | await device.updateInfo(); 89 | const { uuid, name } = device; 90 | 91 | const existingAccessory = this.cachedAccessories.find( 92 | (accessory) => accessory.UUID === uuid, 93 | ); 94 | 95 | if (existingAccessory) { 96 | this.log.info( 97 | 'Restoring existing accessory from cache:', 98 | existingAccessory.displayName, 99 | ); 100 | 101 | existingAccessory.context = { 102 | name, 103 | device, 104 | }; 105 | 106 | this.registeredDevices.push( 107 | new VeSyncAccessory(this, existingAccessory), 108 | ); 109 | 110 | return; 111 | } 112 | 113 | this.log.info('Adding new accessory:', name); 114 | const accessory = new this.api.platformAccessory( 115 | name, 116 | uuid, 117 | ); 118 | accessory.context = { 119 | name, 120 | device, 121 | }; 122 | 123 | this.registeredDevices.push(new VeSyncAccessory(this, accessory)); 124 | return this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ 125 | accessory, 126 | ]); 127 | } catch (error: any) { 128 | this.log.error( 129 | `Error for device: ${device.name}:${device.uuid} | ${error.message}`, 130 | ); 131 | return null; 132 | } 133 | } 134 | 135 | private checkOldDevices() { 136 | this.cachedAccessories.map((accessory) => { 137 | const exists = this.registeredDevices.find( 138 | (device) => device.UUID === accessory.UUID, 139 | ); 140 | 141 | if (!exists) { 142 | this.log.info('Remove cached accessory:', accessory.displayName); 143 | this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ 144 | accessory, 145 | ]); 146 | } 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebridge Levoit Humidifiers 2 | 3 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 4 | [![Discord](https://discordapp.com/api/guilds/432663330281226270/widget.png?style=shield)](https://discord.com/channels/432663330281226270/1055705874460594247) 5 | [![downloads-via-npm](https://img.shields.io/npm/dt/homebridge-levoit-humidifiers)](https://www.npmjs.com/package/homebridge-levoit-humidifiers) 6 | 7 | This is a Homebridge plugin to control Levoit Humidifiers from Apple HomeKit. 8 | 9 | | Supported Versions | Auto / Humidity | Mist | Sleep | Light | Display | Warm | 10 | | ------------------ | --------------- | ---- | ----- | ----- | ------- | ---- | 11 | | Superior 6000S | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | 12 | | OasisMist 1000S | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | 13 | | OasisMist 600S | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | 14 | | OasisMist 450S | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | 15 | | LV600S | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | 16 | | Classic 300S | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | 17 | | Classic 200S | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | 18 | | Dual 200S | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | 19 | 20 | ### Features (if supported by model) 21 | 22 | 1. Humidifier / Auto Mode 23 | - Sets humidifier to Auto / Humidity and sets the Target Humidity to the desired level. 24 | - Can also change Target Humidity in Sleep Mode, except on LV600 / Oasis. 25 | - For LV600s and Oasis, the Humidifier slider will be set to 0% when Sleep Mode is on. 26 | - For LV600s and Oasis, the Auto humidity range is 40-80%. All other models are 30-80%. 27 | - Selecting values outside the Auto range will set the Target Humidity to the lowest or highest number in the 28 | range. 29 | 30 | 2. Mist Level 31 | - Sets humidifier to Manual mode unless model supports changing mist levels in Auto / Humidity mode, and sets Mist level. 32 | - When set to Level 0, turns the device off. 33 | 34 | 3. Warm Mist Level 35 | - Sets Warm Mist Level to the desired level. 36 | 37 | 4. Sleep Mode 38 | - This switches the device between Sleep Mode (On) and Auto Mode (Off) 39 | - Sleep Mode Target Humidity is controlled by the Target Humidity slider, except on LV600s / Oasis. 40 | - On LV600s / Oasis, Sleep Mode Target Humidity is set by VeSync at 50–60% and cannot be changed. 41 | - The LV600s / Oasis turns off Warm Mist by default to keep the humidifier quiet. It can be turned back on with the Warm 42 | Mist slider. 43 | 44 | 5. Night Light 45 | - 4 brightness levels 46 | 47 | 6. Display Toggle 48 | - Toggles the display on/off 49 | 50 | 7. Humidity Sensor 51 | - Sensor that displays current Humidity % 52 | 53 | ### Details 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ### Configuration 64 | 65 | * Make sure 2FA is disabled on your VeSync account. 66 | * Via the Homebridge UI, enter the Homebridge VeSync Client plugin settings. 67 | * Enter your VeSync app credentials. 68 | * Select which controls you want exposed. Humidity / Auto Mode can not be hidden. 69 | * Enter your 2-letter country ISO code (eg. US, DE, FR). 70 | * Setup the platform plugin as a child bridge for better performance 71 | * Save and restart Homebridge. 72 | 73 | This plugin requires your VeSync credentials as it communicates with the VeSync devices via VeSync's own API. Your 74 | credentials are only stored in the Homebridge config and not sent to any server except VeSync's. 75 | 76 | You can also do this directly via the Homebridge config by adding your credentials to the config file under platforms. 77 | Replace the values of `username` and `password` with your credentials. 78 | 79 | You can turn off optional controls via the `accessories` section of the config or through the plugin UI settings. The 80 | Humidifier (Auto mode) slider cannot be turned off and will always be exposed. 81 | 82 | Via UI: 83 | 84 | 85 | 86 | Via config.json: 87 | 88 | ```json 89 | { 90 | "platforms": [ 91 | { 92 | "name": "Levoit Humidifiers", 93 | "email": "email", 94 | "password": "password", 95 | "platform": "LevoitHumidifiers", 96 | "accessories": { 97 | "display": false, 98 | "sleep_mode": false, 99 | "mist": false, 100 | "warm_mist": false, 101 | "night_light": false, 102 | "auto_pro": false, 103 | "humidity_sensor": true 104 | }, 105 | "options": { 106 | "showOffWhenDisconnected": false, 107 | "countryCode": "US" 108 | } 109 | } 110 | ] 111 | } 112 | ``` 113 | 114 | ### Note to Seasonal Humidifier Users: 115 | 116 | By default, if you disconnect a humidifier from WiFi, it will begin showing as "Not Responding" in HomeKit. Restarting 117 | Homebridge will remove the cached device from HomeKit. Once you've re-connected the humidifier, restart Homebridge again 118 | for it to display back in HomeKit. 119 | 120 | If you prefer the disconnected device to be visible in HomeKit at all times, 121 | set `showOffWhenDisconnected` to `true` in the config. The humidifiers will remain in HomeKit in an Off state. 122 | **Note: This will cause benign errors in the Homebridge logs that the device could not be contacted.** 123 | 124 | ### Enabling Debug Mode 125 | 126 | In the config file, add `enableDebugMode: true` 127 | 128 | ```json 129 | { 130 | "platforms": [ 131 | { 132 | "name": "Levoit Humidifiers", 133 | "email": "email", 134 | "password": "password", 135 | "platform": "LevoitHumidifiers", 136 | "enableDebugMode": true 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | ### Local Development 143 | 144 | To setup the local project, clone this repo and run the following from the root directory: 145 | 146 | ``` 147 | yarn install 148 | ``` 149 | 150 | To run locally, make sure to install Homebridge locally, and then run: 151 | 152 | ``` 153 | yarn watch 154 | ``` 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/api/deviceTypes.ts: -------------------------------------------------------------------------------- 1 | export enum DeviceName { 2 | Classic300S = 'Classic300S', 3 | Classic300S_US = 'LUH-A601S-WUSB', 4 | Classic300S_AUS = 'LUH-A601S-AUSW', 5 | Classic200S = 'Classic200S', 6 | Dual200S = 'Dual200S', 7 | Dual200S_LIGHT = 'LUH-D301S-WUSR', 8 | Dual200S_EU = 'LUH-D301S-WEU', 9 | Dual200S_UK = 'LUH-D301S-WUK', 10 | Dual200S_JP = 'LUH-D301S-WJP', 11 | LV600S = 'LUH-A602S-WUS', 12 | LV600S_REMOTE = 'LUH-A602S-WUSR', 13 | LV600S_EU = 'LUH-A602S-WEU', 14 | LV600S_UK = 'LUH-A602S-WUK', 15 | LV600S_JP = 'LUH-A602S-WJP', 16 | OASIS = 'LUH-O451S-WUS', 17 | OASIS_UK = 'LUH-O451S-WUK', 18 | OASIS_EU = 'LUH-O451S-WEU', 19 | OASIS_JP = 'LUH-O451S-WJP', 20 | OASIS_1000S = 'LUH-M101S-WUS', 21 | OASIS_1000S_UK = 'LUH-M101S-WUK', 22 | OASIS_1000S_EU = 'LUH-M101S-WEU', 23 | OASIS_1000S_EUR = 'LUH-M101S-WEUR', 24 | OASIS_1000S_JP = 'LUH-M101S-WJP', 25 | LEH_S601S_WUS = 'LEH-S601S-WUS', 26 | LEH_S601S_WUSR = 'LEH-S601S-WUSR', 27 | LUH_O601S_WUS = 'LUH-O601S-WUS', 28 | LUH_O601S_KUS = 'LUH-O601S-KUS', 29 | } 30 | 31 | export const NewDevices = [ 32 | // Devices with new response formats 33 | DeviceName.OASIS_1000S, 34 | DeviceName.OASIS_1000S_EU, 35 | DeviceName.OASIS_1000S_EUR, 36 | DeviceName.OASIS_1000S_JP, 37 | DeviceName.OASIS_1000S_UK, 38 | DeviceName.LEH_S601S_WUS, 39 | DeviceName.LEH_S601S_WUSR, 40 | ]; 41 | 42 | export interface DeviceType { 43 | isValid: (input: string) => boolean; 44 | hasAutoMode: boolean; 45 | mistLevels: number; 46 | hasLight: boolean; 47 | hasColorMode: boolean; 48 | hasSleepMode: boolean; 49 | hasWarmMode: boolean; 50 | warmMistLevels?: number; 51 | minHumidityLevel: number; 52 | maxHumidityLevel: number; 53 | hasAutoProMode?: boolean; 54 | } 55 | 56 | // All supported models. 57 | // TODO: Refactor this to look for a device prefix instead of the full device name. 58 | // That will allow us to reduce this list size and cause less failures when new model variations pop up. 59 | const deviceTypes: DeviceType[] = [ 60 | { 61 | isValid: (input: string) => input.includes(DeviceName.Classic300S), 62 | hasAutoMode: true, 63 | mistLevels: 9, 64 | hasLight: true, 65 | hasColorMode: false, 66 | hasSleepMode: true, 67 | hasWarmMode: false, 68 | minHumidityLevel: 30, 69 | maxHumidityLevel: 80, 70 | }, 71 | { 72 | isValid: (input: string) => input.includes(DeviceName.Classic300S_US), 73 | hasAutoMode: true, 74 | mistLevels: 9, 75 | hasLight: true, 76 | hasColorMode: false, 77 | hasSleepMode: true, 78 | hasWarmMode: false, 79 | minHumidityLevel: 30, 80 | maxHumidityLevel: 80, 81 | }, 82 | { 83 | isValid: (input: string) => input.includes(DeviceName.Classic300S_AUS), 84 | hasAutoMode: true, 85 | mistLevels: 9, 86 | hasLight: true, 87 | hasColorMode: false, 88 | hasSleepMode: true, 89 | hasWarmMode: false, 90 | minHumidityLevel: 30, 91 | maxHumidityLevel: 80, 92 | }, 93 | { 94 | isValid: (input: string) => input.includes(DeviceName.Classic200S), 95 | hasAutoMode: true, 96 | mistLevels: 9, 97 | hasLight: false, 98 | hasColorMode: false, 99 | hasSleepMode: false, 100 | hasWarmMode: false, 101 | minHumidityLevel: 30, 102 | maxHumidityLevel: 80, 103 | }, 104 | { 105 | isValid: (input: string) => input.includes(DeviceName.Dual200S), 106 | hasAutoMode: true, 107 | mistLevels: 2, 108 | hasLight: false, 109 | hasColorMode: false, 110 | hasSleepMode: false, 111 | hasWarmMode: false, 112 | minHumidityLevel: 30, 113 | maxHumidityLevel: 80, 114 | }, 115 | { 116 | isValid: (input: string) => input.includes(DeviceName.Dual200S_LIGHT), 117 | hasAutoMode: true, 118 | mistLevels: 2, 119 | hasLight: true, 120 | hasColorMode: true, 121 | hasSleepMode: true, 122 | hasWarmMode: false, 123 | minHumidityLevel: 30, 124 | maxHumidityLevel: 80, 125 | }, 126 | { 127 | isValid: (input: string) => input.includes(DeviceName.Dual200S_EU), 128 | hasAutoMode: true, 129 | mistLevels: 2, 130 | hasLight: false, 131 | hasColorMode: false, 132 | hasSleepMode: false, 133 | hasWarmMode: false, 134 | minHumidityLevel: 30, 135 | maxHumidityLevel: 80, 136 | }, 137 | { 138 | isValid: (input: string) => input.includes(DeviceName.Dual200S_UK), 139 | hasAutoMode: true, 140 | mistLevels: 2, 141 | hasLight: false, 142 | hasColorMode: false, 143 | hasSleepMode: false, 144 | hasWarmMode: false, 145 | minHumidityLevel: 30, 146 | maxHumidityLevel: 80, 147 | }, 148 | { 149 | isValid: (input: string) => input.includes(DeviceName.Dual200S_JP), 150 | hasAutoMode: true, 151 | mistLevels: 2, 152 | hasLight: false, 153 | hasColorMode: false, 154 | hasSleepMode: false, 155 | hasWarmMode: false, 156 | minHumidityLevel: 30, 157 | maxHumidityLevel: 80, 158 | }, 159 | { 160 | isValid: (input: string) => input.includes(DeviceName.LV600S), 161 | hasAutoMode: true, 162 | mistLevels: 9, 163 | hasLight: false, 164 | hasColorMode: false, 165 | hasSleepMode: true, 166 | hasWarmMode: true, 167 | warmMistLevels: 3, 168 | minHumidityLevel: 40, 169 | maxHumidityLevel: 80, 170 | }, 171 | { 172 | isValid: (input: string) => input.includes(DeviceName.LV600S_REMOTE), 173 | hasAutoMode: true, 174 | mistLevels: 9, 175 | hasLight: false, 176 | hasColorMode: false, 177 | hasSleepMode: true, 178 | hasWarmMode: true, 179 | warmMistLevels: 3, 180 | minHumidityLevel: 40, 181 | maxHumidityLevel: 80, 182 | }, 183 | { 184 | isValid: (input: string) => input.includes(DeviceName.LV600S_EU), 185 | hasAutoMode: true, 186 | mistLevels: 9, 187 | hasLight: false, 188 | hasColorMode: false, 189 | hasSleepMode: true, 190 | hasWarmMode: true, 191 | warmMistLevels: 3, 192 | minHumidityLevel: 40, 193 | maxHumidityLevel: 80, 194 | }, 195 | { 196 | isValid: (input: string) => input.includes(DeviceName.LV600S_UK), 197 | hasAutoMode: true, 198 | mistLevels: 9, 199 | hasLight: false, 200 | hasColorMode: false, 201 | hasSleepMode: true, 202 | hasWarmMode: true, 203 | warmMistLevels: 3, 204 | minHumidityLevel: 40, 205 | maxHumidityLevel: 80, 206 | }, 207 | { 208 | isValid: (input: string) => input.includes(DeviceName.LV600S_JP), 209 | hasAutoMode: true, 210 | mistLevels: 9, 211 | hasLight: false, 212 | hasColorMode: false, 213 | hasSleepMode: true, 214 | hasWarmMode: true, 215 | warmMistLevels: 3, 216 | minHumidityLevel: 40, 217 | maxHumidityLevel: 80, 218 | }, 219 | { 220 | isValid: (input: string) => input.includes(DeviceName.OASIS), 221 | hasAutoMode: true, 222 | mistLevels: 9, 223 | hasLight: false, 224 | hasColorMode: false, 225 | hasSleepMode: true, 226 | hasWarmMode: true, 227 | warmMistLevels: 3, 228 | minHumidityLevel: 40, 229 | maxHumidityLevel: 80, 230 | }, 231 | { 232 | isValid: (input: string) => input.includes(DeviceName.OASIS_UK), 233 | hasAutoMode: true, 234 | mistLevels: 9, 235 | hasLight: false, 236 | hasColorMode: false, 237 | hasSleepMode: true, 238 | hasWarmMode: true, 239 | warmMistLevels: 3, 240 | minHumidityLevel: 40, 241 | maxHumidityLevel: 80, 242 | }, 243 | { 244 | isValid: (input: string) => input.includes(DeviceName.OASIS_EU), 245 | hasAutoMode: true, 246 | mistLevels: 9, 247 | hasLight: false, 248 | hasColorMode: false, 249 | hasSleepMode: true, 250 | hasWarmMode: true, 251 | warmMistLevels: 3, 252 | minHumidityLevel: 40, 253 | maxHumidityLevel: 80, 254 | }, 255 | { 256 | isValid: (input: string) => input.includes(DeviceName.OASIS_JP), 257 | hasAutoMode: true, 258 | mistLevels: 9, 259 | hasLight: false, 260 | hasColorMode: false, 261 | hasSleepMode: true, 262 | hasWarmMode: true, 263 | warmMistLevels: 3, 264 | minHumidityLevel: 40, 265 | maxHumidityLevel: 80, 266 | }, 267 | { 268 | isValid: (input: string) => input.includes(DeviceName.OASIS_1000S), 269 | hasAutoMode: true, 270 | mistLevels: 9, 271 | hasLight: false, 272 | hasColorMode: false, 273 | hasSleepMode: true, 274 | hasWarmMode: false, 275 | minHumidityLevel: 40, 276 | maxHumidityLevel: 80, 277 | }, 278 | { 279 | isValid: (input: string) => input.includes(DeviceName.OASIS_1000S_UK), 280 | hasAutoMode: true, 281 | mistLevels: 9, 282 | hasLight: false, 283 | hasColorMode: false, 284 | hasSleepMode: true, 285 | hasWarmMode: false, 286 | minHumidityLevel: 40, 287 | maxHumidityLevel: 80, 288 | }, 289 | { 290 | isValid: (input: string) => input.includes(DeviceName.OASIS_1000S_EU), 291 | hasAutoMode: true, 292 | mistLevels: 9, 293 | hasLight: false, 294 | hasColorMode: false, 295 | hasSleepMode: true, 296 | hasWarmMode: false, 297 | minHumidityLevel: 40, 298 | maxHumidityLevel: 80, 299 | }, 300 | { 301 | isValid: (input: string) => input.includes(DeviceName.OASIS_1000S_EUR), 302 | hasAutoMode: true, 303 | mistLevels: 9, 304 | hasLight: false, 305 | hasColorMode: false, 306 | hasSleepMode: true, 307 | hasWarmMode: false, 308 | minHumidityLevel: 40, 309 | maxHumidityLevel: 80, 310 | }, 311 | { 312 | isValid: (input: string) => input.includes(DeviceName.OASIS_1000S_JP), 313 | hasAutoMode: true, 314 | mistLevels: 9, 315 | hasLight: false, 316 | hasColorMode: false, 317 | hasSleepMode: true, 318 | hasWarmMode: false, 319 | minHumidityLevel: 40, 320 | maxHumidityLevel: 80, 321 | }, 322 | { 323 | isValid: (input: string) => input.includes(DeviceName.LEH_S601S_WUSR), 324 | hasAutoMode: true, 325 | hasAutoProMode: true, 326 | mistLevels: 9, 327 | hasLight: false, 328 | hasColorMode: false, 329 | hasSleepMode: true, 330 | hasWarmMode: false, 331 | minHumidityLevel: 30, 332 | maxHumidityLevel: 80, 333 | }, 334 | { 335 | isValid: (input: string) => input.includes(DeviceName.LEH_S601S_WUS), 336 | hasAutoMode: true, 337 | hasAutoProMode: true, 338 | mistLevels: 9, 339 | hasLight: false, 340 | hasColorMode: false, 341 | hasSleepMode: true, 342 | hasWarmMode: false, 343 | minHumidityLevel: 40, 344 | maxHumidityLevel: 80, 345 | }, 346 | { 347 | isValid: (input: string) => input.includes(DeviceName.LUH_O601S_WUS), 348 | hasAutoMode: true, 349 | hasAutoProMode: false, 350 | mistLevels: 9, 351 | hasLight: false, 352 | hasColorMode: false, 353 | hasSleepMode: true, 354 | hasWarmMode: true, 355 | warmMistLevels: 3, 356 | minHumidityLevel: 40, 357 | maxHumidityLevel: 80, 358 | }, 359 | { 360 | isValid: (input: string) => input.includes(DeviceName.LUH_O601S_KUS), 361 | hasAutoMode: true, 362 | hasAutoProMode: false, 363 | mistLevels: 9, 364 | hasLight: false, 365 | hasColorMode: false, 366 | hasSleepMode: true, 367 | hasWarmMode: true, 368 | warmMistLevels: 3, 369 | minHumidityLevel: 40, 370 | maxHumidityLevel: 80, 371 | }, 372 | ]; 373 | 374 | export default deviceTypes; 375 | -------------------------------------------------------------------------------- /src/VeSyncAccessory.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'homebridge'; 2 | 3 | import Platform, { VeSyncPlatformAccessory } from './platform'; 4 | import CurrentState from './characteristics/CurrentState'; 5 | import Humidity from './characteristics/Humidity'; 6 | import Active from './characteristics/Active'; 7 | import VeSyncFan from './api/VeSyncFan'; 8 | import MistLevel from './characteristics/MistLevel'; 9 | import TargetState from './characteristics/TargetState'; 10 | import SleepState from './characteristics/SleepState'; 11 | import LightBrightness from './characteristics/LightBrightness'; 12 | import DisplayState from './characteristics/DisplayState'; 13 | import TargetHumidity from './characteristics/TargetHumidity'; 14 | import LightState from './characteristics/LightState'; 15 | import WarmMistLevel from './characteristics/WarmMistLevel'; 16 | import WarmActive from './characteristics/WarmActive'; 17 | import AutoProState from './characteristics/AutoProState'; 18 | 19 | const HumidifierName = 'Humidifier'; 20 | const HumiditySensorName = 'Humidity Sensor'; 21 | const MistName = 'Mist'; 22 | const CoolMistName = 'Cool Mist'; 23 | const WarmMistName = 'Warm Mist'; 24 | const NightLightName = 'Night Light'; 25 | const SleepModeName = 'Sleep Mode'; 26 | const DisplayName = 'Display'; 27 | const AutoProModeName = 'AutoPro Mode'; 28 | 29 | export type AccessoryThisType = ThisType<{ 30 | humidifierService: Service; 31 | platform: Platform; 32 | device: VeSyncFan; 33 | }>; 34 | 35 | export default class VeSyncAccessory { 36 | private humidifierService: Service; 37 | private humiditySensorService: Service | undefined; 38 | private lightService: Service | undefined; 39 | private sleepService: Service | undefined; 40 | private displayService: Service | undefined; 41 | private mistService: Service | undefined; 42 | private warmMistService: Service | undefined; 43 | private autoProService: Service | undefined; 44 | 45 | public get UUID() { 46 | return this.device.uuid.toString(); 47 | } 48 | 49 | private get device() { 50 | return this.accessory.context.device; 51 | } 52 | 53 | private get getMistValues() { 54 | /* 55 | Determines the number of mist level values to slide through in the Mist Level slider. 56 | Returns an array that contains the range of values between 1 and (mistLevels + 1). 57 | We add 1 to mistLevels to account for 0 as a potential level. 58 | Example: The Classic300s has 9 mist levels, so this function returns [0,1,2,3,4,5,6,7,8,9]. 59 | */ 60 | const arr = [...Array(this.device.deviceType.mistLevels + 1).keys()]; 61 | 62 | return arr; 63 | } 64 | 65 | private get getWarmMistValues() { 66 | /* 67 | Determines the number of mist level values to slide through in the Warm Mist Level slider. 68 | Returns an array that contains the range of values between 1 and (warmMistLevels + 1). 69 | We add 1 to warmMistLevels to account for 0 as a potential level. 70 | Example: The LV600s has 3 warm mist levels, so this function returns [0,1,2,3]. 71 | */ 72 | 73 | const arr = [ 74 | ...Array((this.device.deviceType.warmMistLevels ?? 0) + 1).keys(), 75 | ]; 76 | return arr; 77 | } 78 | 79 | constructor( 80 | private readonly platform: Platform, 81 | private readonly accessory: VeSyncPlatformAccessory, 82 | ) { 83 | const { manufacturer, model, mac } = this.device; 84 | const config = platform.config; 85 | const accessories = config.accessories ? config.accessories : {}; 86 | const mistAccessory = 87 | accessories.mist != false && accessories.cool_mist != false; 88 | const warmMistAccessory = accessories.warm_mist != false; 89 | const nightLightAccessory = accessories.night_light != false; 90 | const sleepModeAccessory = accessories.sleep_mode != false; 91 | const displayAccessory = accessories.display != false; 92 | const autoProAccessory = accessories.auto_pro != false; 93 | const humiditySensor = accessories.humidity_sensor != false; 94 | 95 | // Accessory info 96 | this.accessory 97 | .getService(this.platform.Service.AccessoryInformation)! 98 | .setCharacteristic( 99 | this.platform.Characteristic.Manufacturer, 100 | manufacturer, 101 | ) 102 | .setCharacteristic(this.platform.Characteristic.Model, model) 103 | .setCharacteristic(this.platform.Characteristic.SerialNumber, mac); 104 | 105 | // Humidifier service 106 | this.humidifierService = 107 | this.accessory.getService(HumidifierName) || 108 | this.accessory.addService( 109 | this.platform.Service.HumidifierDehumidifier, 110 | HumidifierName, 111 | HumidifierName, 112 | ); 113 | 114 | this.humidifierService.setPrimaryService(true); 115 | 116 | this.humidifierService 117 | .getCharacteristic(this.platform.Characteristic.Active) 118 | .onGet(Active.get.bind(this)) 119 | .onSet(Active.set.bind(this)); 120 | 121 | this.humidifierService 122 | .getCharacteristic( 123 | this.platform.Characteristic.TargetHumidifierDehumidifierState, 124 | ) 125 | .setProps({ 126 | validValues: [1], 127 | }) 128 | .onGet(TargetState.get.bind(this)); 129 | 130 | this.humidifierService 131 | .getCharacteristic( 132 | this.platform.Characteristic.CurrentHumidifierDehumidifierState, 133 | ) 134 | .setProps({ 135 | validValues: [1, 2], 136 | }) 137 | .onGet(CurrentState.get.bind(this)); 138 | 139 | this.humidifierService 140 | .getCharacteristic( 141 | this.platform.Characteristic.RelativeHumidityHumidifierThreshold, 142 | ) 143 | .setProps({ 144 | minStep: 1, 145 | minValue: 0, 146 | maxValue: 100, 147 | }) 148 | .onGet(TargetHumidity.get.bind(this)) 149 | .onSet(TargetHumidity.set.bind(this)); 150 | 151 | this.humidifierService 152 | .getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) 153 | .onGet(Humidity.get.bind(this)); 154 | 155 | // Mist service 156 | if (mistAccessory) { 157 | this.mistService = 158 | this.accessory.getService(MistName) || 159 | this.accessory.addService( 160 | this.platform.Service.Fan, 161 | MistName, 162 | MistName, 163 | ); 164 | 165 | this.mistService 166 | .getCharacteristic(this.platform.Characteristic.On) 167 | .onGet(Active.get.bind(this)) 168 | .onSet(Active.set.bind(this)); 169 | 170 | this.mistService 171 | .getCharacteristic(this.platform.Characteristic.RotationSpeed) 172 | .setProps({ 173 | minStep: 1, 174 | minValue: 0, 175 | maxValue: this.device.deviceType.mistLevels, 176 | validValues: this.getMistValues, 177 | }) 178 | .onGet(MistLevel.get.bind(this)) 179 | .onSet(MistLevel.set.bind(this)); 180 | this.humidifierService.addLinkedService(this.mistService); 181 | } else { 182 | this.mistService = 183 | this.accessory.getService(MistName) || 184 | this.accessory.getService(CoolMistName); 185 | if (this.mistService) { 186 | this.platform.log.info(`Removing ${MistName} service.`); 187 | this.accessory.removeService(this.mistService); 188 | } 189 | } 190 | 191 | // Display Switch service 192 | if (displayAccessory) { 193 | this.displayService = 194 | this.accessory.getService(DisplayName) || 195 | this.accessory.addService( 196 | this.platform.Service.Switch, 197 | DisplayName, 198 | DisplayName, 199 | ); 200 | 201 | this.humidifierService.addLinkedService(this.displayService); 202 | 203 | this.displayService 204 | .getCharacteristic(this.platform.Characteristic.On) 205 | .onGet(DisplayState.get.bind(this)) 206 | .onSet(DisplayState.set.bind(this)); 207 | } else { 208 | this.displayService = this.accessory.getService(DisplayName); 209 | if (this.displayService) { 210 | this.platform.log.info(`Removing ${DisplayName} service.`); 211 | this.accessory.removeService(this.displayService); 212 | } 213 | } 214 | 215 | // Humidity Sensor service 216 | if (humiditySensor) { 217 | this.humiditySensorService = 218 | this.accessory.getService(HumiditySensorName) || 219 | this.accessory.addService( 220 | this.platform.Service.HumiditySensor, 221 | HumiditySensorName, 222 | HumiditySensorName, 223 | ); 224 | 225 | this.humidifierService.addLinkedService(this.humiditySensorService); 226 | 227 | this.humiditySensorService 228 | .getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) 229 | .onGet(Humidity.get.bind(this)); 230 | } else { 231 | this.humiditySensorService = 232 | this.accessory.getService(HumiditySensorName); 233 | if (this.humiditySensorService) { 234 | this.platform.log.info(`Removing ${HumiditySensorName} service.`); 235 | this.accessory.removeService(this.humiditySensorService); 236 | } 237 | } 238 | 239 | // Warm Mist service 240 | if (this.device.deviceType.hasWarmMode && warmMistAccessory) { 241 | this.warmMistService = 242 | this.accessory.getService(WarmMistName) || 243 | this.accessory.addService( 244 | this.platform.Service.Fan, 245 | WarmMistName, 246 | WarmMistName, 247 | ); 248 | 249 | this.humidifierService.addLinkedService(this.warmMistService); 250 | 251 | this.warmMistService 252 | .getCharacteristic(this.platform.Characteristic.On) 253 | .onGet(WarmActive.get.bind(this)) 254 | .onSet(WarmActive.set.bind(this)); 255 | 256 | this.warmMistService 257 | .getCharacteristic(this.platform.Characteristic.RotationSpeed) 258 | .setProps({ 259 | minStep: 1, 260 | minValue: 0, 261 | maxValue: this.device.deviceType.warmMistLevels, 262 | validValues: this.getWarmMistValues, 263 | }) 264 | .onGet(WarmMistLevel.get.bind(this)) 265 | .onSet(WarmMistLevel.set.bind(this)); 266 | } else { 267 | this.warmMistService = this.accessory.getService(WarmMistName); 268 | if (this.warmMistService) { 269 | this.platform.log.info(`Removing ${WarmMistName} service.`); 270 | this.accessory.removeService(this.warmMistService); 271 | } 272 | } 273 | 274 | // Sleep Mode service 275 | if (this.device.deviceType.hasSleepMode && sleepModeAccessory) { 276 | this.sleepService = 277 | this.accessory.getService(SleepModeName) || 278 | this.accessory.addService( 279 | this.platform.Service.Switch, 280 | SleepModeName, 281 | SleepModeName, 282 | ); 283 | 284 | this.humidifierService.addLinkedService(this.sleepService); 285 | 286 | this.sleepService 287 | .getCharacteristic(this.platform.Characteristic.On) 288 | .onGet(SleepState.get.bind(this)) 289 | .onSet(SleepState.set.bind(this)); 290 | } else { 291 | this.sleepService = this.accessory.getService(SleepModeName); 292 | if (this.sleepService) { 293 | this.platform.log.info(`Removing ${SleepModeName} service.`); 294 | this.accessory.removeService(this.sleepService); 295 | } 296 | } 297 | 298 | // Night Light service 299 | if (this.device.deviceType.hasLight && nightLightAccessory) { 300 | this.lightService = 301 | this.accessory.getService(NightLightName) || 302 | this.accessory.addService( 303 | this.platform.Service.Lightbulb, 304 | NightLightName, 305 | NightLightName, 306 | ); 307 | 308 | this.humidifierService.addLinkedService(this.lightService); 309 | 310 | this.lightService 311 | .getCharacteristic(this.platform.Characteristic.On) 312 | .onGet(LightState.get.bind(this)) 313 | .onSet(LightState.set.bind(this)); 314 | 315 | let props: object; 316 | if (this.device.deviceType.hasColorMode) { 317 | props = { 318 | minValue: 39, 319 | maxValue: 100, 320 | }; 321 | } else { 322 | props = { 323 | minStep: 25, 324 | minValue: 0, 325 | maxValue: 100, 326 | validValues: [0, 25, 50, 75, 100], 327 | }; 328 | } 329 | 330 | this.lightService 331 | .getCharacteristic(this.platform.Characteristic.Brightness) 332 | .setProps(props) 333 | .onGet(LightBrightness.get.bind(this)) 334 | .onSet(LightBrightness.set.bind(this)); 335 | } else { 336 | this.lightService = this.accessory.getService(NightLightName); 337 | if (this.lightService) { 338 | this.platform.log.info(`Removing ${NightLightName} service.`); 339 | this.accessory.removeService(this.lightService); 340 | } 341 | } 342 | // AutoPro Switch service 343 | if (this.device.deviceType.hasAutoProMode && autoProAccessory) { 344 | this.autoProService = 345 | this.accessory.getService(AutoProModeName) || 346 | this.accessory.addService( 347 | this.platform.Service.Switch, 348 | AutoProModeName, 349 | AutoProModeName, 350 | ); 351 | 352 | this.humidifierService.addLinkedService(this.autoProService); 353 | 354 | this.autoProService 355 | .getCharacteristic(this.platform.Characteristic.On) 356 | .onGet(AutoProState.get.bind(this)) 357 | .onSet(AutoProState.set.bind(this)); 358 | } else { 359 | this.autoProService = this.accessory.getService(AutoProModeName); 360 | if (this.autoProService) { 361 | this.platform.log.info(`Removing ${AutoProModeName} service.`); 362 | this.accessory.removeService(this.autoProService); 363 | } 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/api/VeSyncFan.ts: -------------------------------------------------------------------------------- 1 | import AsyncLock from 'async-lock'; 2 | import deviceTypes, { DeviceName, DeviceType, NewDevices } from './deviceTypes'; 3 | 4 | import VeSync, { BypassMethod } from './VeSync'; 5 | 6 | export enum Mode { 7 | Manual = 'manual', 8 | Sleep = 'sleep', 9 | Auto = 'auto', 10 | AutoPro = 'autoPro', 11 | Humidity = 'humidity', 12 | } 13 | 14 | export default class VeSyncFan { 15 | private lock: AsyncLock = new AsyncLock(); 16 | public readonly deviceType: DeviceType; 17 | private lastCheck = 0; 18 | 19 | private _displayOn = true; 20 | 21 | public readonly manufacturer = 'Levoit'; 22 | 23 | public get humidityLevel() { 24 | return this._humidityLevel; 25 | } 26 | 27 | public get targetHumidity() { 28 | return this._targetHumidity; 29 | } 30 | 31 | public get displayOn() { 32 | return this._displayOn; 33 | } 34 | 35 | public get brightnessLevel() { 36 | return this._brightnessLevel; 37 | } 38 | 39 | public get mistLevel() { 40 | return this._mistLevel; 41 | } 42 | 43 | public get warmLevel() { 44 | return this._warmLevel; 45 | } 46 | 47 | public get warmEnabled() { 48 | return this._warmEnabled; 49 | } 50 | 51 | public get lightOn() { 52 | return this._lightOn; 53 | } 54 | 55 | public get mode() { 56 | return this._mode; 57 | } 58 | 59 | public get targetReached() { 60 | return this._targetReached; 61 | } 62 | 63 | public get isOn() { 64 | return this._isOn; 65 | } 66 | 67 | public get getBlue() { 68 | return this._blue; 69 | } 70 | 71 | public get getGreen() { 72 | return this._green; 73 | } 74 | 75 | public get getColorMode() { 76 | return this._colorMode; 77 | } 78 | 79 | public get getColorSliderLocation() { 80 | return this._colorSliderLocation; 81 | } 82 | 83 | public get getLightSpeed() { 84 | return this._lightSpeed; 85 | } 86 | 87 | public get getRed() { 88 | return this._red; 89 | } 90 | 91 | constructor( 92 | private readonly client: VeSync, 93 | public readonly name: string, 94 | private _mode: Mode, 95 | private _isOn: boolean, 96 | private _mistLevel: number, 97 | private _warmLevel: number, 98 | private _warmEnabled: boolean, 99 | private _brightnessLevel: number, 100 | private _humidityLevel: number, 101 | private _targetHumidity: number, 102 | private _targetReached: boolean, 103 | private _lightOn: string, 104 | private _lightSpeed: number, 105 | private _red: number, 106 | private _blue: number, 107 | private _green: number, 108 | private _colorMode: string, 109 | private _colorSliderLocation: number, 110 | public readonly configModule: string, 111 | public readonly cid: string, 112 | public readonly region: string, 113 | public readonly model: string, 114 | public readonly mac: string, 115 | public readonly uuid: string, 116 | ) { 117 | this.deviceType = deviceTypes.find(({ isValid }) => isValid(this.model))!; 118 | } 119 | 120 | public async setPower(power: boolean): Promise { 121 | this.client.log.info('Setting Power to ' + power); 122 | let switchJson; 123 | if (NewDevices.includes(this.model as DeviceName)) { 124 | switchJson = { 125 | powerSwitch: power ? 1 : 0, 126 | id: 0, 127 | }; 128 | } else { 129 | switchJson = { 130 | enabled: power, 131 | id: 0, 132 | }; 133 | } 134 | const success = await this.client.sendCommand( 135 | this, 136 | BypassMethod.SWITCH, 137 | switchJson, 138 | ); 139 | 140 | if (success) { 141 | this._isOn = power; 142 | if (!this._isOn) { 143 | this._humidityLevel = 0; 144 | this._targetHumidity = 0; 145 | this._mistLevel = 0; 146 | this._warmLevel = 0; 147 | } 148 | } else { 149 | this.client.log.error('Failed to setPower due to unreachable device.'); 150 | if (this.client.config.options.showOffWhenDisconnected) { 151 | this._isOn = false; 152 | this._humidityLevel = 0; 153 | this._targetHumidity = 0; 154 | this._displayOn = false; 155 | this._mistLevel = 0; 156 | this._warmLevel = 0; 157 | this._brightnessLevel = 0; 158 | this._lightOn = 'off'; 159 | } else { 160 | return false; 161 | } 162 | } 163 | 164 | return success; 165 | } 166 | 167 | public async setTargetHumidity(level: number): Promise { 168 | this.client.log.info('Setting Target Humidity to ' + level); 169 | 170 | // Oasis 1000 uses camelcase instead of snakecase 171 | let humidityJson; 172 | if (NewDevices.includes(this.model as DeviceName)) { 173 | humidityJson = { 174 | targetHumidity: level, 175 | id: 0, 176 | }; 177 | } else { 178 | humidityJson = { 179 | target_humidity: level, 180 | id: 0, 181 | }; 182 | } 183 | 184 | const success = await this.client.sendCommand( 185 | this, 186 | BypassMethod.HUMIDITY, 187 | humidityJson, 188 | ); 189 | 190 | if (success) { 191 | this._targetHumidity = level; 192 | } 193 | 194 | return success; 195 | } 196 | 197 | public async changeMode(mode: Mode): Promise { 198 | // LV600s models use "Humidity" mode instead of "Auto" 199 | const humidity_models = [ 200 | DeviceName.LV600S, 201 | DeviceName.LV600S_REMOTE, 202 | DeviceName.LV600S_EU, 203 | DeviceName.LV600S_UK, 204 | DeviceName.LV600S_JP, 205 | ]; 206 | if ( 207 | humidity_models.includes(this.model as DeviceName) && 208 | mode == Mode.Auto 209 | ) { 210 | mode = Mode.Humidity; 211 | } 212 | // Some models use "AutoPro" mode instead of "Auto" 213 | if (this.deviceType.hasAutoProMode && mode == Mode.Auto) { 214 | mode = Mode.AutoPro; 215 | } 216 | 217 | let success: boolean; 218 | 219 | // Oasis 1000 uses camelcase instead of snakecase 220 | let modeJson; 221 | if (NewDevices.includes(this.model as DeviceName)) { 222 | modeJson = { 223 | workMode: mode.toString(), 224 | }; 225 | } else { 226 | modeJson = { 227 | mode: mode.toString(), 228 | }; 229 | } 230 | // Don't change the mode if we are already in that mode 231 | if (this._mode == mode) { 232 | success = true; 233 | } else { 234 | this.client.log.info('Changing Mode to ' + mode); 235 | success = await this.client.sendCommand( 236 | this, 237 | BypassMethod.MODE, 238 | modeJson, 239 | ); 240 | } 241 | if (success) { 242 | this._mode = mode; 243 | } 244 | 245 | return success; 246 | } 247 | 248 | public async setBrightness(brightness: number): Promise { 249 | this.client.log.info('Setting Night Light to ' + brightness); 250 | 251 | const success = await this.client.sendCommand( 252 | this, 253 | BypassMethod.NIGHT_LIGHT_BRIGHTNESS, 254 | { 255 | night_light_brightness: brightness, 256 | }, 257 | ); 258 | 259 | if (success) { 260 | this._brightnessLevel = brightness; 261 | } 262 | 263 | return success; 264 | } 265 | 266 | public async setDisplay(power: boolean): Promise { 267 | this.client.log.info('Setting Display to ' + power); 268 | 269 | // Oasis 1000 uses camelcase instead of snakecase 270 | let displayJson; 271 | if (NewDevices.includes(this.model as DeviceName)) { 272 | displayJson = { 273 | screenSwitch: power ? 1 : 0, 274 | id: 0, 275 | }; 276 | } else { 277 | displayJson = { 278 | state: power, 279 | id: 0, 280 | }; 281 | } 282 | 283 | const success = await this.client.sendCommand( 284 | this, 285 | BypassMethod.DISPLAY, 286 | displayJson, 287 | ); 288 | 289 | if (success) { 290 | this._displayOn = power; 291 | } 292 | 293 | return success; 294 | } 295 | 296 | public async changeMistLevel(mistLevel: number): Promise { 297 | if (mistLevel > this.deviceType.mistLevels || mistLevel < 1) { 298 | return false; 299 | } 300 | 301 | this.client.log.info('Setting Mist Level to ' + mistLevel); 302 | 303 | // New models use different JSON keys 304 | let mistJson; 305 | const method = BypassMethod.MIST_LEVEL; 306 | if (NewDevices.includes(this.model as DeviceName)) { 307 | mistJson = { 308 | virtualLevel: mistLevel, 309 | levelType: 'mist', 310 | id: 0, 311 | }; 312 | } else { 313 | mistJson = { 314 | level: mistLevel, 315 | type: 'mist', 316 | id: 0, 317 | }; 318 | } 319 | 320 | const success = await this.client.sendCommand(this, method, mistJson); 321 | 322 | if (success) { 323 | this._mistLevel = mistLevel; 324 | } 325 | 326 | return success; 327 | } 328 | 329 | public async changeWarmMistLevel(warmMistLevel: number): Promise { 330 | if (!this.deviceType.warmMistLevels) { 331 | this.client.log.error( 332 | 'Error: Attempted to set warm level on device without warmMistLevels field.', 333 | ); 334 | return false; 335 | } 336 | 337 | if (warmMistLevel > this.deviceType.warmMistLevels || warmMistLevel < 0) { 338 | return false; 339 | } 340 | 341 | this.client.log.info('Setting Warm Level to ' + warmMistLevel); 342 | 343 | const success = await this.client.sendCommand(this, BypassMethod.LEVEL, { 344 | level: warmMistLevel, 345 | type: 'warm', 346 | id: 0, 347 | }); 348 | 349 | if (success) { 350 | this._warmLevel = warmMistLevel; 351 | if (this._warmLevel == 0) { 352 | this._warmEnabled = false; 353 | } else { 354 | this._warmEnabled = true; 355 | } 356 | } 357 | 358 | return success; 359 | } 360 | 361 | public async setLightStatus( 362 | action: string, 363 | brightness: number, 364 | ): Promise { 365 | // Get the current RGB values and brightness % 366 | const red = this.getRed; 367 | const green = this.getGreen; 368 | const blue = this.getBlue; 369 | const currentBrightness = this.brightnessLevel; 370 | let newRed: number | undefined; 371 | let newGreen: number | undefined; 372 | let newBlue: number | undefined; 373 | 374 | // If we're changing brightness, calculate the RGB values to adjust to 375 | if (brightness !== this._brightnessLevel) { 376 | newRed = Math.round(red * (brightness / currentBrightness)); 377 | newGreen = Math.round(green * (brightness / currentBrightness)); 378 | newBlue = Math.round(blue * (brightness / currentBrightness)); 379 | } 380 | 381 | const lightJson = { 382 | action: action, 383 | speed: this.getLightSpeed, 384 | red: newRed || this.getRed, 385 | green: newGreen || this.getGreen, 386 | blue: newBlue || this.getBlue, 387 | brightness: brightness, 388 | colorMode: this.getColorMode, 389 | colorSliderLocation: this.getColorSliderLocation, 390 | }; 391 | this.client.log.debug( 392 | 'Setting Night Light Status to ' + JSON.stringify(lightJson), 393 | ); 394 | 395 | const success = await this.client.sendCommand( 396 | this, 397 | BypassMethod.LIGHT_STATUS, 398 | lightJson, 399 | ); 400 | 401 | if (success) { 402 | this._brightnessLevel = brightness; 403 | this._blue = newBlue || this.getBlue; 404 | this._green = newGreen || this.getGreen; 405 | this._red = newRed || this.getRed; 406 | this._lightOn = action; 407 | } 408 | 409 | return success; 410 | } 411 | 412 | public async updateInfo(): Promise { 413 | return this.lock.acquire('update-info', async () => { 414 | try { 415 | if (Date.now() - this.lastCheck < 5 * 1000) { 416 | return; 417 | } 418 | 419 | const data = await this.client.getDeviceInfo(this); 420 | 421 | this.lastCheck = Date.now(); 422 | if ( 423 | !data?.result?.result && 424 | this.client.config.options.showOffWhenDisconnected 425 | ) { 426 | this._isOn = false; 427 | this._humidityLevel = 0; 428 | this._targetHumidity = 0; 429 | this._displayOn = false; 430 | this._mistLevel = 0; 431 | this._warmLevel = 0; 432 | this._brightnessLevel = 0; 433 | return; 434 | } else if (!data?.result?.result) { 435 | return; 436 | } 437 | 438 | const result = data?.result?.result; 439 | 440 | this._humidityLevel = result.humidity; 441 | // Fields are different on newer models 442 | if (NewDevices.includes(this.model as DeviceName)) { 443 | this._targetHumidity = result.targetHumidity; 444 | this._displayOn = result.screenSwitch; 445 | this._mode = result.workMode; 446 | this._isOn = result.powerSwitch; 447 | this._targetReached = result.autoStopState; 448 | this._mistLevel = result.virtualLevel; 449 | } else { 450 | this._targetHumidity = result.configuration?.auto_target_humidity; 451 | this._displayOn = result.display; 452 | this._mode = result.mode; 453 | this._isOn = result.enabled; 454 | this._targetReached = result.automatic_stop_reach_target; 455 | this._mistLevel = result.mist_virtual_level; 456 | } 457 | 458 | this._warmLevel = result.warm_level; 459 | this._warmEnabled = result.warm_enabled; 460 | 461 | this._brightnessLevel = 462 | result.night_light_brightness ?? result.rgbNightLight?.brightness; 463 | // RGB Light Devices Only: 464 | this._lightOn = result.rgbNightLight?.action; 465 | this._blue = result.rgbNightLight?.blue; 466 | this._green = result.rgbNightLight?.green; 467 | this._red = result.rgbNightLight?.red; 468 | this._colorMode = result.rgbNightLight?.colorMode; 469 | this._lightSpeed = result.rgbNightLight?.speed; 470 | this._colorSliderLocation = result.rgbNightLight?.colorSliderLocation; 471 | 472 | if (result.rgbNightLight) { 473 | const lightJson = { 474 | action: this._lightOn, 475 | speed: this._lightSpeed, 476 | green: this._green, 477 | blue: this._blue, 478 | red: this._red, 479 | brightness: this._brightnessLevel, 480 | colorMode: this._colorMode, 481 | colorSliderLocation: this._colorSliderLocation, 482 | }; 483 | 484 | this.client.debugMode.debug( 485 | '[GET LIGHT JSON]', 486 | JSON.stringify(lightJson), 487 | ); 488 | } 489 | } catch (err: any) { 490 | this.client.log.error( 491 | 'Failed to updateInfo due to unreachable device: ' + err?.message, 492 | ); 493 | if (this.client.config.options.showOffWhenDisconnected) { 494 | this._isOn = false; 495 | this._humidityLevel = 0; 496 | this._targetHumidity = 0; 497 | this._displayOn = false; 498 | this._mistLevel = 0; 499 | this._warmLevel = 0; 500 | this._brightnessLevel = 0; 501 | } else { 502 | throw new Error( 503 | 'Device was unreachable. Ensure it is plugged in and connected to WiFi.', 504 | ); 505 | } 506 | } 507 | }); 508 | } 509 | 510 | public static fromResponse = 511 | (client: VeSync) => 512 | ({ 513 | deviceName, 514 | mode, 515 | deviceStatus, 516 | mistLevel, 517 | warmLevel, 518 | warmEnabled, 519 | brightnessLevel, 520 | humidity, 521 | targetHumidity, 522 | targetReached, 523 | lightOn, 524 | lightSpeed, 525 | red, 526 | blue, 527 | green, 528 | colorMode, 529 | colorSliderLocation, 530 | configModule, 531 | cid, 532 | deviceRegion, 533 | deviceType, 534 | macID, 535 | uuid, 536 | }) => 537 | new VeSyncFan( 538 | client, 539 | deviceName, 540 | mode, 541 | deviceStatus, 542 | mistLevel, 543 | warmLevel, 544 | warmEnabled, 545 | brightnessLevel, 546 | humidity, 547 | targetHumidity, 548 | targetReached, 549 | lightOn, 550 | lightSpeed, 551 | red, 552 | blue, 553 | green, 554 | colorMode, 555 | colorSliderLocation, 556 | configModule, 557 | cid, 558 | deviceRegion, 559 | deviceType, 560 | macID, 561 | uuid, 562 | ); 563 | } 564 | -------------------------------------------------------------------------------- /src/api/VeSync.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import { Logger, PlatformConfig } from 'homebridge'; 3 | import AsyncLock from 'async-lock'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import * as crypto from 'crypto'; 6 | 7 | import deviceTypes from './deviceTypes'; 8 | import DebugMode from '../debugMode'; 9 | import VeSyncFan from './VeSyncFan'; 10 | 11 | export enum BypassMethod { 12 | STATUS = 'getHumidifierStatus', 13 | MODE = 'setHumidityMode', 14 | NIGHT_LIGHT_BRIGHTNESS = 'setNightLightBrightness', 15 | DISPLAY = 'setDisplay', 16 | SWITCH = 'setSwitch', 17 | HUMIDITY = 'setTargetHumidity', 18 | MIST_LEVEL = 'setVirtualLevel', 19 | LEVEL = 'setLevel', 20 | LIGHT_STATUS = 'setLightStatus', 21 | DRYING_MODE = 'setDryingMode', 22 | } 23 | 24 | function initialHostForCountry(cc: string): string { 25 | const upper = cc.toUpperCase(); 26 | if (['US', 'CA', 'MX', 'JP'].includes(upper)) return US_HOST; 27 | return EU_HOST; // everything else starts on EU 28 | } 29 | 30 | function pickCountryCodeForRetry(step2Resp: any, originalCC: string): string { 31 | const region = (step2Resp?.result?.currentRegion || step2Resp?.currentRegion || '').toUpperCase(); 32 | if (region === 'US') return 'US'; 33 | 34 | // Prefer server-provided countryCode if present 35 | const serverCC = step2Resp?.result?.countryCode || step2Resp?.countryCode; 36 | const cc = (serverCC || originalCC || '').toUpperCase(); 37 | 38 | // Basic sanity: must be 2 letters; otherwise keep original if valid, else fallback to 'US' 39 | return /^[A-Z]{2}$/.test(cc) ? cc : (/^[A-Z]{2}$/.test(originalCC.toUpperCase()) ? originalCC.toUpperCase() : 'US'); 40 | } 41 | 42 | const lock = new AsyncLock(); 43 | 44 | // Known API hosts 45 | const US_HOST = 'https://smartapi.vesync.com'; 46 | const EU_HOST = 'https://smartapi.vesync.eu'; 47 | const ACCOUNT_HOST = 'https://accountapi.vesync.com'; 48 | 49 | // Server error code indicating cross-region login is required 50 | const CROSS_REGION_CODE = -11260022; 51 | 52 | // Convert server region → host (default to US if unknown) 53 | function regionToHost(region?: string): string { 54 | if (typeof region === 'string' && region.toUpperCase() === 'EU') return EU_HOST; 55 | return US_HOST; 56 | } 57 | 58 | export default class VeSync { 59 | private api?: AxiosInstance; 60 | private accountId?: string; 61 | private token?: string; 62 | 63 | // dynamic baseURL; starts with US then may flip to EU on cross-region 64 | private baseURL: string; 65 | 66 | private readonly VERSION = '5.6.60'; 67 | private readonly FULL_VERSION = `VeSync ${this.VERSION}`; 68 | private readonly AGENT = `VeSync/${this.VERSION} (iPhone; iOS 17.2.1; Humidifier/5.00)`; 69 | private readonly TIMEZONE = 'America/New_York'; 70 | private readonly OS = 'iOS 17.2.1'; 71 | private readonly BRAND = 'iPhone 15 Pro'; 72 | private readonly LANG = 'en'; 73 | 74 | // Terminal/device identifier that VeSync expects to remain stable 75 | private readonly terminalId = '2' + uuidv4().replace(/-/g, ''); 76 | private readonly appID = Math.random().toString(36).substring(2, 10); 77 | 78 | constructor( 79 | private readonly email: string, 80 | private readonly password: string, 81 | readonly config: PlatformConfig, 82 | public readonly debugMode: DebugMode, 83 | public readonly log: Logger, 84 | ) { 85 | // Allow explicit override via config.options.apiHost; otherwise start with US 86 | const cc = (config.options?.countryCode || 'US').toUpperCase(); 87 | this.baseURL = config.options?.apiHost || initialHostForCountry(cc); 88 | this.debugMode.debug?.('[CONFIG]', `countryCode=${cc}, initialBaseURL=${this.baseURL}`); 89 | } 90 | 91 | private AXIOS_OPTIONS() { 92 | return { 93 | baseURL: this.baseURL, 94 | timeout: this.config.options?.apiTimeout || 15000, 95 | }; 96 | } 97 | 98 | private ACCOUNT_AXIOS_OPTIONS() { 99 | return { 100 | baseURL: ACCOUNT_HOST, 101 | timeout: this.config.options?.apiTimeout || 15000, 102 | headers: { 103 | 'content-type': 'application/json', 104 | 'accept-language': this.LANG, 105 | 'user-agent': this.AGENT, 106 | appversion: this.FULL_VERSION, 107 | tz: this.TIMEZONE, 108 | }, 109 | }; 110 | } 111 | 112 | private generateDetailBody() { 113 | return { 114 | appVersion: this.FULL_VERSION, 115 | phoneBrand: this.BRAND, 116 | traceId: `APP${Date.now()}-00001`, 117 | phoneOS: this.OS, 118 | }; 119 | } 120 | 121 | private generateBody(includeAuth = false) { 122 | return { 123 | acceptLanguage: this.LANG, 124 | timeZone: this.TIMEZONE, 125 | ...(includeAuth 126 | ? { 127 | accountID: this.accountId, 128 | token: this.token, 129 | } 130 | : {}), 131 | }; 132 | } 133 | 134 | private generateV2Body(fan: VeSyncFan, method: BypassMethod, data = {}) { 135 | return { 136 | method: 'bypassV2', 137 | debugMode: false, 138 | deviceRegion: fan.region, 139 | cid: fan.cid, 140 | configModule: fan.configModule, 141 | payload: { 142 | data: { 143 | ...data, 144 | }, 145 | method, 146 | source: 'APP', 147 | }, 148 | }; 149 | } 150 | 151 | public async sendCommand( 152 | fan: VeSyncFan, 153 | method: BypassMethod, 154 | body = {}, 155 | ): Promise { 156 | return lock.acquire('api-call', async () => { 157 | if (!this.api) { 158 | throw new Error('The user is not logged in!'); 159 | } 160 | 161 | this.debugMode.debug( 162 | '[SEND COMMAND]', 163 | `Sending command ${method} to ${fan.name}`, 164 | `with (${JSON.stringify(body)})...`, 165 | ); 166 | 167 | const response = await this.api.put('cloud/v2/deviceManaged/bypassV2', { 168 | ...this.generateV2Body(fan, method, body), 169 | ...this.generateDetailBody(), 170 | ...this.generateBody(true), 171 | }); 172 | 173 | if (response.data?.msg === 'device offline') { 174 | this.log.error( 175 | 'VeSync cannot communicate with humidifier! Check the VeSync App.', 176 | ); 177 | if (this.config.options?.showOffWhenDisconnected) { 178 | return false; 179 | } else { 180 | throw new Error( 181 | 'Device was unreachable. Ensure it is plugged in and connected to WiFi.', 182 | ); 183 | } 184 | } 185 | 186 | if (!response?.data) { 187 | this.debugMode.debug( 188 | '[SEND COMMAND]', 189 | 'No response data!! JSON:', 190 | JSON.stringify(response?.data), 191 | ); 192 | } 193 | 194 | const isSuccess = response?.data?.code === 0; 195 | if (!isSuccess) { 196 | this.debugMode.debug( 197 | '[SEND COMMAND]', 198 | `Failed to send command ${method} to ${fan.name}`, 199 | `with (${JSON.stringify(body)})!`, 200 | `Response: ${JSON.stringify(response?.data)}`, 201 | ); 202 | } else { 203 | this.debugMode.debug( 204 | '[SEND COMMAND]', 205 | `Successfully sent command ${method} to ${fan.name}`, 206 | `with (${JSON.stringify(body)})!`, 207 | `Response: ${JSON.stringify(response.data)}`, 208 | ); 209 | } 210 | 211 | return isSuccess; 212 | }); 213 | } 214 | 215 | public async getDeviceInfo(fan: VeSyncFan): Promise { 216 | return lock.acquire('api-call', async () => { 217 | if (!this.api) { 218 | throw new Error('The user is not logged in!'); 219 | } 220 | 221 | this.debugMode.debug('[GET DEVICE INFO]', 'Getting device info...'); 222 | 223 | const response = await this.api.post('cloud/v2/deviceManaged/bypassV2', { 224 | ...this.generateV2Body(fan, BypassMethod.STATUS), 225 | ...this.generateDetailBody(), 226 | ...this.generateBody(true), 227 | }); 228 | 229 | this.debugMode.debug('[DEVICE INFO]', JSON.stringify(response.data)); 230 | 231 | if (response.data?.msg === 'device offline') { 232 | this.log.error( 233 | 'VeSync cannot communicate with humidifier! Check the VeSync App.', 234 | ); 235 | if (this.config.options?.showOffWhenDisconnected) { 236 | return false; 237 | } else { 238 | throw new Error( 239 | 'Device was unreachable. Ensure it is plugged in and connected to WiFi.', 240 | ); 241 | } 242 | } 243 | 244 | if (!response?.data) { 245 | this.debugMode.debug( 246 | '[GET DEVICE INFO]', 247 | 'No response data!! JSON:', 248 | JSON.stringify(response?.data), 249 | ); 250 | } 251 | 252 | return response.data; 253 | }); 254 | } 255 | 256 | public async startSession(): Promise { 257 | this.debugMode.debug('[START SESSION]', 'Starting auth session...'); 258 | const ok = await this.login(); 259 | if (ok) setInterval(this.login.bind(this), 1000 * 60 * 55); 260 | return ok; 261 | } 262 | 263 | private async login(): Promise { 264 | return lock.acquire('auth-call', async () => { 265 | if (!this.email || !this.password) { 266 | throw new Error('Email and password are required'); 267 | } 268 | 269 | const userCountryCode = (this.config.options?.countryCode || 'US').toUpperCase(); 270 | 271 | this.debugMode.debug('[LOGIN]', 'Step 1: authByPWDOrOTM…'); 272 | const { authorizeCode, bizToken: initialBizToken } = 273 | await this.authByPWDOrOTM(userCountryCode); 274 | 275 | this.debugMode.debug('[LOGIN]', `Step 2: loginByAuthorizeCode on ${this.baseURL}…`); 276 | let step2Resp = await this.loginByAuthorizeCode4Vesync({ 277 | userCountryCode, 278 | authorizeCode, 279 | host: this.baseURL, 280 | }); 281 | 282 | if (step2Resp?.code === CROSS_REGION_CODE) { 283 | const currentRegion = 284 | step2Resp?.result?.currentRegion || 285 | step2Resp?.data?.currentRegion || 286 | step2Resp?.currentRegion; 287 | 288 | const crossBizToken = 289 | step2Resp?.result?.bizToken || 290 | step2Resp?.data?.bizToken || 291 | initialBizToken || 292 | null; 293 | 294 | const regionHost = regionToHost(currentRegion); 295 | const overrideCC = pickCountryCodeForRetry(step2Resp, userCountryCode); 296 | 297 | this.debugMode.debug( 298 | '[LOGIN]', 299 | `Cross-region detected (${currentRegion}). Retrying on ${regionHost} with bizToken and userCountryCode=${overrideCC} (regionChange=last_region)…`, 300 | ); 301 | 302 | this.baseURL = (this.config.options?.apiHost as string) || regionHost; 303 | 304 | step2Resp = await this.loginByAuthorizeCode4Vesync({ 305 | userCountryCode, 306 | bizToken: crossBizToken, 307 | host: this.baseURL, 308 | regionChange: 'last_region', 309 | overrideCountryCode: overrideCC, 310 | currentRegion, 311 | }); 312 | } 313 | 314 | if (!step2Resp || step2Resp.code !== 0 || !step2Resp.result?.token || !step2Resp.result?.accountID) { 315 | this.debugMode.debug('[LOGIN] Failed final step', JSON.stringify(step2Resp)); 316 | return false; 317 | } 318 | 319 | const { token, accountID } = step2Resp.result; 320 | 321 | this.debugMode.debug('[LOGIN]', 'Authentication was successful'); 322 | 323 | this.accountId = accountID; 324 | this.token = token; 325 | 326 | this.api = axios.create({ 327 | ...this.AXIOS_OPTIONS(), 328 | headers: { 329 | 'content-type': 'application/json', 330 | 'accept-language': this.LANG, 331 | accountid: this.accountId!, 332 | 'user-agent': this.AGENT, 333 | appversion: this.FULL_VERSION, 334 | tz: this.TIMEZONE, 335 | tk: this.token!, 336 | }, 337 | }); 338 | 339 | this.api.interceptors.response.use( 340 | (resp) => resp, 341 | async (err) => { 342 | if (err?.response?.status === 401) { 343 | this.debugMode.debug('[AUTH]', '401 detected, re-authenticating…'); 344 | const ok = await this.login(); 345 | if (ok && err.config) { 346 | err.config.headers = err.config.headers || {}; 347 | err.config.headers.tk = this.token!; 348 | err.config.headers.accountid = this.accountId!; 349 | return this.api!.request(err.config); 350 | } 351 | } 352 | throw err; 353 | }, 354 | ); 355 | return true; 356 | }); 357 | } 358 | 359 | private async authByPWDOrOTM( 360 | userCountryCode: string, 361 | ): Promise<{ authorizeCode: string | null; bizToken: string | null }> { 362 | const pwdHashed = crypto.createHash('md5').update(this.password).digest('hex'); 363 | const body = { 364 | email: this.email, 365 | method: 'authByPWDOrOTM', 366 | password: pwdHashed, 367 | acceptLanguage: this.LANG, 368 | accountID: '', 369 | authProtocolType: 'generic', 370 | clientInfo: this.BRAND, 371 | clientType: 'vesyncApp', 372 | clientVersion: this.FULL_VERSION, 373 | debugMode: false, 374 | osInfo: this.OS.includes('iOS') ? 'iOS' : 'Android', 375 | terminalId: this.terminalId, 376 | timeZone: this.TIMEZONE, 377 | token: '', 378 | userCountryCode, 379 | userType: 1, 380 | devToken: '', 381 | appID: this.appID, 382 | sourceAppID: this.appID, 383 | ...this.generateDetailBody(), 384 | }; 385 | 386 | // Prefer the account API for step 1 (matches app behavior) 387 | let resp; 388 | try { 389 | resp = await axios.post( 390 | '/globalPlatform/api/accountAuth/v1/authByPWDOrOTM', 391 | body, 392 | this.ACCOUNT_AXIOS_OPTIONS(), 393 | ); 394 | } catch (e) { 395 | // Fallback to smartapi host if accountapi ever blocks this 396 | this.debugMode.debug('[AUTH] accountapi failed, falling back to smartapi', String(e)); 397 | resp = await axios.post( 398 | '/globalPlatform/api/accountAuth/v1/authByPWDOrOTM', 399 | body, 400 | this.AXIOS_OPTIONS(), 401 | ); 402 | } 403 | 404 | if (!resp?.data || resp.data.code !== 0 || !resp.data.result) { 405 | this.debugMode.debug('[AUTH] Failed authByPWDOrOTM', JSON.stringify(resp?.data)); 406 | throw new Error('VeSync authentication failed at step 1'); 407 | } 408 | 409 | const { authorizeCode = null, bizToken = null } = resp.data.result; 410 | return { authorizeCode, bizToken }; 411 | } 412 | 413 | private async loginByAuthorizeCode4Vesync(opts: { 414 | userCountryCode: string; 415 | host: string; 416 | authorizeCode?: string | null; 417 | bizToken?: string | null; 418 | regionChange?: 'last_region'; 419 | overrideCountryCode?: string; 420 | currentRegion?: string; 421 | }): Promise { 422 | const { 423 | userCountryCode, 424 | host, 425 | authorizeCode = null, 426 | bizToken = null, 427 | regionChange, 428 | overrideCountryCode, 429 | currentRegion, 430 | } = opts; 431 | 432 | const body: any = { 433 | method: 'loginByAuthorizeCode4Vesync', 434 | authorizeCode, 435 | acceptLanguage: this.LANG, 436 | accountID: '', 437 | clientInfo: this.BRAND, 438 | clientType: 'vesyncApp', 439 | clientVersion: this.FULL_VERSION, 440 | debugMode: false, 441 | emailSubscriptions: false, 442 | osInfo: this.OS.includes('iOS') ? 'iOS' : 'Android', 443 | terminalId: this.terminalId, 444 | timeZone: this.TIMEZONE, 445 | token: '', 446 | userCountryCode: overrideCountryCode || userCountryCode, 447 | ...(regionChange ? { regionChange } : {}), 448 | ...(currentRegion ? { region: String(currentRegion).toUpperCase() } : {}), 449 | appID: this.appID, 450 | sourceAppID: this.appID, 451 | ...this.generateDetailBody(), 452 | }; 453 | 454 | if (bizToken) { 455 | body.bizToken = bizToken; 456 | body.authorizeCode = null; 457 | } 458 | 459 | try { 460 | const resp = await axios.post( 461 | '/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 462 | body, 463 | { baseURL: host, timeout: this.config.options?.apiTimeout || 15000 }, 464 | ); 465 | return resp?.data; 466 | } catch (e) { 467 | this.debugMode.debug('[LOGIN STEP 2] network error', String(e)); 468 | return undefined; 469 | } 470 | } 471 | 472 | public async getDevices(): Promise { 473 | return lock.acquire('api-call', async () => { 474 | if (!this.api) { 475 | this.log.error('The user is not logged in!'); 476 | return []; 477 | } 478 | 479 | const response = await this.api.post('cloud/v2/deviceManaged/devices', { 480 | method: 'devices', 481 | pageNo: 1, 482 | pageSize: 1000, 483 | ...this.generateDetailBody(), 484 | ...this.generateBody(true), 485 | }); 486 | 487 | if (!response?.data) { 488 | this.debugMode.debug( 489 | '[GET DEVICES]', 490 | 'No response data!! JSON:', 491 | JSON.stringify(response?.data), 492 | ); 493 | return []; 494 | } 495 | 496 | if (!Array.isArray(response.data?.result?.list)) { 497 | this.debugMode.debug( 498 | '[GET DEVICES]', 499 | 'No list found!! JSON:', 500 | JSON.stringify(response.data), 501 | ); 502 | return []; 503 | } 504 | 505 | const { list } = response.data.result ?? { list: [] }; 506 | 507 | this.debugMode.debug( 508 | '[GET DEVICES]', 509 | 'Device List -> JSON:', 510 | JSON.stringify(list), 511 | ); 512 | 513 | const devices = list 514 | .filter( 515 | ({ deviceType, type }) => 516 | !!deviceTypes.find(({ isValid }) => isValid(deviceType)) && 517 | type === 'wifi-air', 518 | ) 519 | .map(VeSyncFan.fromResponse(this)); 520 | 521 | return devices; 522 | }); 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@aashutoshrathi/word-wrap@^1.2.3": 6 | version "1.2.6" 7 | resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" 8 | integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== 9 | 10 | "@cspotcode/source-map-support@^0.8.0": 11 | version "0.8.1" 12 | resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" 13 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 14 | dependencies: 15 | "@jridgewell/trace-mapping" "0.3.9" 16 | 17 | "@eslint-community/eslint-utils@^4.2.0": 18 | version "4.4.0" 19 | resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" 20 | integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== 21 | dependencies: 22 | eslint-visitor-keys "^3.3.0" 23 | 24 | "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": 25 | version "4.10.0" 26 | resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz" 27 | integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== 28 | 29 | "@eslint/eslintrc@^2.1.4": 30 | version "2.1.4" 31 | resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" 32 | integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== 33 | dependencies: 34 | ajv "^6.12.4" 35 | debug "^4.3.2" 36 | espree "^9.6.0" 37 | globals "^13.19.0" 38 | ignore "^5.2.0" 39 | import-fresh "^3.2.1" 40 | js-yaml "^4.1.0" 41 | minimatch "^3.1.2" 42 | strip-json-comments "^3.1.1" 43 | 44 | "@eslint/js@8.56.0": 45 | version "8.56.0" 46 | resolved "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz" 47 | integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== 48 | 49 | "@homebridge/ciao@^1.1.5": 50 | version "1.1.8" 51 | resolved "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.8.tgz" 52 | integrity sha512-Atn8+vwYtfI/J6nYCOVm4uVBAmiQO4rPi0umVbh766cf/OsVxQ+Qedbo9lxIf15iDsMbBlDV7T1wATdHqI5lXw== 53 | dependencies: 54 | debug "^4.3.4" 55 | fast-deep-equal "^3.1.3" 56 | source-map-support "^0.5.21" 57 | tslib "^2.6.2" 58 | 59 | "@homebridge/dbus-native@^0.5.1": 60 | version "0.5.1" 61 | resolved "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.1.tgz" 62 | integrity sha512-7xXz3R1W/kcbfQOGp32y4K7etqtowICR1vpx8j85KwPYXbNQrgiZ3zcwDYgDGBWq3FD9xzsW7h4YWJ4vTR2seQ== 63 | dependencies: 64 | "@homebridge/long" "^5.2.1" 65 | "@homebridge/put" "~0.0.8" 66 | event-stream "^4.0.0" 67 | hexy "^0.2.10" 68 | minimist "^1.2.6" 69 | safe-buffer "^5.1.1" 70 | xml2js "^0.5.0" 71 | 72 | "@homebridge/long@^5.2.1": 73 | version "5.2.1" 74 | resolved "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz" 75 | integrity sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA== 76 | 77 | "@homebridge/put@~0.0.8": 78 | version "0.0.8" 79 | resolved "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz" 80 | integrity sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA== 81 | 82 | "@humanwhocodes/config-array@^0.11.13": 83 | version "0.11.14" 84 | resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" 85 | integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== 86 | dependencies: 87 | "@humanwhocodes/object-schema" "^2.0.2" 88 | debug "^4.3.1" 89 | minimatch "^3.0.5" 90 | 91 | "@humanwhocodes/module-importer@^1.0.1": 92 | version "1.0.1" 93 | resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" 94 | integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== 95 | 96 | "@humanwhocodes/object-schema@^2.0.2": 97 | version "2.0.2" 98 | resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz" 99 | integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== 100 | 101 | "@jridgewell/resolve-uri@^3.0.3": 102 | version "3.1.1" 103 | resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" 104 | integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== 105 | 106 | "@jridgewell/sourcemap-codec@^1.4.10": 107 | version "1.4.15" 108 | resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" 109 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 110 | 111 | "@jridgewell/trace-mapping@0.3.9": 112 | version "0.3.9" 113 | resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" 114 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 115 | dependencies: 116 | "@jridgewell/resolve-uri" "^3.0.3" 117 | "@jridgewell/sourcemap-codec" "^1.4.10" 118 | 119 | "@leichtgewicht/ip-codec@^2.0.1": 120 | version "2.0.4" 121 | resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" 122 | integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== 123 | 124 | "@nodelib/fs.scandir@2.1.5": 125 | version "2.1.5" 126 | resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" 127 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 128 | dependencies: 129 | "@nodelib/fs.stat" "2.0.5" 130 | run-parallel "^1.1.9" 131 | 132 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 133 | version "2.0.5" 134 | resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" 135 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 136 | 137 | "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": 138 | version "1.2.8" 139 | resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" 140 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 141 | dependencies: 142 | "@nodelib/fs.scandir" "2.1.5" 143 | fastq "^1.6.0" 144 | 145 | "@pkgr/core@^0.1.0": 146 | version "0.1.0" 147 | resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz" 148 | integrity sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ== 149 | 150 | "@tsconfig/node10@^1.0.7": 151 | version "1.0.9" 152 | resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" 153 | integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== 154 | 155 | "@tsconfig/node12@^1.0.7": 156 | version "1.0.11" 157 | resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" 158 | integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== 159 | 160 | "@tsconfig/node14@^1.0.0": 161 | version "1.0.3" 162 | resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" 163 | integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 164 | 165 | "@tsconfig/node16@^1.0.2": 166 | version "1.0.4" 167 | resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" 168 | integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== 169 | 170 | "@types/async-lock@^1.1.3": 171 | version "1.4.2" 172 | resolved "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz" 173 | integrity sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw== 174 | 175 | "@types/json-schema@^7.0.9": 176 | version "7.0.15" 177 | resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" 178 | integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== 179 | 180 | "@types/node@^16.11.7": 181 | version "16.18.71" 182 | resolved "https://registry.npmjs.org/@types/node/-/node-16.18.71.tgz" 183 | integrity sha512-ARO+458bNJQeNEFuPyT6W+q9ULotmsQzhV3XABsFSxEvRMUYENcBsNAHWYPlahU+UHa5gCVwyKT1Z3f1Wwr26Q== 184 | 185 | "@types/semver@^7.3.12": 186 | version "7.5.6" 187 | resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz" 188 | integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== 189 | 190 | "@typescript-eslint/eslint-plugin@^5.3.1": 191 | version "5.62.0" 192 | resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz" 193 | integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== 194 | dependencies: 195 | "@eslint-community/regexpp" "^4.4.0" 196 | "@typescript-eslint/scope-manager" "5.62.0" 197 | "@typescript-eslint/type-utils" "5.62.0" 198 | "@typescript-eslint/utils" "5.62.0" 199 | debug "^4.3.4" 200 | graphemer "^1.4.0" 201 | ignore "^5.2.0" 202 | natural-compare-lite "^1.4.0" 203 | semver "^7.3.7" 204 | tsutils "^3.21.0" 205 | 206 | "@typescript-eslint/parser@^5.3.1": 207 | version "5.62.0" 208 | resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" 209 | integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== 210 | dependencies: 211 | "@typescript-eslint/scope-manager" "5.62.0" 212 | "@typescript-eslint/types" "5.62.0" 213 | "@typescript-eslint/typescript-estree" "5.62.0" 214 | debug "^4.3.4" 215 | 216 | "@typescript-eslint/scope-manager@5.62.0": 217 | version "5.62.0" 218 | resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz" 219 | integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== 220 | dependencies: 221 | "@typescript-eslint/types" "5.62.0" 222 | "@typescript-eslint/visitor-keys" "5.62.0" 223 | 224 | "@typescript-eslint/type-utils@5.62.0": 225 | version "5.62.0" 226 | resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz" 227 | integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== 228 | dependencies: 229 | "@typescript-eslint/typescript-estree" "5.62.0" 230 | "@typescript-eslint/utils" "5.62.0" 231 | debug "^4.3.4" 232 | tsutils "^3.21.0" 233 | 234 | "@typescript-eslint/types@5.62.0": 235 | version "5.62.0" 236 | resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz" 237 | integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== 238 | 239 | "@typescript-eslint/typescript-estree@5.62.0": 240 | version "5.62.0" 241 | resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" 242 | integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== 243 | dependencies: 244 | "@typescript-eslint/types" "5.62.0" 245 | "@typescript-eslint/visitor-keys" "5.62.0" 246 | debug "^4.3.4" 247 | globby "^11.1.0" 248 | is-glob "^4.0.3" 249 | semver "^7.3.7" 250 | tsutils "^3.21.0" 251 | 252 | "@typescript-eslint/utils@5.62.0": 253 | version "5.62.0" 254 | resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" 255 | integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== 256 | dependencies: 257 | "@eslint-community/eslint-utils" "^4.2.0" 258 | "@types/json-schema" "^7.0.9" 259 | "@types/semver" "^7.3.12" 260 | "@typescript-eslint/scope-manager" "5.62.0" 261 | "@typescript-eslint/types" "5.62.0" 262 | "@typescript-eslint/typescript-estree" "5.62.0" 263 | eslint-scope "^5.1.1" 264 | semver "^7.3.7" 265 | 266 | "@typescript-eslint/visitor-keys@5.62.0": 267 | version "5.62.0" 268 | resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz" 269 | integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== 270 | dependencies: 271 | "@typescript-eslint/types" "5.62.0" 272 | eslint-visitor-keys "^3.3.0" 273 | 274 | "@ungap/structured-clone@^1.2.0": 275 | version "1.2.0" 276 | resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" 277 | integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== 278 | 279 | abbrev@1: 280 | version "1.1.1" 281 | resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" 282 | integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== 283 | 284 | acorn-jsx@^5.3.2: 285 | version "5.3.2" 286 | resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" 287 | integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== 288 | 289 | acorn-walk@^8.1.1: 290 | version "8.3.2" 291 | resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz" 292 | integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== 293 | 294 | acorn@^8.4.1, acorn@^8.9.0: 295 | version "8.11.3" 296 | resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" 297 | integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== 298 | 299 | ajv@^6.12.4: 300 | version "6.12.6" 301 | resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" 302 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 303 | dependencies: 304 | fast-deep-equal "^3.1.1" 305 | fast-json-stable-stringify "^2.0.0" 306 | json-schema-traverse "^0.4.1" 307 | uri-js "^4.2.2" 308 | 309 | ansi-regex@^5.0.1: 310 | version "5.0.1" 311 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 312 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 313 | 314 | ansi-styles@^4.1.0: 315 | version "4.3.0" 316 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" 317 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 318 | dependencies: 319 | color-convert "^2.0.1" 320 | 321 | anymatch@~3.1.2: 322 | version "3.1.3" 323 | resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" 324 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== 325 | dependencies: 326 | normalize-path "^3.0.0" 327 | picomatch "^2.0.4" 328 | 329 | arg@^4.1.0: 330 | version "4.1.3" 331 | resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" 332 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 333 | 334 | argparse@^2.0.1: 335 | version "2.0.1" 336 | resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" 337 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 338 | 339 | array-buffer-byte-length@^1.0.0: 340 | version "1.0.0" 341 | resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz" 342 | integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== 343 | dependencies: 344 | call-bind "^1.0.2" 345 | is-array-buffer "^3.0.1" 346 | 347 | array-flatten@^2.1.2: 348 | version "2.1.2" 349 | resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" 350 | integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== 351 | 352 | array-union@^2.1.0: 353 | version "2.1.0" 354 | resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" 355 | integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== 356 | 357 | async-lock@^1.3.0: 358 | version "1.4.1" 359 | resolved "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz" 360 | integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== 361 | 362 | available-typed-arrays@^1.0.5: 363 | version "1.0.5" 364 | resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" 365 | integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== 366 | 367 | axios@^0.24.0: 368 | version "0.24.0" 369 | resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz" 370 | integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== 371 | dependencies: 372 | follow-redirects "^1.14.4" 373 | 374 | balanced-match@^1.0.0: 375 | version "1.0.2" 376 | resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" 377 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 378 | 379 | binary-extensions@^2.0.0: 380 | version "2.2.0" 381 | resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" 382 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 383 | 384 | bonjour-hap@~3.6.4: 385 | version "3.6.4" 386 | resolved "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz" 387 | integrity sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng== 388 | dependencies: 389 | array-flatten "^2.1.2" 390 | deep-equal "^2.0.5" 391 | ip "^1.1.8" 392 | multicast-dns "^7.2.5" 393 | multicast-dns-service-types "^1.1.0" 394 | 395 | brace-expansion@^1.1.7: 396 | version "1.1.11" 397 | resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" 398 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 399 | dependencies: 400 | balanced-match "^1.0.0" 401 | concat-map "0.0.1" 402 | 403 | braces@^3.0.2, braces@~3.0.2: 404 | version "3.0.2" 405 | resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" 406 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 407 | dependencies: 408 | fill-range "^7.0.1" 409 | 410 | buffer-from@^1.0.0: 411 | version "1.1.2" 412 | resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" 413 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 414 | 415 | call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: 416 | version "1.0.5" 417 | resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz" 418 | integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== 419 | dependencies: 420 | function-bind "^1.1.2" 421 | get-intrinsic "^1.2.1" 422 | set-function-length "^1.1.1" 423 | 424 | callsites@^3.0.0: 425 | version "3.1.0" 426 | resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" 427 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 428 | 429 | chalk@^4.0.0, chalk@^4.1.2: 430 | version "4.1.2" 431 | resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" 432 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 433 | dependencies: 434 | ansi-styles "^4.1.0" 435 | supports-color "^7.1.0" 436 | 437 | chokidar@^3.5.2: 438 | version "3.5.3" 439 | resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" 440 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 441 | dependencies: 442 | anymatch "~3.1.2" 443 | braces "~3.0.2" 444 | glob-parent "~5.1.2" 445 | is-binary-path "~2.1.0" 446 | is-glob "~4.0.1" 447 | normalize-path "~3.0.0" 448 | readdirp "~3.6.0" 449 | optionalDependencies: 450 | fsevents "~2.3.2" 451 | 452 | color-convert@^2.0.1: 453 | version "2.0.1" 454 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 455 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 456 | dependencies: 457 | color-name "~1.1.4" 458 | 459 | color-name@~1.1.4: 460 | version "1.1.4" 461 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" 462 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 463 | 464 | commander@^7.2.0: 465 | version "7.2.0" 466 | resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" 467 | integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== 468 | 469 | concat-map@0.0.1: 470 | version "0.0.1" 471 | resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" 472 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 473 | 474 | create-require@^1.1.0: 475 | version "1.1.1" 476 | resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" 477 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 478 | 479 | cross-spawn@^7.0.2: 480 | version "7.0.3" 481 | resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" 482 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 483 | dependencies: 484 | path-key "^3.1.0" 485 | shebang-command "^2.0.0" 486 | which "^2.0.1" 487 | 488 | debug@^3.2.7: 489 | version "3.2.7" 490 | resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" 491 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 492 | dependencies: 493 | ms "^2.1.1" 494 | 495 | debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: 496 | version "4.3.4" 497 | resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" 498 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 499 | dependencies: 500 | ms "2.1.2" 501 | 502 | deep-equal@^2.0.5: 503 | version "2.2.3" 504 | resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz" 505 | integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== 506 | dependencies: 507 | array-buffer-byte-length "^1.0.0" 508 | call-bind "^1.0.5" 509 | es-get-iterator "^1.1.3" 510 | get-intrinsic "^1.2.2" 511 | is-arguments "^1.1.1" 512 | is-array-buffer "^3.0.2" 513 | is-date-object "^1.0.5" 514 | is-regex "^1.1.4" 515 | is-shared-array-buffer "^1.0.2" 516 | isarray "^2.0.5" 517 | object-is "^1.1.5" 518 | object-keys "^1.1.1" 519 | object.assign "^4.1.4" 520 | regexp.prototype.flags "^1.5.1" 521 | side-channel "^1.0.4" 522 | which-boxed-primitive "^1.0.2" 523 | which-collection "^1.0.1" 524 | which-typed-array "^1.1.13" 525 | 526 | deep-is@^0.1.3: 527 | version "0.1.4" 528 | resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" 529 | integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== 530 | 531 | define-data-property@^1.0.1, define-data-property@^1.1.1: 532 | version "1.1.1" 533 | resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz" 534 | integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== 535 | dependencies: 536 | get-intrinsic "^1.2.1" 537 | gopd "^1.0.1" 538 | has-property-descriptors "^1.0.0" 539 | 540 | define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: 541 | version "1.2.1" 542 | resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" 543 | integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== 544 | dependencies: 545 | define-data-property "^1.0.1" 546 | has-property-descriptors "^1.0.0" 547 | object-keys "^1.1.1" 548 | 549 | diff@^4.0.1: 550 | version "4.0.2" 551 | resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" 552 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 553 | 554 | dir-glob@^3.0.1: 555 | version "3.0.1" 556 | resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" 557 | integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== 558 | dependencies: 559 | path-type "^4.0.0" 560 | 561 | dns-packet@^5.2.2: 562 | version "5.6.1" 563 | resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" 564 | integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== 565 | dependencies: 566 | "@leichtgewicht/ip-codec" "^2.0.1" 567 | 568 | doctrine@^3.0.0: 569 | version "3.0.0" 570 | resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" 571 | integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== 572 | dependencies: 573 | esutils "^2.0.2" 574 | 575 | duplexer@^0.1.1, duplexer@~0.1.1: 576 | version "0.1.2" 577 | resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" 578 | integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== 579 | 580 | es-get-iterator@^1.1.3: 581 | version "1.1.3" 582 | resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz" 583 | integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== 584 | dependencies: 585 | call-bind "^1.0.2" 586 | get-intrinsic "^1.1.3" 587 | has-symbols "^1.0.3" 588 | is-arguments "^1.1.1" 589 | is-map "^2.0.2" 590 | is-set "^2.0.2" 591 | is-string "^1.0.7" 592 | isarray "^2.0.5" 593 | stop-iteration-iterator "^1.0.0" 594 | 595 | escape-string-regexp@^4.0.0: 596 | version "4.0.0" 597 | resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" 598 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 599 | 600 | eslint-config-prettier@^9.1.0: 601 | version "9.1.0" 602 | resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" 603 | integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== 604 | 605 | eslint-plugin-prettier@^5.1.2: 606 | version "5.1.3" 607 | resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz" 608 | integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== 609 | dependencies: 610 | prettier-linter-helpers "^1.0.0" 611 | synckit "^0.8.6" 612 | 613 | eslint-plugin-unused-imports@^2.0.0: 614 | version "2.0.0" 615 | resolved "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz" 616 | integrity sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A== 617 | dependencies: 618 | eslint-rule-composer "^0.3.0" 619 | 620 | eslint-rule-composer@^0.3.0: 621 | version "0.3.0" 622 | resolved "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz" 623 | integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== 624 | 625 | eslint-scope@^5.1.1: 626 | version "5.1.1" 627 | resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" 628 | integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== 629 | dependencies: 630 | esrecurse "^4.3.0" 631 | estraverse "^4.1.1" 632 | 633 | eslint-scope@^7.2.2: 634 | version "7.2.2" 635 | resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" 636 | integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== 637 | dependencies: 638 | esrecurse "^4.3.0" 639 | estraverse "^5.2.0" 640 | 641 | eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: 642 | version "3.4.3" 643 | resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" 644 | integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== 645 | 646 | eslint@^8.56.0: 647 | version "8.56.0" 648 | resolved "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz" 649 | integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== 650 | dependencies: 651 | "@eslint-community/eslint-utils" "^4.2.0" 652 | "@eslint-community/regexpp" "^4.6.1" 653 | "@eslint/eslintrc" "^2.1.4" 654 | "@eslint/js" "8.56.0" 655 | "@humanwhocodes/config-array" "^0.11.13" 656 | "@humanwhocodes/module-importer" "^1.0.1" 657 | "@nodelib/fs.walk" "^1.2.8" 658 | "@ungap/structured-clone" "^1.2.0" 659 | ajv "^6.12.4" 660 | chalk "^4.0.0" 661 | cross-spawn "^7.0.2" 662 | debug "^4.3.2" 663 | doctrine "^3.0.0" 664 | escape-string-regexp "^4.0.0" 665 | eslint-scope "^7.2.2" 666 | eslint-visitor-keys "^3.4.3" 667 | espree "^9.6.1" 668 | esquery "^1.4.2" 669 | esutils "^2.0.2" 670 | fast-deep-equal "^3.1.3" 671 | file-entry-cache "^6.0.1" 672 | find-up "^5.0.0" 673 | glob-parent "^6.0.2" 674 | globals "^13.19.0" 675 | graphemer "^1.4.0" 676 | ignore "^5.2.0" 677 | imurmurhash "^0.1.4" 678 | is-glob "^4.0.0" 679 | is-path-inside "^3.0.3" 680 | js-yaml "^4.1.0" 681 | json-stable-stringify-without-jsonify "^1.0.1" 682 | levn "^0.4.1" 683 | lodash.merge "^4.6.2" 684 | minimatch "^3.1.2" 685 | natural-compare "^1.4.0" 686 | optionator "^0.9.3" 687 | strip-ansi "^6.0.1" 688 | text-table "^0.2.0" 689 | 690 | espree@^9.6.0, espree@^9.6.1: 691 | version "9.6.1" 692 | resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" 693 | integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== 694 | dependencies: 695 | acorn "^8.9.0" 696 | acorn-jsx "^5.3.2" 697 | eslint-visitor-keys "^3.4.1" 698 | 699 | esquery@^1.4.2: 700 | version "1.5.0" 701 | resolved "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz" 702 | integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== 703 | dependencies: 704 | estraverse "^5.1.0" 705 | 706 | esrecurse@^4.3.0: 707 | version "4.3.0" 708 | resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" 709 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 710 | dependencies: 711 | estraverse "^5.2.0" 712 | 713 | estraverse@^4.1.1: 714 | version "4.3.0" 715 | resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" 716 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 717 | 718 | estraverse@^5.1.0, estraverse@^5.2.0: 719 | version "5.3.0" 720 | resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" 721 | integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== 722 | 723 | esutils@^2.0.2: 724 | version "2.0.3" 725 | resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" 726 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 727 | 728 | event-stream@^4.0.0: 729 | version "4.0.1" 730 | resolved "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz" 731 | integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== 732 | dependencies: 733 | duplexer "^0.1.1" 734 | from "^0.1.7" 735 | map-stream "0.0.7" 736 | pause-stream "^0.0.11" 737 | split "^1.0.1" 738 | stream-combiner "^0.2.2" 739 | through "^2.3.8" 740 | 741 | fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 742 | version "3.1.3" 743 | resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" 744 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 745 | 746 | fast-diff@^1.1.2: 747 | version "1.3.0" 748 | resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" 749 | integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== 750 | 751 | fast-glob@^3.2.9: 752 | version "3.3.2" 753 | resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" 754 | integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== 755 | dependencies: 756 | "@nodelib/fs.stat" "^2.0.2" 757 | "@nodelib/fs.walk" "^1.2.3" 758 | glob-parent "^5.1.2" 759 | merge2 "^1.3.0" 760 | micromatch "^4.0.4" 761 | 762 | fast-json-stable-stringify@^2.0.0: 763 | version "2.1.0" 764 | resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" 765 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 766 | 767 | fast-levenshtein@^2.0.6: 768 | version "2.0.6" 769 | resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" 770 | integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== 771 | 772 | fast-srp-hap@~2.0.4: 773 | version "2.0.4" 774 | resolved "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz" 775 | integrity sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ== 776 | 777 | fastq@^1.6.0: 778 | version "1.16.0" 779 | resolved "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz" 780 | integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA== 781 | dependencies: 782 | reusify "^1.0.4" 783 | 784 | file-entry-cache@^6.0.1: 785 | version "6.0.1" 786 | resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" 787 | integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== 788 | dependencies: 789 | flat-cache "^3.0.4" 790 | 791 | fill-range@^7.0.1: 792 | version "7.0.1" 793 | resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" 794 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 795 | dependencies: 796 | to-regex-range "^5.0.1" 797 | 798 | find-up@^5.0.0: 799 | version "5.0.0" 800 | resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" 801 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 802 | dependencies: 803 | locate-path "^6.0.0" 804 | path-exists "^4.0.0" 805 | 806 | flat-cache@^3.0.4: 807 | version "3.2.0" 808 | resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" 809 | integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== 810 | dependencies: 811 | flatted "^3.2.9" 812 | keyv "^4.5.3" 813 | rimraf "^3.0.2" 814 | 815 | flatted@^3.2.9: 816 | version "3.2.9" 817 | resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" 818 | integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== 819 | 820 | follow-redirects@^1.14.4: 821 | version "1.15.5" 822 | resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz" 823 | integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== 824 | 825 | for-each@^0.3.3: 826 | version "0.3.3" 827 | resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" 828 | integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== 829 | dependencies: 830 | is-callable "^1.1.3" 831 | 832 | from@^0.1.7: 833 | version "0.1.7" 834 | resolved "https://registry.npmjs.org/from/-/from-0.1.7.tgz" 835 | integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== 836 | 837 | fs-extra@^10.1.0: 838 | version "10.1.0" 839 | resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" 840 | integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== 841 | dependencies: 842 | graceful-fs "^4.2.0" 843 | jsonfile "^6.0.1" 844 | universalify "^2.0.0" 845 | 846 | fs.realpath@^1.0.0: 847 | version "1.0.0" 848 | resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" 849 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 850 | 851 | fsevents@~2.3.2: 852 | version "2.3.3" 853 | resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" 854 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 855 | 856 | function-bind@^1.1.2: 857 | version "1.1.2" 858 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" 859 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 860 | 861 | functions-have-names@^1.2.3: 862 | version "1.2.3" 863 | resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" 864 | integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== 865 | 866 | futoin-hkdf@~1.4.3: 867 | version "1.4.3" 868 | resolved "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz" 869 | integrity sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w== 870 | 871 | get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: 872 | version "1.2.2" 873 | resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz" 874 | integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== 875 | dependencies: 876 | function-bind "^1.1.2" 877 | has-proto "^1.0.1" 878 | has-symbols "^1.0.3" 879 | hasown "^2.0.0" 880 | 881 | glob-parent@^5.1.2, glob-parent@~5.1.2: 882 | version "5.1.2" 883 | resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" 884 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 885 | dependencies: 886 | is-glob "^4.0.1" 887 | 888 | glob-parent@^6.0.2: 889 | version "6.0.2" 890 | resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" 891 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 892 | dependencies: 893 | is-glob "^4.0.3" 894 | 895 | glob@^7.1.3: 896 | version "7.2.3" 897 | resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" 898 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 899 | dependencies: 900 | fs.realpath "^1.0.0" 901 | inflight "^1.0.4" 902 | inherits "2" 903 | minimatch "^3.1.1" 904 | once "^1.3.0" 905 | path-is-absolute "^1.0.0" 906 | 907 | globals@^13.19.0: 908 | version "13.24.0" 909 | resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" 910 | integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== 911 | dependencies: 912 | type-fest "^0.20.2" 913 | 914 | globby@^11.1.0: 915 | version "11.1.0" 916 | resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" 917 | integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== 918 | dependencies: 919 | array-union "^2.1.0" 920 | dir-glob "^3.0.1" 921 | fast-glob "^3.2.9" 922 | ignore "^5.2.0" 923 | merge2 "^1.4.1" 924 | slash "^3.0.0" 925 | 926 | gopd@^1.0.1: 927 | version "1.0.1" 928 | resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" 929 | integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== 930 | dependencies: 931 | get-intrinsic "^1.1.3" 932 | 933 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 934 | version "4.2.11" 935 | resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" 936 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== 937 | 938 | graphemer@^1.4.0: 939 | version "1.4.0" 940 | resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" 941 | integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== 942 | 943 | hap-nodejs@~0.11.1: 944 | version "0.11.1" 945 | resolved "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.11.1.tgz" 946 | integrity sha512-hJuGyjng2jlzhZsviWCldaokT7l7BE3iGmWdlE6DNmQFDTmiBN3deNksAZ2nt7qp5jYEv7ZUvW7WBZqJsLh3ww== 947 | dependencies: 948 | "@homebridge/ciao" "^1.1.5" 949 | "@homebridge/dbus-native" "^0.5.1" 950 | bonjour-hap "~3.6.4" 951 | debug "^4.3.4" 952 | fast-srp-hap "~2.0.4" 953 | futoin-hkdf "~1.4.3" 954 | node-persist "^0.0.11" 955 | source-map-support "^0.5.21" 956 | tslib "^2.4.0" 957 | tweetnacl "^1.0.3" 958 | 959 | has-bigints@^1.0.1: 960 | version "1.0.2" 961 | resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" 962 | integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== 963 | 964 | has-flag@^3.0.0: 965 | version "3.0.0" 966 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" 967 | integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 968 | 969 | has-flag@^4.0.0: 970 | version "4.0.0" 971 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" 972 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 973 | 974 | has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: 975 | version "1.0.1" 976 | resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz" 977 | integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== 978 | dependencies: 979 | get-intrinsic "^1.2.2" 980 | 981 | has-proto@^1.0.1: 982 | version "1.0.1" 983 | resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" 984 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== 985 | 986 | has-symbols@^1.0.2, has-symbols@^1.0.3: 987 | version "1.0.3" 988 | resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" 989 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 990 | 991 | has-tostringtag@^1.0.0: 992 | version "1.0.0" 993 | resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" 994 | integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== 995 | dependencies: 996 | has-symbols "^1.0.2" 997 | 998 | hasown@^2.0.0: 999 | version "2.0.0" 1000 | resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" 1001 | integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== 1002 | dependencies: 1003 | function-bind "^1.1.2" 1004 | 1005 | hexy@^0.2.10: 1006 | version "0.2.11" 1007 | resolved "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz" 1008 | integrity sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A== 1009 | 1010 | homebridge@^1.3.5: 1011 | version "1.7.0" 1012 | resolved "https://registry.npmjs.org/homebridge/-/homebridge-1.7.0.tgz" 1013 | integrity sha512-2QikXnmpnFe2s33Q8TeYE5+sXyKHUZ+9l5WfDmpuupHdct6H/G6b6z3HCj+2rlMRKKY5ElLv5XtLoxOcafnL0g== 1014 | dependencies: 1015 | chalk "^4.1.2" 1016 | commander "^7.2.0" 1017 | fs-extra "^10.1.0" 1018 | hap-nodejs "~0.11.1" 1019 | qrcode-terminal "^0.12.0" 1020 | semver "^7.5.4" 1021 | source-map-support "^0.5.21" 1022 | 1023 | ignore-by-default@^1.0.1: 1024 | version "1.0.1" 1025 | resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" 1026 | integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== 1027 | 1028 | ignore@^5.2.0: 1029 | version "5.3.0" 1030 | resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz" 1031 | integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== 1032 | 1033 | import-fresh@^3.2.1: 1034 | version "3.3.0" 1035 | resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" 1036 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 1037 | dependencies: 1038 | parent-module "^1.0.0" 1039 | resolve-from "^4.0.0" 1040 | 1041 | imurmurhash@^0.1.4: 1042 | version "0.1.4" 1043 | resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" 1044 | integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== 1045 | 1046 | inflight@^1.0.4: 1047 | version "1.0.6" 1048 | resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" 1049 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 1050 | dependencies: 1051 | once "^1.3.0" 1052 | wrappy "1" 1053 | 1054 | inherits@2: 1055 | version "2.0.4" 1056 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 1057 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 1058 | 1059 | internal-slot@^1.0.4: 1060 | version "1.0.6" 1061 | resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz" 1062 | integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== 1063 | dependencies: 1064 | get-intrinsic "^1.2.2" 1065 | hasown "^2.0.0" 1066 | side-channel "^1.0.4" 1067 | 1068 | ip@^1.1.8: 1069 | version "1.1.8" 1070 | resolved "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz" 1071 | integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== 1072 | 1073 | is-arguments@^1.1.1: 1074 | version "1.1.1" 1075 | resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" 1076 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== 1077 | dependencies: 1078 | call-bind "^1.0.2" 1079 | has-tostringtag "^1.0.0" 1080 | 1081 | is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: 1082 | version "3.0.2" 1083 | resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz" 1084 | integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== 1085 | dependencies: 1086 | call-bind "^1.0.2" 1087 | get-intrinsic "^1.2.0" 1088 | is-typed-array "^1.1.10" 1089 | 1090 | is-bigint@^1.0.1: 1091 | version "1.0.4" 1092 | resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" 1093 | integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== 1094 | dependencies: 1095 | has-bigints "^1.0.1" 1096 | 1097 | is-binary-path@~2.1.0: 1098 | version "2.1.0" 1099 | resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" 1100 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 1101 | dependencies: 1102 | binary-extensions "^2.0.0" 1103 | 1104 | is-boolean-object@^1.1.0: 1105 | version "1.1.2" 1106 | resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" 1107 | integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== 1108 | dependencies: 1109 | call-bind "^1.0.2" 1110 | has-tostringtag "^1.0.0" 1111 | 1112 | is-callable@^1.1.3: 1113 | version "1.2.7" 1114 | resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" 1115 | integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== 1116 | 1117 | is-date-object@^1.0.5: 1118 | version "1.0.5" 1119 | resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" 1120 | integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== 1121 | dependencies: 1122 | has-tostringtag "^1.0.0" 1123 | 1124 | is-extglob@^2.1.1: 1125 | version "2.1.1" 1126 | resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" 1127 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 1128 | 1129 | is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: 1130 | version "4.0.3" 1131 | resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" 1132 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 1133 | dependencies: 1134 | is-extglob "^2.1.1" 1135 | 1136 | is-map@^2.0.1, is-map@^2.0.2: 1137 | version "2.0.2" 1138 | resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz" 1139 | integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== 1140 | 1141 | is-number-object@^1.0.4: 1142 | version "1.0.7" 1143 | resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" 1144 | integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== 1145 | dependencies: 1146 | has-tostringtag "^1.0.0" 1147 | 1148 | is-number@^7.0.0: 1149 | version "7.0.0" 1150 | resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" 1151 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 1152 | 1153 | is-path-inside@^3.0.3: 1154 | version "3.0.3" 1155 | resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" 1156 | integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== 1157 | 1158 | is-regex@^1.1.4: 1159 | version "1.1.4" 1160 | resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" 1161 | integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== 1162 | dependencies: 1163 | call-bind "^1.0.2" 1164 | has-tostringtag "^1.0.0" 1165 | 1166 | is-set@^2.0.1, is-set@^2.0.2: 1167 | version "2.0.2" 1168 | resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz" 1169 | integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== 1170 | 1171 | is-shared-array-buffer@^1.0.2: 1172 | version "1.0.2" 1173 | resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" 1174 | integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== 1175 | dependencies: 1176 | call-bind "^1.0.2" 1177 | 1178 | is-string@^1.0.5, is-string@^1.0.7: 1179 | version "1.0.7" 1180 | resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" 1181 | integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== 1182 | dependencies: 1183 | has-tostringtag "^1.0.0" 1184 | 1185 | is-symbol@^1.0.3: 1186 | version "1.0.4" 1187 | resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" 1188 | integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== 1189 | dependencies: 1190 | has-symbols "^1.0.2" 1191 | 1192 | is-typed-array@^1.1.10: 1193 | version "1.1.12" 1194 | resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz" 1195 | integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== 1196 | dependencies: 1197 | which-typed-array "^1.1.11" 1198 | 1199 | is-weakmap@^2.0.1: 1200 | version "2.0.1" 1201 | resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz" 1202 | integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== 1203 | 1204 | is-weakset@^2.0.1: 1205 | version "2.0.2" 1206 | resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz" 1207 | integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== 1208 | dependencies: 1209 | call-bind "^1.0.2" 1210 | get-intrinsic "^1.1.1" 1211 | 1212 | isarray@^2.0.5: 1213 | version "2.0.5" 1214 | resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" 1215 | integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== 1216 | 1217 | isexe@^2.0.0: 1218 | version "2.0.0" 1219 | resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" 1220 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 1221 | 1222 | js-yaml@^4.1.0: 1223 | version "4.1.0" 1224 | resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" 1225 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 1226 | dependencies: 1227 | argparse "^2.0.1" 1228 | 1229 | json-buffer@3.0.1: 1230 | version "3.0.1" 1231 | resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" 1232 | integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 1233 | 1234 | json-schema-traverse@^0.4.1: 1235 | version "0.4.1" 1236 | resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" 1237 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 1238 | 1239 | json-stable-stringify-without-jsonify@^1.0.1: 1240 | version "1.0.1" 1241 | resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" 1242 | integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== 1243 | 1244 | jsonfile@^6.0.1: 1245 | version "6.1.0" 1246 | resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" 1247 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== 1248 | dependencies: 1249 | universalify "^2.0.0" 1250 | optionalDependencies: 1251 | graceful-fs "^4.1.6" 1252 | 1253 | keyv@^4.5.3: 1254 | version "4.5.4" 1255 | resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" 1256 | integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== 1257 | dependencies: 1258 | json-buffer "3.0.1" 1259 | 1260 | levn@^0.4.1: 1261 | version "0.4.1" 1262 | resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" 1263 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 1264 | dependencies: 1265 | prelude-ls "^1.2.1" 1266 | type-check "~0.4.0" 1267 | 1268 | locate-path@^6.0.0: 1269 | version "6.0.0" 1270 | resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" 1271 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 1272 | dependencies: 1273 | p-locate "^5.0.0" 1274 | 1275 | lodash.merge@^4.6.2: 1276 | version "4.6.2" 1277 | resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" 1278 | integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== 1279 | 1280 | lru-cache@^6.0.0: 1281 | version "6.0.0" 1282 | resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" 1283 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 1284 | dependencies: 1285 | yallist "^4.0.0" 1286 | 1287 | make-error@^1.1.1: 1288 | version "1.3.6" 1289 | resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" 1290 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 1291 | 1292 | map-stream@0.0.7: 1293 | version "0.0.7" 1294 | resolved "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz" 1295 | integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== 1296 | 1297 | merge2@^1.3.0, merge2@^1.4.1: 1298 | version "1.4.1" 1299 | resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" 1300 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 1301 | 1302 | micromatch@^4.0.4: 1303 | version "4.0.5" 1304 | resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" 1305 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 1306 | dependencies: 1307 | braces "^3.0.2" 1308 | picomatch "^2.3.1" 1309 | 1310 | minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: 1311 | version "3.1.2" 1312 | resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" 1313 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 1314 | dependencies: 1315 | brace-expansion "^1.1.7" 1316 | 1317 | minimist@^1.2.6: 1318 | version "1.2.8" 1319 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" 1320 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 1321 | 1322 | mkdirp@~0.5.1: 1323 | version "0.5.6" 1324 | resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" 1325 | integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== 1326 | dependencies: 1327 | minimist "^1.2.6" 1328 | 1329 | ms@2.1.2, ms@^2.1.1: 1330 | version "2.1.2" 1331 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" 1332 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 1333 | 1334 | multicast-dns-service-types@^1.1.0: 1335 | version "1.1.0" 1336 | resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz" 1337 | integrity sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ== 1338 | 1339 | multicast-dns@^7.2.5: 1340 | version "7.2.5" 1341 | resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" 1342 | integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== 1343 | dependencies: 1344 | dns-packet "^5.2.2" 1345 | thunky "^1.0.2" 1346 | 1347 | natural-compare-lite@^1.4.0: 1348 | version "1.4.0" 1349 | resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" 1350 | integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== 1351 | 1352 | natural-compare@^1.4.0: 1353 | version "1.4.0" 1354 | resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" 1355 | integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== 1356 | 1357 | node-persist@^0.0.11: 1358 | version "0.0.11" 1359 | resolved "https://registry.npmjs.org/node-persist/-/node-persist-0.0.11.tgz" 1360 | integrity sha512-J3EPzQDgPxPBID7TqHSd5KkpTULFqJUvYDoISfOWg9EihpeVCH3b6YQeDeubzVuc4e6+aiVmkz2sdkWI4K+ghA== 1361 | dependencies: 1362 | mkdirp "~0.5.1" 1363 | q "~1.1.1" 1364 | 1365 | nodemon@^2.0.13: 1366 | version "2.0.22" 1367 | resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz" 1368 | integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== 1369 | dependencies: 1370 | chokidar "^3.5.2" 1371 | debug "^3.2.7" 1372 | ignore-by-default "^1.0.1" 1373 | minimatch "^3.1.2" 1374 | pstree.remy "^1.1.8" 1375 | semver "^5.7.1" 1376 | simple-update-notifier "^1.0.7" 1377 | supports-color "^5.5.0" 1378 | touch "^3.1.0" 1379 | undefsafe "^2.0.5" 1380 | 1381 | nopt@~1.0.10: 1382 | version "1.0.10" 1383 | resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" 1384 | integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== 1385 | dependencies: 1386 | abbrev "1" 1387 | 1388 | normalize-path@^3.0.0, normalize-path@~3.0.0: 1389 | version "3.0.0" 1390 | resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" 1391 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 1392 | 1393 | object-inspect@^1.9.0: 1394 | version "1.13.1" 1395 | resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" 1396 | integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== 1397 | 1398 | object-is@^1.1.5: 1399 | version "1.1.5" 1400 | resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" 1401 | integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== 1402 | dependencies: 1403 | call-bind "^1.0.2" 1404 | define-properties "^1.1.3" 1405 | 1406 | object-keys@^1.1.1: 1407 | version "1.1.1" 1408 | resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" 1409 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 1410 | 1411 | object.assign@^4.1.4: 1412 | version "4.1.5" 1413 | resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz" 1414 | integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== 1415 | dependencies: 1416 | call-bind "^1.0.5" 1417 | define-properties "^1.2.1" 1418 | has-symbols "^1.0.3" 1419 | object-keys "^1.1.1" 1420 | 1421 | once@^1.3.0: 1422 | version "1.4.0" 1423 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" 1424 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 1425 | dependencies: 1426 | wrappy "1" 1427 | 1428 | optionator@^0.9.3: 1429 | version "0.9.3" 1430 | resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz" 1431 | integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== 1432 | dependencies: 1433 | "@aashutoshrathi/word-wrap" "^1.2.3" 1434 | deep-is "^0.1.3" 1435 | fast-levenshtein "^2.0.6" 1436 | levn "^0.4.1" 1437 | prelude-ls "^1.2.1" 1438 | type-check "^0.4.0" 1439 | 1440 | p-limit@^3.0.2: 1441 | version "3.1.0" 1442 | resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" 1443 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 1444 | dependencies: 1445 | yocto-queue "^0.1.0" 1446 | 1447 | p-locate@^5.0.0: 1448 | version "5.0.0" 1449 | resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" 1450 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 1451 | dependencies: 1452 | p-limit "^3.0.2" 1453 | 1454 | parent-module@^1.0.0: 1455 | version "1.0.1" 1456 | resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" 1457 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 1458 | dependencies: 1459 | callsites "^3.0.0" 1460 | 1461 | path-exists@^4.0.0: 1462 | version "4.0.0" 1463 | resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" 1464 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 1465 | 1466 | path-is-absolute@^1.0.0: 1467 | version "1.0.1" 1468 | resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" 1469 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 1470 | 1471 | path-key@^3.1.0: 1472 | version "3.1.1" 1473 | resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" 1474 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 1475 | 1476 | path-type@^4.0.0: 1477 | version "4.0.0" 1478 | resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" 1479 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 1480 | 1481 | pause-stream@^0.0.11: 1482 | version "0.0.11" 1483 | resolved "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz" 1484 | integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== 1485 | dependencies: 1486 | through "~2.3" 1487 | 1488 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: 1489 | version "2.3.1" 1490 | resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" 1491 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 1492 | 1493 | prelude-ls@^1.2.1: 1494 | version "1.2.1" 1495 | resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" 1496 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 1497 | 1498 | prettier-linter-helpers@^1.0.0: 1499 | version "1.0.0" 1500 | resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" 1501 | integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== 1502 | dependencies: 1503 | fast-diff "^1.1.2" 1504 | 1505 | prettier@^3.1.1: 1506 | version "3.2.2" 1507 | resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.2.tgz" 1508 | integrity sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A== 1509 | 1510 | pstree.remy@^1.1.8: 1511 | version "1.1.8" 1512 | resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" 1513 | integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== 1514 | 1515 | punycode@^2.1.0: 1516 | version "2.3.1" 1517 | resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" 1518 | integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== 1519 | 1520 | q@~1.1.1: 1521 | version "1.1.2" 1522 | resolved "https://registry.npmjs.org/q/-/q-1.1.2.tgz" 1523 | integrity sha512-ROtylwux7Vkc4C07oKE/ReigUmb33kVoLtcR4SJ1QVqwaZkBEDL3vX4/kwFzIERQ5PfCl0XafbU8u2YUhyGgVA== 1524 | 1525 | qrcode-terminal@^0.12.0: 1526 | version "0.12.0" 1527 | resolved "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz" 1528 | integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== 1529 | 1530 | queue-microtask@^1.2.2: 1531 | version "1.2.3" 1532 | resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" 1533 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 1534 | 1535 | readdirp@~3.6.0: 1536 | version "3.6.0" 1537 | resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" 1538 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 1539 | dependencies: 1540 | picomatch "^2.2.1" 1541 | 1542 | regexp.prototype.flags@^1.5.1: 1543 | version "1.5.1" 1544 | resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz" 1545 | integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== 1546 | dependencies: 1547 | call-bind "^1.0.2" 1548 | define-properties "^1.2.0" 1549 | set-function-name "^2.0.0" 1550 | 1551 | resolve-from@^4.0.0: 1552 | version "4.0.0" 1553 | resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" 1554 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 1555 | 1556 | reusify@^1.0.4: 1557 | version "1.0.4" 1558 | resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" 1559 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 1560 | 1561 | rimraf@^3.0.2: 1562 | version "3.0.2" 1563 | resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" 1564 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 1565 | dependencies: 1566 | glob "^7.1.3" 1567 | 1568 | run-parallel@^1.1.9: 1569 | version "1.2.0" 1570 | resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" 1571 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 1572 | dependencies: 1573 | queue-microtask "^1.2.2" 1574 | 1575 | safe-buffer@^5.1.1: 1576 | version "5.2.1" 1577 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" 1578 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1579 | 1580 | sax@>=0.6.0: 1581 | version "1.3.0" 1582 | resolved "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz" 1583 | integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== 1584 | 1585 | semver@^5.7.1: 1586 | version "5.7.2" 1587 | resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" 1588 | integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== 1589 | 1590 | semver@^7.3.7, semver@^7.5.4: 1591 | version "7.5.4" 1592 | resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" 1593 | integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== 1594 | dependencies: 1595 | lru-cache "^6.0.0" 1596 | 1597 | semver@~7.0.0: 1598 | version "7.0.0" 1599 | resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" 1600 | integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== 1601 | 1602 | set-function-length@^1.1.1: 1603 | version "1.2.0" 1604 | resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz" 1605 | integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== 1606 | dependencies: 1607 | define-data-property "^1.1.1" 1608 | function-bind "^1.1.2" 1609 | get-intrinsic "^1.2.2" 1610 | gopd "^1.0.1" 1611 | has-property-descriptors "^1.0.1" 1612 | 1613 | set-function-name@^2.0.0: 1614 | version "2.0.1" 1615 | resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz" 1616 | integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== 1617 | dependencies: 1618 | define-data-property "^1.0.1" 1619 | functions-have-names "^1.2.3" 1620 | has-property-descriptors "^1.0.0" 1621 | 1622 | shebang-command@^2.0.0: 1623 | version "2.0.0" 1624 | resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" 1625 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 1626 | dependencies: 1627 | shebang-regex "^3.0.0" 1628 | 1629 | shebang-regex@^3.0.0: 1630 | version "3.0.0" 1631 | resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" 1632 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 1633 | 1634 | side-channel@^1.0.4: 1635 | version "1.0.4" 1636 | resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" 1637 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 1638 | dependencies: 1639 | call-bind "^1.0.0" 1640 | get-intrinsic "^1.0.2" 1641 | object-inspect "^1.9.0" 1642 | 1643 | simple-update-notifier@^1.0.7: 1644 | version "1.1.0" 1645 | resolved "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz" 1646 | integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== 1647 | dependencies: 1648 | semver "~7.0.0" 1649 | 1650 | slash@^3.0.0: 1651 | version "3.0.0" 1652 | resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" 1653 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 1654 | 1655 | source-map-support@^0.5.21: 1656 | version "0.5.21" 1657 | resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" 1658 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 1659 | dependencies: 1660 | buffer-from "^1.0.0" 1661 | source-map "^0.6.0" 1662 | 1663 | source-map@^0.6.0: 1664 | version "0.6.1" 1665 | resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" 1666 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 1667 | 1668 | split@^1.0.1: 1669 | version "1.0.1" 1670 | resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz" 1671 | integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== 1672 | dependencies: 1673 | through "2" 1674 | 1675 | stop-iteration-iterator@^1.0.0: 1676 | version "1.0.0" 1677 | resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" 1678 | integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== 1679 | dependencies: 1680 | internal-slot "^1.0.4" 1681 | 1682 | stream-combiner@^0.2.2: 1683 | version "0.2.2" 1684 | resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz" 1685 | integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== 1686 | dependencies: 1687 | duplexer "~0.1.1" 1688 | through "~2.3.4" 1689 | 1690 | strip-ansi@^6.0.1: 1691 | version "6.0.1" 1692 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 1693 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 1694 | dependencies: 1695 | ansi-regex "^5.0.1" 1696 | 1697 | strip-json-comments@^3.1.1: 1698 | version "3.1.1" 1699 | resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" 1700 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 1701 | 1702 | supports-color@^5.5.0: 1703 | version "5.5.0" 1704 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" 1705 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1706 | dependencies: 1707 | has-flag "^3.0.0" 1708 | 1709 | supports-color@^7.1.0: 1710 | version "7.2.0" 1711 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 1712 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 1713 | dependencies: 1714 | has-flag "^4.0.0" 1715 | 1716 | synckit@^0.8.6: 1717 | version "0.8.8" 1718 | resolved "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz" 1719 | integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== 1720 | dependencies: 1721 | "@pkgr/core" "^0.1.0" 1722 | tslib "^2.6.2" 1723 | 1724 | text-table@^0.2.0: 1725 | version "0.2.0" 1726 | resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" 1727 | integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== 1728 | 1729 | through@2, through@^2.3.8, through@~2.3, through@~2.3.4: 1730 | version "2.3.8" 1731 | resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" 1732 | integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== 1733 | 1734 | thunky@^1.0.2: 1735 | version "1.1.0" 1736 | resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" 1737 | integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== 1738 | 1739 | to-regex-range@^5.0.1: 1740 | version "5.0.1" 1741 | resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" 1742 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1743 | dependencies: 1744 | is-number "^7.0.0" 1745 | 1746 | touch@^3.1.0: 1747 | version "3.1.0" 1748 | resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz" 1749 | integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== 1750 | dependencies: 1751 | nopt "~1.0.10" 1752 | 1753 | ts-node@^10.3.0: 1754 | version "10.9.2" 1755 | resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" 1756 | integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== 1757 | dependencies: 1758 | "@cspotcode/source-map-support" "^0.8.0" 1759 | "@tsconfig/node10" "^1.0.7" 1760 | "@tsconfig/node12" "^1.0.7" 1761 | "@tsconfig/node14" "^1.0.0" 1762 | "@tsconfig/node16" "^1.0.2" 1763 | acorn "^8.4.1" 1764 | acorn-walk "^8.1.1" 1765 | arg "^4.1.0" 1766 | create-require "^1.1.0" 1767 | diff "^4.0.1" 1768 | make-error "^1.1.1" 1769 | v8-compile-cache-lib "^3.0.1" 1770 | yn "3.1.1" 1771 | 1772 | tslib@^1.8.1: 1773 | version "1.14.1" 1774 | resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" 1775 | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 1776 | 1777 | tslib@^2.4.0, tslib@^2.6.2: 1778 | version "2.6.2" 1779 | resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" 1780 | integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== 1781 | 1782 | tsutils@^3.21.0: 1783 | version "3.21.0" 1784 | resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" 1785 | integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== 1786 | dependencies: 1787 | tslib "^1.8.1" 1788 | 1789 | tweetnacl@^1.0.3: 1790 | version "1.0.3" 1791 | resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" 1792 | integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== 1793 | 1794 | type-check@^0.4.0, type-check@~0.4.0: 1795 | version "0.4.0" 1796 | resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" 1797 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 1798 | dependencies: 1799 | prelude-ls "^1.2.1" 1800 | 1801 | type-fest@^0.20.2: 1802 | version "0.20.2" 1803 | resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" 1804 | integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== 1805 | 1806 | typescript@^4.4.4: 1807 | version "4.9.5" 1808 | resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" 1809 | integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== 1810 | 1811 | undefsafe@^2.0.5: 1812 | version "2.0.5" 1813 | resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" 1814 | integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== 1815 | 1816 | universalify@^2.0.0: 1817 | version "2.0.1" 1818 | resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" 1819 | integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== 1820 | 1821 | uri-js@^4.2.2: 1822 | version "4.4.1" 1823 | resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" 1824 | integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== 1825 | dependencies: 1826 | punycode "^2.1.0" 1827 | 1828 | uuid@^9.0.0: 1829 | version "9.0.1" 1830 | resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" 1831 | integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== 1832 | 1833 | v8-compile-cache-lib@^3.0.1: 1834 | version "3.0.1" 1835 | resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" 1836 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 1837 | 1838 | which-boxed-primitive@^1.0.2: 1839 | version "1.0.2" 1840 | resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" 1841 | integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== 1842 | dependencies: 1843 | is-bigint "^1.0.1" 1844 | is-boolean-object "^1.1.0" 1845 | is-number-object "^1.0.4" 1846 | is-string "^1.0.5" 1847 | is-symbol "^1.0.3" 1848 | 1849 | which-collection@^1.0.1: 1850 | version "1.0.1" 1851 | resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz" 1852 | integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== 1853 | dependencies: 1854 | is-map "^2.0.1" 1855 | is-set "^2.0.1" 1856 | is-weakmap "^2.0.1" 1857 | is-weakset "^2.0.1" 1858 | 1859 | which-typed-array@^1.1.11, which-typed-array@^1.1.13: 1860 | version "1.1.13" 1861 | resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz" 1862 | integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== 1863 | dependencies: 1864 | available-typed-arrays "^1.0.5" 1865 | call-bind "^1.0.4" 1866 | for-each "^0.3.3" 1867 | gopd "^1.0.1" 1868 | has-tostringtag "^1.0.0" 1869 | 1870 | which@^2.0.1: 1871 | version "2.0.2" 1872 | resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" 1873 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1874 | dependencies: 1875 | isexe "^2.0.0" 1876 | 1877 | wrappy@1: 1878 | version "1.0.2" 1879 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" 1880 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1881 | 1882 | xml2js@^0.5.0: 1883 | version "0.5.0" 1884 | resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz" 1885 | integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== 1886 | dependencies: 1887 | sax ">=0.6.0" 1888 | xmlbuilder "~11.0.0" 1889 | 1890 | xmlbuilder@~11.0.0: 1891 | version "11.0.1" 1892 | resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" 1893 | integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== 1894 | 1895 | yallist@^4.0.0: 1896 | version "4.0.0" 1897 | resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" 1898 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1899 | 1900 | yarn@^1.22.22: 1901 | version "1.22.22" 1902 | resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz" 1903 | integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg== 1904 | 1905 | yn@3.1.1: 1906 | version "3.1.1" 1907 | resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" 1908 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 1909 | 1910 | yocto-queue@^0.1.0: 1911 | version "0.1.0" 1912 | resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" 1913 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 1914 | --------------------------------------------------------------------------------