diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7a7d9e6..0454331b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: if: ${{ github.repository == 'OpenWonderLabs/node-switchbot' }} uses: OpenWonderLabs/.github/.github/workflows/discord-webhooks.yml@latest with: - title: "Node-SwitchBot Beta Release" + title: "Node-SwitchBot Release" description: | Version `v${{ needs.publish.outputs.NPM_VERSION }}` url: "https://github.com/homebridge/camera-utils/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" diff --git a/BLE.md b/BLE.md index 4bd7633f..2a9861c3 100644 --- a/BLE.md +++ b/BLE.md @@ -80,7 +80,7 @@ const switchBotBLE = new SwitchBotBLE() try { await switchBotBLE.startScan() } catch (e: any) { - console.error(`Failed to start BLE scanning. Error:${e}`) + console.error(`Failed to start BLE scanning, Error: ${e.message ?? e}`) } ``` @@ -230,7 +230,7 @@ switchBotBLE.onadvertisement = async (ad: any) => { try { this.bleEventHandler[ad.address]?.(ad.serviceData) } catch (e: any) { - await this.errorLog(`Failed to handle BLE event. Error:${e}`) + await this.errorLog(`Failed to handle BLE event, Error: ${e.message ?? e}`) } } ``` @@ -246,7 +246,7 @@ try { switchBotBLE.stopScan() console.log('Stopped BLE scanning to close listening.') } catch (e: any) { - console.error(`Failed to stop BLE scanning, error:${e.message}`) + console.error(`Failed to stop BLE scanning, Error: ${e.message ?? e}`) } ``` @@ -731,6 +731,7 @@ Actually, the `WoSmartLock ` is an object inherited from the [`SwitchbotDevice`] The `setKey()` method initialises the key information required for encrypted communication with the SmartLock This must be set before any control commands are sent to the device. To obtain the key information you will need to use an external tool - see [`pySwitchbot`](https://github.com/Danielhiversen/pySwitchbot/tree/master?tab=readme-ov-file#obtaining-locks-encryption-key) project for an example script. +Or, use [`switchbot-get-encryption-key`](https://www.npmjs.com/package/switchbot-get-encryption-key) npm script. | Property | Type | Description | | :-------------- | :----- | :----------------------------------------------------------------------------------------------- | diff --git a/CHANGELOG.md b/CHANGELOG.md index f72e127b..1c6ffdac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) +## [3.1.0](https://github.com/OpenWonderLabs/node-switchbot/releases/tag/v3.1.0) (2024-10-11) + +### What's Changed +- Added support for emitting logs from this module from the `SwitchBotBLE` class. +- Housekeeping and update dependencies + +**Full Changelog**: https://github.com/OpenWonderLabs/node-switchbot/compare/v3.0.1...v3.1.0 + ## [3.0.1](https://github.com/OpenWonderLabs/node-switchbot/releases/tag/v3.0.1) (2024-10-05) ### What's Changed @@ -16,6 +24,7 @@ All notable changes to this project will be documented in this file. This projec #### ⚠️ Breaking Changes - Have added OpenAPI Functionality into `node-switchbot`, so now it is able to handle both APIs with just one module. - There are now two different Classes `SwitchBotBLE` and `SwitchBotOpenAPI` that can be used interact with the two different APIs + - `SwitchBotOpenApi` support emitting logs from this module. - You will need to update your current setup from`SwitchBot` to `SwitchBotBLE` to be able to interact with the BLE API - Updated the documents to explain both the `BLE` and the `OpenAPI` Functionality within `node-switchbot` diff --git a/OpenAPI.md b/OpenAPI.md index 39c3894c..3869513c 100644 --- a/OpenAPI.md +++ b/OpenAPI.md @@ -47,8 +47,8 @@ async function getDevices() { try { const devices = await switchBotAPI.getDevices() console.log('Devices:', devices) - } catch (error) { - console.error('Error getting devices:', error) + } catch (e: any) { + console.error(`failed to get devices, Error: ${e.message ?? e}`) } } diff --git a/docs/media/BLE.md b/docs/media/BLE.md index 4bd7633f..72a57d92 100644 --- a/docs/media/BLE.md +++ b/docs/media/BLE.md @@ -80,7 +80,7 @@ const switchBotBLE = new SwitchBotBLE() try { await switchBotBLE.startScan() } catch (e: any) { - console.error(`Failed to start BLE scanning. Error:${e}`) + console.error(`Failed to start BLE scanning, Error: ${e.message ?? e}`) } ``` @@ -230,7 +230,7 @@ switchBotBLE.onadvertisement = async (ad: any) => { try { this.bleEventHandler[ad.address]?.(ad.serviceData) } catch (e: any) { - await this.errorLog(`Failed to handle BLE event. Error:${e}`) + await this.errorLog(`Failed to handle BLE event, Error: ${e.message ?? e}`) } } ``` @@ -246,7 +246,7 @@ try { switchBotBLE.stopScan() console.log('Stopped BLE scanning to close listening.') } catch (e: any) { - console.error(`Failed to stop BLE scanning, error:${e.message}`) + console.error(`Failed to stop BLE scanning, Error: ${e.message ?? e}`) } ``` diff --git a/package-lock.json b/package-lock.json index 5adbd7d3..646856bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "node-switchbot", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-switchbot", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "dependencies": { "@stoprocent/noble": "^1.15.1", "async-mutex": "^0.5.0", - "undici": "^6.19.8" + "undici": "^6.20.0" }, "devDependencies": { "@antfu/eslint-config": "^3.7.3", @@ -20,7 +20,7 @@ "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.13", "@types/mdast": "^4.0.4", - "@types/node": "^22.7.4", + "@types/node": "^22.7.5", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/source-map-support": "^0.5.10", @@ -33,8 +33,8 @@ "shx": "^0.3.4", "sinon": "^19.0.2", "ts-node": "^10.9.2", - "typedoc": "^0.26.8", - "typescript": "^5.6.2", + "typedoc": "^0.26.9", + "typescript": "^5.6.3", "vitest": "^2.1.2" }, "engines": { @@ -204,9 +204,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", - "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "dev": true, "license": "MIT", "engines": { @@ -214,9 +214,9 @@ } }, "node_modules/@babel/core": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", - "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "dev": true, "license": "MIT", "dependencies": { @@ -226,10 +226,10 @@ "@babel/helper-compilation-targets": "^7.25.7", "@babel/helper-module-transforms": "^7.25.7", "@babel/helpers": "^7.25.7", - "@babel/parser": "^7.25.7", + "@babel/parser": "^7.25.8", "@babel/template": "^7.25.7", "@babel/traverse": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -493,13 +493,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -792,9 +792,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, "license": "MIT", "dependencies": { @@ -2763,58 +2763,58 @@ "optional": true }, "node_modules/@shikijs/core": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.21.0.tgz", - "integrity": "sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-javascript": "1.21.0", - "@shikijs/engine-oniguruma": "1.21.0", - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.3" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.21.0.tgz", - "integrity": "sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "oniguruma-to-js": "0.4.3" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.21.0.tgz", - "integrity": "sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2" + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" } }, "node_modules/@shikijs/types": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.21.0.tgz", - "integrity": "sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.2.tgz", - "integrity": "sha512-TMp15K+GGYrWlZM8+Lnj9EaHEFmOen0WJBrfa17hF7taDOYthuPPV0GWzfd/9iMij0akS/8Yw2ikquH7uVi/fg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", "dev": true, "license": "MIT" }, @@ -3140,9 +3140,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3229,17 +3229,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz", - "integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/type-utils": "8.8.0", - "@typescript-eslint/utils": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3263,16 +3263,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz", - "integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4" }, "engines": { @@ -3292,14 +3292,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz", - "integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0" + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3310,14 +3310,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz", - "integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.0", - "@typescript-eslint/utils": "8.8.0", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3335,9 +3335,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz", - "integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", "dev": true, "license": "MIT", "engines": { @@ -3349,14 +3349,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz", - "integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/visitor-keys": "8.8.0", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3378,16 +3378,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz", - "integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.0", - "@typescript-eslint/types": "8.8.0", - "@typescript-eslint/typescript-estree": "8.8.0" + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3401,13 +3401,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz", - "integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.0", + "@typescript-eslint/types": "8.8.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3472,9 +3472,9 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.6.tgz", - "integrity": "sha512-sFuAnD9iycnOzLHHhNCULXeb6ejOSo5Lcq/ODhdlUOoUrXkQPcVeYqXurZMA3neOqf+wNCQ6YuU1zyoYH/WEcg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.7.tgz", + "integrity": "sha512-pTWGW3y6lH2ukCuuffpan6kFxG6nIuoesbhMiQxskyQMRcCN5t9SXsKrNHvEw3p8wcCsgJoRqFZVkOTn6TjclA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3486,6 +3486,9 @@ "peerDependenciesMeta": { "typescript": { "optional": true + }, + "vitest": { + "optional": true } } }, @@ -3614,45 +3617,45 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz", - "integrity": "sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.11", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz", - "integrity": "sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-core": "3.5.11", - "@vue/shared": "3.5.11" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz", - "integrity": "sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.11", - "@vue/compiler-dom": "3.5.11", - "@vue/compiler-ssr": "3.5.11", - "@vue/shared": "3.5.11", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.47", @@ -3660,21 +3663,21 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz", - "integrity": "sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.11", - "@vue/shared": "3.5.11" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/shared": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz", - "integrity": "sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", "dev": true, "license": "MIT", "peer": true @@ -4197,9 +4200,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", "dev": true, "funding": [ { @@ -4481,9 +4484,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, "license": "MIT" }, @@ -4731,9 +4734,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.32", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", - "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", + "version": "1.5.36", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz", + "integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==", "dev": true, "license": "ISC" }, @@ -5153,9 +5156,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "50.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz", - "integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==", + "version": "50.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.2.tgz", + "integrity": "sha512-TjgZocG53N3a84PdCFGqVMWLWwDitOUuKjlJftwTu/iTiD7N/Q2Q3eEy/Q4GfJqpM4rTJCkzUYWQfol6RZNDcA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5248,9 +5251,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.10.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.3.tgz", - "integrity": "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw==", + "version": "17.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.11.1.tgz", + "integrity": "sha512-93IUD82N6tIEgjztVI/l3ElHtC2wTa9boJHrD8iN+NyDxjxz/daZUZKfkedjBZNdg6EqDk4irybUsiPwDqXAEA==", "dev": true, "license": "MIT", "dependencies": { @@ -5415,9 +5418,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", - "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", + "integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5956,16 +5959,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -6082,9 +6075,9 @@ } }, "node_modules/globals": { - "version": "15.10.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.10.0.tgz", - "integrity": "sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, "license": "MIT", "engines": { @@ -7555,14 +7548,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -7582,9 +7572,9 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "dependencies": { @@ -8598,16 +8588,16 @@ } }, "node_modules/mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" } }, "node_modules/ms": { @@ -8680,9 +8670,9 @@ } }, "node_modules/node-addon-api": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.1.0.tgz", - "integrity": "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz", + "integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -9007,9 +8997,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz", - "integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", "dev": true, "license": "MIT" }, @@ -9314,14 +9304,14 @@ } }, "node_modules/pkg-types": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", - "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", "dev": true, "license": "MIT", "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.1", + "confbox": "^0.1.8", + "mlly": "^1.7.2", "pathe": "^1.1.2" } }, @@ -10064,17 +10054,17 @@ } }, "node_modules/shiki": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.21.0.tgz", - "integrity": "sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "1.21.0", - "@shikijs/engine-javascript": "1.21.0", - "@shikijs/engine-oniguruma": "1.21.0", - "@shikijs/types": "1.21.0", - "@shikijs/vscode-textmate": "^9.2.2", + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", "@types/hast": "^3.0.4" } }, @@ -10475,9 +10465,9 @@ } }, "node_modules/synckit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", - "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "license": "MIT", "dependencies": { @@ -10791,9 +10781,9 @@ } }, "node_modules/typedoc": { - "version": "0.26.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.8.tgz", - "integrity": "sha512-QBF0BMbnNeUc6U7pRHY7Jb8pjhmiNWZNQT8LU6uk9qP9t3goP9bJptdlNqMC0wBB2w9sQrxjZt835bpRSSq1LA==", + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.9.tgz", + "integrity": "sha512-Rc7QpWL7EtmrT8yxV0GmhOR6xHgFnnhphbD9Suti3fz3um7ZOrou6q/g9d6+zC5PssTLZmjaW4Upmzv8T1rCcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10814,9 +10804,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10849,9 +10839,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.0.tgz", + "integrity": "sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 7627352b..a2636ee1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-switchbot", "type": "module", - "version": "3.0.1", + "version": "3.1.0", "description": "The node-switchbot is a Node.js module which allows you to control your Switchbot Devices through Bluetooth (BLE).", "author": "OpenWonderLabs (https://github.com/OpenWonderLabs)", "license": "MIT", @@ -50,7 +50,7 @@ "dependencies": { "@stoprocent/noble": "^1.15.1", "async-mutex": "^0.5.0", - "undici": "^6.19.8" + "undici": "^6.20.0" }, "optionalDependencies": { "@stoprocent/bluetooth-hci-socket": "^1.4.1" @@ -62,7 +62,7 @@ "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.13", "@types/mdast": "^4.0.4", - "@types/node": "^22.7.4", + "@types/node": "^22.7.5", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", "@types/source-map-support": "^0.5.10", @@ -75,8 +75,8 @@ "shx": "^0.3.4", "sinon": "^19.0.2", "ts-node": "^10.9.2", - "typedoc": "^0.26.8", - "typescript": "^5.6.2", + "typedoc": "^0.26.9", + "typescript": "^5.6.3", "vitest": "^2.1.2" } } diff --git a/src/advertising.ts b/src/advertising.ts index 4de43480..3131ddfb 100644 --- a/src/advertising.ts +++ b/src/advertising.ts @@ -60,7 +60,7 @@ export class Advertising { const model = serviceData.subarray(0, 1).toString('utf8') const sd = await Advertising.parseServiceData(model, serviceData, manufacturerData, emitLog) if (!sd) { - emitLog('debugerror', `[parseAdvertising.${peripheral.id}.${model}] return null, parsed serviceData empty!`) + // emitLog('debugerror', `[parseAdvertising.${peripheral.id}.${model}] return null, parsed serviceData empty!`) return null } @@ -95,7 +95,7 @@ export class Advertising { * @param {Function} emitLog - The function to emit log messages. * @returns {Promise} - The parsed service data. */ - private static async parseServiceData( + public static async parseServiceData( model: string, serviceData: Buffer, manufacturerData: Buffer, diff --git a/src/device.ts b/src/device.ts index ff3c9bf8..21fc56ab 100644 --- a/src/device.ts +++ b/src/device.ts @@ -4,40 +4,33 @@ */ import type * as Noble from '@stoprocent/noble' -import type { Chars, SwitchBotBLEModel, SwitchBotBLEModelName } from './types/types.js' +import type { Chars, SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from './types/types.js' import { Buffer } from 'node:buffer' import { EventEmitter } from 'node:events' import { Advertising } from './advertising.js' import { parameterChecker } from './parameter-checker.js' -import { - CHAR_UUID_DEVICE, - CHAR_UUID_NOTIFY, - CHAR_UUID_WRITE, - READ_TIMEOUT_MSEC, - SERV_UUID_PRIMARY, - WRITE_TIMEOUT_MSEC, -} from './settings.js' -import { SwitchBotBLE } from './switchbot-ble.js' +import { CHAR_UUID_DEVICE, CHAR_UUID_NOTIFY, CHAR_UUID_WRITE, READ_TIMEOUT_MSEC, SERV_UUID_PRIMARY, WRITE_TIMEOUT_MSEC } from './settings.js' /** * Represents a Switchbot Device. */ export class SwitchbotDevice extends EventEmitter { - private _noble: typeof Noble - private _peripheral: Noble.Peripheral - private _characteristics: Chars | null = null - private _id!: string - private _address!: string - private _model!: SwitchBotBLEModel - private _modelName!: SwitchBotBLEModelName - private _explicitly = false - private _connected = false - private onnotify_internal: (buf: Buffer) => void = () => {} - - private ondisconnect_internal: () => Promise = async () => {} - private onconnect_internal: () => Promise = async () => {} + [x: string]: any + private noble: typeof Noble + private peripheral: Noble.Peripheral + private characteristics: Chars | null = null + private deviceId!: string + private deviceAddress!: string + private deviceModel!: SwitchBotBLEModel + private deviceModelName!: SwitchBotBLEModelName + private deviceFriendlyName!: SwitchBotBLEModelFriendlyName + private explicitlyConnected = false + private isConnected = false + private onNotify: (buf: Buffer) => void = () => {} + private onDisconnect: () => Promise = async () => {} + private onConnect: () => Promise = async () => {} /** * Initializes a new instance of the SwitchbotDevice class. @@ -46,70 +39,74 @@ export class SwitchbotDevice extends EventEmitter { */ constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { super() - this._peripheral = peripheral - this._noble = noble - - Advertising.parse(peripheral, this.emitLog.bind(this)).then((ad) => { - this._id = ad?.id ?? '' - this._address = ad?.address ?? '' - this._model = ad?.serviceData.model as SwitchBotBLEModel ?? '' - this._modelName = ad?.serviceData.modelName as SwitchBotBLEModelName ?? '' + this.peripheral = peripheral + this.noble = noble + + Advertising.parse(peripheral, this.log.bind(this)).then((ad) => { + this.deviceId = ad?.id ?? '' + this.deviceAddress = ad?.address ?? '' + this.deviceModel = ad?.serviceData.model as SwitchBotBLEModel ?? '' + this.deviceModelName = ad?.serviceData.modelName as SwitchBotBLEModelName ?? '' + this.deviceFriendlyName = ad?.serviceData.modelFriendlyName as SwitchBotBLEModelFriendlyName ?? '' }) } /** - * Emits a log event with the specified log level and message. - * - * @param level - The severity level of the log (e.g., 'info', 'warn', 'error'). - * @param message - The log message to be emitted. + * Logs a message with the specified log level. + * @param level The severity level of the log (e.g., 'info', 'warn', 'error'). + * @param message The log message to be emitted. */ - public async emitLog(level: string, message: string): Promise { + public async log(level: string, message: string): Promise { this.emit('log', { level, message }) } // Getters get id(): string { - return this._id + return this.deviceId } get address(): string { - return this._address + return this.deviceAddress } get model(): SwitchBotBLEModel { - return this._model + return this.deviceModel } get modelName(): SwitchBotBLEModelName { - return this._modelName + return this.deviceModelName + } + + get friendlyName(): SwitchBotBLEModelFriendlyName { + return this.deviceFriendlyName } get connectionState(): string { - return this._connected ? 'connected' : this._peripheral.state + return this.isConnected ? 'connected' : this.peripheral.state } - get onconnect(): () => Promise { - return this.onconnect_internal + get onConnectHandler(): () => Promise { + return this.onConnect } - set onconnect(func: () => Promise) { + set onConnectHandler(func: () => Promise) { if (typeof func !== 'function') { - throw new TypeError('The `onconnect` must be a function that returns a Promise.') + throw new TypeError('The `onConnectHandler` must be a function that returns a Promise.') } - this.onconnect_internal = async () => { + this.onConnect = async () => { await func() } } - get ondisconnect(): () => Promise { - return this.ondisconnect_internal + get onDisconnectHandler(): () => Promise { + return this.onDisconnect } - set ondisconnect(func: () => Promise) { + set onDisconnectHandler(func: () => Promise) { if (typeof func !== 'function') { - throw new TypeError('The `ondisconnect` must be a function that returns a Promise.') + throw new TypeError('The `onDisconnectHandler` must be a function that returns a Promise.') } - this.ondisconnect_internal = async () => { + this.onDisconnect = async () => { await func() } } @@ -119,17 +116,17 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves when the connection is complete. */ async connect(): Promise { - this._explicitly = true - await this.connect_internal() + this.explicitlyConnected = true + await this.internalConnect() } /** * Internal method to handle the connection process. * @returns A Promise that resolves when the connection is complete. */ - private async connect_internal(): Promise { - if (this._noble._state !== 'poweredOn') { - throw new Error(`The Bluetooth status is ${this._noble._state}, not poweredOn.`) + private async internalConnect(): Promise { + if (this.noble._state !== 'poweredOn') { + throw new Error(`The Bluetooth status is ${this.noble._state}, not poweredOn.`) } const state = this.connectionState @@ -140,21 +137,21 @@ export class SwitchbotDevice extends EventEmitter { throw new Error(`Now ${state}. Wait for a few seconds then try again.`) } - this._peripheral.once('connect', async () => { - this._connected = true - await this.onconnect() + this.peripheral.once('connect', async () => { + this.isConnected = true + await this.onConnect() }) - this._peripheral.once('disconnect', async () => { - this._connected = false - this._characteristics = null - this._peripheral.removeAllListeners() - await this.ondisconnect_internal() + this.peripheral.once('disconnect', async () => { + this.isConnected = false + this.characteristics = null + this.peripheral.removeAllListeners() + await this.onDisconnect() }) - await this._peripheral.connectAsync() - this._characteristics = await this.getCharacteristics() - await this.subscribe() + await this.peripheral.connectAsync() + this.characteristics = await this.getCharacteristics() + await this.subscribeToNotify() } /** @@ -200,12 +197,17 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves with the list of services. */ public async discoverServices(): Promise { - const services = await this._peripheral.discoverServicesAsync([]) - const primaryServices = services.filter(s => s.uuid === SERV_UUID_PRIMARY) - if (primaryServices.length === 0) { - throw new Error('No service was found.') + try { + const services = await this.peripheral.discoverServicesAsync([]) + const primaryServices = services.filter(s => s.uuid === SERV_UUID_PRIMARY) + + if (primaryServices.length === 0) { + throw new Error('No service was found.') + } + return primaryServices + } catch (e: any) { + throw new Error(`Failed to discover services, Error: ${e.message ?? e}`) } - return primaryServices } /** @@ -221,21 +223,21 @@ export class SwitchbotDevice extends EventEmitter { * Subscribes to the notify characteristic. * @returns A Promise that resolves when the subscription is complete. */ - private async subscribe(): Promise { - const char = this._characteristics?.notify + private async subscribeToNotify(): Promise { + const char = this.characteristics?.notify if (!char) { throw new Error('No notify characteristic was found.') } await char.subscribeAsync() - char.on('data', this.onnotify_internal) + char.on('data', this.onNotify) } /** * Unsubscribes from the notify characteristic. * @returns A Promise that resolves when the unsubscription is complete. */ - async unsubscribe(): Promise { - const char = this._characteristics?.notify + async unsubscribeFromNotify(): Promise { + const char = this.characteristics?.notify if (!char) { return } @@ -248,8 +250,8 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves when the disconnection is complete. */ async disconnect(): Promise { - this._explicitly = false - const state = this._peripheral.state + this.explicitlyConnected = false + const state = this.peripheral.state if (state === 'disconnected') { return @@ -258,18 +260,18 @@ export class SwitchbotDevice extends EventEmitter { throw new Error(`Now ${state}. Wait for a few seconds then try again.`) } - await this.unsubscribe() - await this._peripheral.disconnectAsync() + await this.unsubscribeFromNotify() + await this.peripheral.disconnectAsync() } /** * Internal method to handle disconnection if not explicitly initiated. * @returns A Promise that resolves when the disconnection is complete. */ - private async disconnect_internal(): Promise { - if (!this._explicitly) { + private async internalDisconnect(): Promise { + if (!this.explicitlyConnected) { await this.disconnect() - this._explicitly = true + this.explicitlyConnected = true } } @@ -278,12 +280,12 @@ export class SwitchbotDevice extends EventEmitter { * @returns A Promise that resolves with the device name. */ async getDeviceName(): Promise { - await this.connect_internal() - if (!this._characteristics?.device) { + await this.internalConnect() + if (!this.characteristics?.device) { throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`) } - const buf = await this.read(this._characteristics.device) - await this.disconnect_internal() + const buf = await this.readCharacteristic(this.characteristics.device) + await this.internalDisconnect() return buf.toString('utf8') } @@ -304,41 +306,41 @@ export class SwitchbotDevice extends EventEmitter { } const buf = Buffer.from(name, 'utf8') - await this.connect_internal() - if (!this._characteristics?.device) { + await this.internalConnect() + if (!this.characteristics?.device) { throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`) } - await this.write(this._characteristics.device, buf) - await this.disconnect_internal() + await this.writeCharacteristic(this.characteristics.device, buf) + await this.internalDisconnect() } /** * Sends a command to the device and awaits a response. - * @param req_buf The command buffer. + * @param reqBuf The command buffer. * @returns A Promise that resolves with the response buffer. */ - async command(req_buf: Buffer): Promise { - if (!Buffer.isBuffer(req_buf)) { + async command(reqBuf: Buffer): Promise { + if (!Buffer.isBuffer(reqBuf)) { throw new TypeError('The specified data is not acceptable for writing.') } - await this.connect_internal() - if (!this._characteristics?.write) { + await this.internalConnect() + if (!this.characteristics?.write) { throw new Error('No characteristics available.') } - await this.write(this._characteristics.write, req_buf) - const res_buf = await this._waitCommandResponseAsync() - await this.disconnect_internal() + await this.writeCharacteristic(this.characteristics.write, reqBuf) + const resBuf = await this.waitForCommandResponse() + await this.internalDisconnect() - return res_buf + return resBuf } /** * Waits for a response from the device after sending a command. * @returns A Promise that resolves with the response buffer. */ - private async _waitCommandResponseAsync(): Promise { + private async waitForCommandResponse(): Promise { const timeout = READ_TIMEOUT_MSEC let timer: NodeJS.Timeout | null = null @@ -347,7 +349,7 @@ export class SwitchbotDevice extends EventEmitter { }) const readPromise = new Promise((resolve) => { - this.onnotify_internal = (buf: Buffer) => { + this.onNotify = (buf: Buffer) => { if (timer) { clearTimeout(timer) } @@ -363,7 +365,7 @@ export class SwitchbotDevice extends EventEmitter { * @param char The characteristic to read from. * @returns A Promise that resolves with the data buffer. */ - private async read(char: Noble.Characteristic): Promise { + private async readCharacteristic(char: Noble.Characteristic): Promise { const timer = setTimeout(() => { throw new Error('READ_TIMEOUT') }, READ_TIMEOUT_MSEC) @@ -384,7 +386,7 @@ export class SwitchbotDevice extends EventEmitter { * @param buf The data buffer. * @returns A Promise that resolves when the write is complete. */ - private async write(char: Noble.Characteristic, buf: Buffer): Promise { + private async writeCharacteristic(char: Noble.Characteristic, buf: Buffer): Promise { const timer = setTimeout(() => { throw new Error('WRITE_TIMEOUT') }, WRITE_TIMEOUT_MSEC) diff --git a/src/device/woblindtilt.ts b/src/device/woblindtilt.ts index fad9b0e2..eb7d8cda 100644 --- a/src/device/woblindtilt.ts +++ b/src/device/woblindtilt.ts @@ -2,6 +2,10 @@ * * woblindtilt.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { blindTiltServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -20,14 +24,14 @@ export class WoBlindTilt extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. * @param {boolean} [reverse] - Whether to reverse the tilt percentage. - * @returns {Promise} - The parsed data object or null if the data is invalid. + * @returns {Promise} - The parsed data object or null if the data is invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, reverse: boolean = false, - ): Promise { + ): Promise { if (![5, 6].includes(manufacturerData.length)) { emitLog('debugerror', `[parseServiceDataForWoBlindTilt] Buffer length ${manufacturerData.length} !== 5 or 6!`) return null @@ -43,7 +47,7 @@ export class WoBlindTilt extends SwitchbotDevice { const sequenceNumber = byte6.readUInt8(0) const battery = serviceData.length > 2 ? byte2 & 0b01111111 : null - return { + const data: blindTiltServiceData = { model: SwitchBotBLEModel.BlindTilt, modelName: SwitchBotBLEModelName.BlindTilt, modelFriendlyName: SwitchBotBLEModelFriendlyName.BlindTilt, @@ -54,6 +58,12 @@ export class WoBlindTilt extends SwitchbotDevice { lightLevel, sequenceNumber, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wobulb.ts b/src/device/wobulb.ts index e503a694..9e93c0e2 100644 --- a/src/device/wobulb.ts +++ b/src/device/wobulb.ts @@ -2,6 +2,10 @@ * * wobulb.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { colorBulbServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -17,19 +21,20 @@ export class WoBulb extends SwitchbotDevice { * @param {Buffer} serviceData - The service data buffer. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, + // eslint-disable-next-line unused-imports/no-unused-vars emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 18) { - emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${serviceData.length} !== 18!`) + // emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${serviceData.length} !== 18!`) return null } if (manufacturerData.length !== 13) { - emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${manufacturerData.length} !== 13!`) + // emitLog('debugerror', `[parseServiceDataForWoBulb] Buffer length ${manufacturerData.length} !== 13!`) return null } @@ -45,23 +50,29 @@ export class WoBulb extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: colorBulbServiceData = { model: SwitchBotBLEModel.ColorBulb, modelName: SwitchBotBLEModelName.ColorBulb, modelFriendlyName: SwitchBotBLEModelFriendlyName.ColorBulb, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) >> 7, + preset: (byte8 & 0b00001000) >> 3, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/woceilinglight.ts b/src/device/woceilinglight.ts index 8cd0c06a..c7e7a322 100644 --- a/src/device/woceilinglight.ts +++ b/src/device/woceilinglight.ts @@ -2,6 +2,10 @@ * * woceilinglight.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { ceilingLightProServiceData, ceilingLightServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -16,12 +20,12 @@ export class WoCeilingLight extends SwitchbotDevice { * Parses the service data for WoCeilingLight. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 13) { emitLog('debugerror', `[parseServiceDataForWoCeilingLight] Buffer length ${manufacturerData.length} !== 13!`) return null @@ -39,35 +43,37 @@ export class WoCeilingLight extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: ceilingLightServiceData = { model: SwitchBotBLEModel.CeilingLight, modelName: SwitchBotBLEModelName.CeilingLight, modelFriendlyName: SwitchBotBLEModelFriendlyName.CeilingLight, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) ? 1 : 0, + preset: (byte8 & 0b00001000) ? 1 : 0, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data } /** * Parses the service data for WoCeilingLight Pro. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_Pro( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 13) { emitLog('debugerror', `[parseServiceDataForWoCeilingLightPro] Buffer length ${manufacturerData.length} !== 13!`) return null @@ -85,23 +91,29 @@ export class WoCeilingLight extends SwitchbotDevice { byte10, ] = manufacturerData - return { + const data: ceilingLightProServiceData = { model: SwitchBotBLEModel.CeilingLightPro, modelName: SwitchBotBLEModelName.CeilingLightPro, modelFriendlyName: SwitchBotBLEModelFriendlyName.CeilingLightPro, - power: byte1, + power: !!byte1, red: byte3, green: byte4, blue: byte5, color_temperature: byte6, state: !!(byte7 & 0b01111111), brightness: byte7 & 0b01111111, - delay: !!(byte8 & 0b10000000), - preset: !!(byte8 & 0b00001000), + delay: (byte8 & 0b10000000) ? 1 : 0, + preset: (byte8 & 0b00001000) ? 1 : 0, color_mode: byte8 & 0b00000111, speed: byte9 & 0b01111111, loop_index: byte10 & 0b11111110, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wocontact.ts b/src/device/wocontact.ts index c00c03d7..eed8af15 100644 --- a/src/device/wocontact.ts +++ b/src/device/wocontact.ts @@ -1,8 +1,12 @@ +import type { Buffer } from 'node:buffer' + /* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * wocontact.ts: Switchbot BLE API registration. */ -import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { contactSensorServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -16,12 +20,12 @@ export class WoContact extends SwitchbotDevice { * Parses the service data for WoContact. * @param {Buffer} serviceData - The service data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 9) { emitLog('debugerror', `[parseServiceDataForWoContact] Buffer length ${serviceData.length} !== 9!`) return null @@ -39,7 +43,7 @@ export class WoContact extends SwitchbotDevice { const button_count = byte8 & 0b00001111 const doorState = hallState === 0 ? 'close' : hallState === 1 ? 'open' : 'timeout no closed' - return { + const data: contactSensorServiceData = { model: SwitchBotBLEModel.ContactSensor, modelName: SwitchBotBLEModelName.ContactSensor, modelFriendlyName: SwitchBotBLEModelFriendlyName.ContactSensor, @@ -52,5 +56,11 @@ export class WoContact extends SwitchbotDevice { button_count, doorState, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/wocurtain.ts b/src/device/wocurtain.ts index 49a5b517..001d4b3a 100644 --- a/src/device/wocurtain.ts +++ b/src/device/wocurtain.ts @@ -2,10 +2,14 @@ * * wocurtain.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { curtain3ServiceData, curtainServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' -import { SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' +import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' /** * Class representing a WoCurtain device. @@ -19,14 +23,14 @@ export class WoCurtain extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. * @param {boolean} [reverse] - Whether to reverse the position. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, reverse: boolean = false, - ): Promise { + ): Promise { if (![5, 6].includes(serviceData.length)) { emitLog('debugerror', `[parseServiceDataForWoCurtain] Buffer length ${serviceData.length} !== 5 or 6!`) return null @@ -49,9 +53,7 @@ export class WoCurtain extends SwitchbotDevice { batteryData = byte2 } - const model = serviceData.subarray(0, 1).toString('utf8') - const modelName = model === 'c' ? SwitchBotBLEModelName.Curtain : SwitchBotBLEModelName.Curtain3 - const modelFriendlyName = model === 'c' ? SwitchBotBLEModelFriendlyName.Curtain : SwitchBotBLEModelFriendlyName.Curtain3 + const model = serviceData.subarray(0, 1).toString('utf8') as string ? SwitchBotBLEModel.Curtain : SwitchBotBLEModel.Curtain3 const calibration = Boolean(byte1 & 0b01000000) const position = Math.max(Math.min(deviceData.readUInt8(0) & 0b01111111, 100), 0) const inMotion = Boolean(deviceData.readUInt8(0) & 0b10000000) @@ -59,19 +61,39 @@ export class WoCurtain extends SwitchbotDevice { const deviceChain = deviceData.readUInt8(1) & 0b00000111 const battery = batteryData !== null ? batteryData & 0b01111111 : null - return { - model, - modelName, - modelFriendlyName, - calibration, - battery, - inMotion, - position: reverse ? 100 - position : position, - lightLevel, - deviceChain, + if (model === SwitchBotBLEModel.Curtain) { + const data: curtainServiceData = { + model: SwitchBotBLEModel.Curtain, + modelName: SwitchBotBLEModelName.Curtain, + modelFriendlyName: SwitchBotBLEModelFriendlyName.Curtain, + calibration, + battery: battery ?? 0, + inMotion, + position: reverse ? 100 - position : position, + lightLevel, + deviceChain, + } + return data + } else { + const data: curtain3ServiceData = { + model: SwitchBotBLEModel.Curtain3, + modelName: SwitchBotBLEModelName.Curtain3, + modelFriendlyName: SwitchBotBLEModelFriendlyName.Curtain3, + calibration, + battery: battery ?? 0, + inMotion, + position: reverse ? 100 - position : position, + lightLevel, + deviceChain, + } + return data } } + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } + /** * Opens the curtain. * @param {number} [mode] - Running mode (0x01 = QuietDrift, 0xFF = Default). diff --git a/src/device/wohand.ts b/src/device/wohand.ts index 68e93243..fe0e0d86 100644 --- a/src/device/wohand.ts +++ b/src/device/wohand.ts @@ -2,6 +2,10 @@ * * wohand.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { botServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -21,7 +25,7 @@ export class WoHand extends SwitchbotDevice { static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 3) { emitLog('debugerror', `[parseServiceData] Buffer length ${serviceData.length} !== 3!`) return null @@ -30,23 +34,28 @@ export class WoHand extends SwitchbotDevice { const byte1 = serviceData.readUInt8(1) const byte2 = serviceData.readUInt8(2) - return { + const data: botServiceData = { model: SwitchBotBLEModel.Bot, modelName: SwitchBotBLEModelName.Bot, modelFriendlyName: SwitchBotBLEModelFriendlyName.Bot, - mode: !!(byte1 & 0b10000000), // Whether the light switch Add-on is used or not. 0 = press, 1 = switch + mode: (!!(byte1 & 0b10000000)).toString(), // Whether the light switch Add-on is used or not. 0 = press, 1 = switch state: !(byte1 & 0b01000000), // Whether the switch status is ON or OFF. 0 = on, 1 = off battery: byte2 & 0b01111111, // % } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** * Sends a command to the bot. - * @param {number[]} bytes - The command bytes. + * @param {number[]} reqBuf - The command bytes. * @returns {Promise} */ - private async operateBot(bytes: number[]): Promise { - const reqBuf = Buffer.from(bytes) + protected async sendCommand(reqBuf: Buffer): Promise { const resBuf = await this.command(reqBuf) const code = resBuf.readUInt8(0) @@ -59,39 +68,39 @@ export class WoHand extends SwitchbotDevice { * Presses the bot. * @returns {Promise} */ - async press(): Promise { - await this.operateBot([0x57, 0x01, 0x00]) + public async press(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x00])) } /** * Turns on the bot. * @returns {Promise} */ - async turnOn(): Promise { - await this.operateBot([0x57, 0x01, 0x01]) + public async turnOn(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x01])) } /** * Turns off the bot. * @returns {Promise} */ - async turnOff(): Promise { - await this.operateBot([0x57, 0x01, 0x02]) + public async turnOff(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x02])) } /** * Moves the bot down. * @returns {Promise} */ - async down(): Promise { - await this.operateBot([0x57, 0x01, 0x03]) + public async down(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x03])) } /** * Moves the bot up. * @returns {Promise} */ - async up(): Promise { - await this.operateBot([0x57, 0x01, 0x04]) + public async up(): Promise { + await this.sendCommand(Buffer.from([0x57, 0x01, 0x04])) } } diff --git a/src/device/wohub2.ts b/src/device/wohub2.ts index 8579be87..f8264027 100644 --- a/src/device/wohub2.ts +++ b/src/device/wohub2.ts @@ -4,6 +4,10 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { hub2ServiceData } from '../types/bledevicestatus.js' + import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -16,12 +20,12 @@ export class WoHub2 extends SwitchbotDevice { * Parses the service data for WoHub2. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 16) { emitLog('debugerror', `[parseServiceDataForWoHub2] Buffer length ${manufacturerData.length} !== 16!`) return null @@ -34,7 +38,7 @@ export class WoHub2 extends SwitchbotDevice { const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10 const lightLevel = byte12 & 0b11111 - return { + const data: hub2ServiceData = { model: SwitchBotBLEModel.Hub2, modelName: SwitchBotBLEModelName.Hub2, modelFriendlyName: SwitchBotBLEModelFriendlyName.Hub2, @@ -44,5 +48,11 @@ export class WoHub2 extends SwitchbotDevice { humidity: byte2 & 0b01111111, lightLevel, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/wohumi.ts b/src/device/wohumi.ts index c8ea12df..dab37934 100644 --- a/src/device/wohumi.ts +++ b/src/device/wohumi.ts @@ -2,6 +2,10 @@ * * wohumi.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { humidifierServiceData } from '../types/bledevicestatus.js' + import { Buffer } from 'node:buffer' import { SwitchbotDevice } from '../device.js' @@ -16,12 +20,12 @@ export class WoHumi extends SwitchbotDevice { * Parses the service data for WoHumi. * @param {Buffer} serviceData - The service data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 8) { emitLog('debugerror', `[parseServiceDataForWoHumi] Buffer length ${serviceData.length} !== 8!`) return null @@ -35,7 +39,7 @@ export class WoHumi extends SwitchbotDevice { const percentage = byte4 & 0b01111111 // 0-100%, 101/102/103 - Quick gear 1/2/3 const humidity = autoMode ? 0 : percentage === 101 ? 33 : percentage === 102 ? 66 : percentage === 103 ? 100 : percentage - return { + const data: humidifierServiceData = { model: SwitchBotBLEModel.Humidifier, modelName: SwitchBotBLEModelName.Humidifier, modelFriendlyName: SwitchBotBLEModelFriendlyName.Humidifier, @@ -44,6 +48,12 @@ export class WoHumi extends SwitchbotDevice { percentage: autoMode ? 0 : percentage, humidity, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/woiosensorth.ts b/src/device/woiosensorth.ts index 6fe25500..b1f82d38 100644 --- a/src/device/woiosensorth.ts +++ b/src/device/woiosensorth.ts @@ -4,6 +4,10 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + +import type { outdoorMeterServiceData } from '../types/bledevicestatus.js' + import { SwitchbotDevice } from '../device.js' import { SwitchBotBLEModel, SwitchBotBLEModelFriendlyName, SwitchBotBLEModelName } from '../types/types.js' @@ -17,13 +21,13 @@ export class WoIOSensorTH extends SwitchbotDevice { * @param {Buffer} serviceData - The service data buffer. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData( serviceData: Buffer, manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (serviceData.length !== 3) { emitLog('debugerror', `[parseServiceDataForWoIOSensorTH] Service Data Buffer length ${serviceData.length} !== 3!`) return null @@ -44,7 +48,7 @@ export class WoIOSensorTH extends SwitchbotDevice { const tempC = tempSign * ((mdByte11 & 0b01111111) + (mdByte10 & 0b00001111) / 10) const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10 - return { + const data: outdoorMeterServiceData = { model: SwitchBotBLEModel.OutdoorMeter, modelName: SwitchBotBLEModelName.OutdoorMeter, modelFriendlyName: SwitchBotBLEModelFriendlyName.OutdoorMeter, @@ -54,5 +58,11 @@ export class WoIOSensorTH extends SwitchbotDevice { humidity: mdByte12 & 0b01111111, battery: sdByte2 & 0b01111111, } + + return data + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } } diff --git a/src/device/woplugmini.ts b/src/device/woplugmini.ts index 9b3c8cfa..507c2311 100644 --- a/src/device/woplugmini.ts +++ b/src/device/woplugmini.ts @@ -2,6 +2,9 @@ * * woplugmini.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + +import type { plugMiniJPServiceData, plugMiniUSServiceData } from '../types/bledevicestatus.js' import { Buffer } from 'node:buffer' @@ -17,26 +20,26 @@ export class WoPlugMini extends SwitchbotDevice { * Parses the service data for WoPlugMini US. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_US( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { - return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniUS, emitLog) + ): Promise { + return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniUS, emitLog) as Promise } /** * Parses the service data for WoPlugMini JP. * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ static async parseServiceData_JP( manufacturerData: Buffer, emitLog: (level: string, message: string) => void, - ): Promise { - return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniJP, emitLog) + ): Promise { + return this.parseServiceData(manufacturerData, SwitchBotBLEModel.PlugMiniJP, emitLog) as Promise } /** @@ -44,13 +47,13 @@ export class WoPlugMini extends SwitchbotDevice { * @param {Buffer} manufacturerData - The manufacturer data buffer. * @param {SwitchBotBLEModel} model - The model of the plug mini. * @param {Function} emitLog - The function to emit log messages. - * @returns {Promise} - Parsed service data or null if invalid. + * @returns {Promise} - Parsed service data or null if invalid. */ private static async parseServiceData( manufacturerData: Buffer, model: SwitchBotBLEModel, emitLog: (level: string, message: string) => void, - ): Promise { + ): Promise { if (manufacturerData.length !== 14) { emitLog('debugerror', `[parseServiceDataForWoPlugMini] Buffer length ${manufacturerData.length} should be 14`) return null @@ -72,11 +75,11 @@ export class WoPlugMini extends SwitchbotDevice { const overload = !!(byte12 & 0b10000000) const currentPower = (((byte12 & 0b01111111) << 8) + byte13) / 10 // in watt - return { - model, + const data = { + model: model === SwitchBotBLEModel.PlugMiniUS ? SwitchBotBLEModel.PlugMiniUS : SwitchBotBLEModel.PlugMiniJP, modelName: SwitchBotBLEModelName.PlugMini, modelFriendlyName: SwitchBotBLEModelFriendlyName.PlugMini, - state, + state: state ?? 'unknown', delay, timer, syncUtcTime, @@ -84,6 +87,12 @@ export class WoPlugMini extends SwitchbotDevice { overload, currentPower, } + + return data as plugMiniUSServiceData | plugMiniJPServiceData + } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) } /** diff --git a/src/device/wopresence.ts b/src/device/wopresence.ts index b2ed2ed0..e07b97ea 100644 --- a/src/device/wopresence.ts +++ b/src/device/wopresence.ts @@ -4,6 +4,8 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + import type { motionSensorServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' @@ -47,4 +49,8 @@ export class WoPresence extends SwitchbotDevice { return data } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } } diff --git a/src/device/wosensorth.ts b/src/device/wosensorth.ts index 2d570709..a5bc4df9 100644 --- a/src/device/wosensorth.ts +++ b/src/device/wosensorth.ts @@ -4,6 +4,8 @@ */ import type { Buffer } from 'node:buffer' +import type * as Noble from '@stoprocent/noble' + import type { meterPlusServiceData, meterServiceData } from '../types/bledevicestatus.js' import { SwitchbotDevice } from '../device.js' @@ -77,4 +79,8 @@ export class WoSensorTH extends SwitchbotDevice { battery: byte2 & 0b01111111, } } + + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } } diff --git a/src/device/wostrip.ts b/src/device/wostrip.ts index ac1ea667..22d77bd4 100644 --- a/src/device/wostrip.ts +++ b/src/device/wostrip.ts @@ -2,6 +2,8 @@ * * wostrip.ts: Switchbot BLE API registration. */ +import type * as Noble from '@stoprocent/noble' + import type { stripLightServiceData } from '../types/bledevicestatus.js' import { Buffer } from 'node:buffer' @@ -59,6 +61,10 @@ export class WoStrip extends SwitchbotDevice { return data } + constructor(peripheral: Noble.Peripheral, noble: typeof Noble) { + super(peripheral, noble) + } + /** * Reads the state of the strip light. * @returns {Promise} - Resolves with true if the strip light is ON, false otherwise. diff --git a/src/index.ts b/src/index.ts index 77118bce..d37ba917 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,23 @@ * * index.ts: Switchbot BLE API registration. */ +export * from './advertising.js' +export * from './device.js' +export * from './device/woblindtilt.js' +export * from './device/wobulb.js' +export * from './device/woceilinglight.js' +export * from './device/wocontact.js' +export * from './device/wocurtain.js' +export * from './device/wohand.js' +export * from './device/wohub2.js' +export * from './device/wohumi.js' +export * from './device/woiosensorth.js' +export * from './device/woplugmini.js' +export * from './device/wopresence.js' +export * from './device/wosensorth.js' +export * from './device/wosmartlock.js' +export * from './device/wosmartlockpro.js' +export * from './device/wostrip.js' export * from './switchbot-ble.js' export * from './switchbot-openapi.js' export * from './types/bledevicestatus.js' diff --git a/src/parameter-checker.ts b/src/parameter-checker.ts index de1d86cf..aca25548 100644 --- a/src/parameter-checker.ts +++ b/src/parameter-checker.ts @@ -96,6 +96,7 @@ export class ParameterChecker extends EventEmitter { } } + this.emitLog('debug', 'All checks passed.') return true } diff --git a/src/switchbot-ble.ts b/src/switchbot-ble.ts index 1b7635cb..95a038cc 100644 --- a/src/switchbot-ble.ts +++ b/src/switchbot-ble.ts @@ -2,12 +2,12 @@ * * switchbot.ts: Switchbot BLE API registration. */ -import type * as Noble from '@stoprocent/noble' - -import type { Ad, Params } from './types/types.js' +import type { Ad, Params, Rule } from './types/types.js' import { EventEmitter } from 'node:events' +import * as Noble from '@stoprocent/noble' + import { Advertising } from './advertising.js' import { SwitchbotDevice } from './device.js' import { WoBlindTilt } from './device/woblindtilt.js' @@ -28,14 +28,15 @@ import { WoStrip } from './device/wostrip.js' import { parameterChecker } from './parameter-checker.js' import { DEFAULT_DISCOVERY_DURATION, PRIMARY_SERVICE_UUID_LIST } from './settings.js' import { SwitchBotBLEModel } from './types/types.js' + /** - * SwitchBot class to interact with SwitchBot devices. + * SwitchBotBLE class to interact with SwitchBot devices. */ export class SwitchBotBLE extends EventEmitter { - private ready: Promise - noble!: typeof Noble - ondiscover?: (device: SwitchbotDevice) => void - onadvertisement?: (ad: Ad) => void + public ready: Promise + public noble!: typeof Noble + ondiscover?: (device: SwitchbotDevice) => Promise | void + onadvertisement?: (ad: Ad) => Promise | void /** * Constructor @@ -44,7 +45,7 @@ export class SwitchBotBLE extends EventEmitter { */ constructor(params?: Params) { super() - this.ready = this.init(params) + this.ready = this.initialize(params) } /** @@ -53,7 +54,7 @@ export class SwitchBotBLE extends EventEmitter { * @param level - The severity level of the log (e.g., 'info', 'warn', 'error'). * @param message - The log message to be emitted. */ - public async emitLog(level: string, message: string): Promise { + public async log(level: string, message: string): Promise { this.emit('log', { level, message }) } @@ -63,259 +64,162 @@ export class SwitchBotBLE extends EventEmitter { * @param {Params} [params] - Optional parameters * @returns {Promise} - Resolves when initialization is complete */ - async init(params?: Params): Promise { - let noble: typeof Noble - if (params && params.noble) { - noble = params.noble - } else { - noble = (await import('@stoprocent/noble')).default as typeof Noble - } - - // Public properties - this.noble = noble + private async initialize(params?: Params): Promise { + this.noble = params?.noble ?? Noble.default as typeof Noble } /** - * Discover SwitchBot devices based on the provided parameters. + * Validates the parameters. * - * @param {Params} params - The parameters for discovery. - * @returns {Promise} - A promise that resolves with a list of discovered devices. + * @param {Params} params - The parameters to validate. + * @param {Record} schema - The schema to validate against. + * @returns {Promise} - Resolves if parameters are valid, otherwise throws an error. */ - async discover(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - duration: { required: false, type: 'integer', min: 1, max: 60000 }, - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - quick: { required: false, type: 'boolean' }, - }, - false, - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } - - if (!params) { - params = {} - } - - // Determine the values of the parameters - const p = { - duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, - model: params.model ?? '', - id: params.id ?? '', - quick: !!params.quick, - } - - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble failed to initialize')) - } - const peripherals: Record = {} - let timer: NodeJS.Timeout = setTimeout(() => { }, 0) - const finishDiscovery = () => { - if (timer) { - clearTimeout(timer) - } - - this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - - const device_list: SwitchbotDevice[] = [] - for (const addr in peripherals) { - device_list.push(peripherals[addr]) - } - - resolve(device_list) - } - - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const device = await this.getDeviceObject(peripheral, p.id, p.model) - if (!device) { - return - } - const id = device.id - peripherals[id!] = device - - if (this.ondiscover && typeof this.ondiscover === 'function') { - this.ondiscover(device) - } - - if (p.quick) { - finishDiscovery() - } - }) - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - false, - ).then(() => { - timer = setTimeout(() => { - finishDiscovery() - }, p.duration) - }).catch((error: Error) => { - reject(error) - }) - }) - .catch((error) => { - reject(error) - }) - }) - return promise + public async validate(params: Params, schema: Record): Promise { + const valid = parameterChecker.check(params as Record, schema as Record, false) + if (!valid) { + this.log('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) + throw new Error(parameterChecker.error!.message) + } } /** - * Initializes the noble object and waits for it to be powered on. + * Waits for the noble object to be powered on. * * @returns {Promise} - Resolves when the noble object is powered on. */ - async _init(): Promise { + private async waitForPowerOn(): Promise { await this.ready - const promise = new Promise((resolve, reject) => { - let err - if (this.noble._state === 'poweredOn') { - resolve() - return - } + if (this.noble._state === 'poweredOn') { + return + } + + return new Promise((resolve, reject) => { this.noble.once('stateChange', (state: typeof Noble._state) => { switch (state) { case 'unsupported': case 'unauthorized': case 'poweredOff': - err = new Error( - `Failed to initialize the Noble object: ${this.noble._state}`, - ) - reject(err) - return + reject(new Error(`Failed to initialize the Noble object: ${state}`)) + break case 'resetting': case 'unknown': - err = new Error( - `Adapter is not ready: ${this.noble._state}`, - ) - reject(err) - return + reject(new Error(`Adapter is not ready: ${state}`)) + break case 'poweredOn': resolve() - return + break default: - err = new Error( - `Unknown state: ${this.noble._state}`, - ) - reject(err) + reject(new Error(`Unknown state: ${state}`)) + } + }) + }) + } + + /** + * Discover SwitchBot devices based on the provided parameters. + * + * @param {Params} params - The parameters for discovery. + * @returns {Promise} - A promise that resolves with a list of discovered devices. + */ + public async discover(params: Params = {}): Promise { + await this.ready + await this.validate(params, { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { required: false, type: 'string', enum: Object.values(SwitchBotBLEModel) }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }) + + await this.waitForPowerOn() + + if (!this.noble) { + throw new Error('noble failed to initialize') + } + + const p = { + duration: params.duration ?? DEFAULT_DISCOVERY_DURATION, + model: params.model ?? '', + id: params.id ?? '', + quick: !!params.quick, + } + + const peripherals: Record = {} + let timer: NodeJS.Timeout + + const finishDiscovery = async () => { + if (timer) { + clearTimeout(timer) + } + this.noble.removeAllListeners('discover') + try { + await this.noble.stopScanningAsync() + this.log('info', 'Stopped Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.log('error', `discover stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } + return Object.values(peripherals) + } + + return new Promise((resolve, reject) => { + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const device = await this.createDevice(peripheral, p.id, p.model) + if (!device) { + return + } + peripherals[device.id!] = device + + if (this.ondiscover) { + this.ondiscover(device) + } + if (p.quick) { + resolve(await finishDiscovery()) } }) + + this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, false) + .then(() => { + timer = setTimeout(async () => resolve(await finishDiscovery()), p.duration) + }) + .catch(reject) }) - return promise } /** - * Gets the device object based on the peripheral, id, and model. + * Creates a device object based on the peripheral, id, and model. * * @param {Noble.Peripheral} peripheral - The peripheral object. * @param {string} id - The device id. * @param {string} model - The device model. * @returns {Promise} - The device object or null. */ - async getDeviceObject(peripheral: Noble.Peripheral, id: string, model: string): Promise { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, id, model)) { - let device - if (ad && ad.serviceData && ad.serviceData.model) { - switch (ad.serviceData.model) { - case SwitchBotBLEModel.Bot: - device = new WoHand(peripheral, this.noble) - break - case SwitchBotBLEModel.Curtain: - case SwitchBotBLEModel.Curtain3: - device = new WoCurtain(peripheral, this.noble) - break - case SwitchBotBLEModel.Humidifier: - device = new WoHumi(peripheral, this.noble) - break - case SwitchBotBLEModel.Meter: - device = new WoSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.MeterPlus: - device = new WoSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.Hub2: - device = new WoHub2(peripheral, this.noble) - break - case SwitchBotBLEModel.OutdoorMeter: - device = new WoIOSensorTH(peripheral, this.noble) - break - case SwitchBotBLEModel.MotionSensor: - device = new WoPresence(peripheral, this.noble) - break - case SwitchBotBLEModel.ContactSensor: - device = new WoContact(peripheral, this.noble) - break - case SwitchBotBLEModel.ColorBulb: - device = new WoBulb(peripheral, this.noble) - break - case SwitchBotBLEModel.CeilingLight: - device = new WoCeilingLight(peripheral, this.noble) - break - case SwitchBotBLEModel.CeilingLightPro: - device = new WoCeilingLight(peripheral, this.noble) - break - case SwitchBotBLEModel.StripLight: - device = new WoStrip(peripheral, this.noble) - break - case SwitchBotBLEModel.PlugMiniUS: - case SwitchBotBLEModel.PlugMiniJP: - device = new WoPlugMini(peripheral, this.noble) - break - case SwitchBotBLEModel.Lock: - device = new WoSmartLock(peripheral, this.noble) - break - case SwitchBotBLEModel.LockPro: - device = new WoSmartLockPro(peripheral, this.noble) - break - case SwitchBotBLEModel.BlindTilt: - device = new WoBlindTilt(peripheral, this.noble) - break - default: // 'resetting', 'unknown' - device = new SwitchbotDevice(peripheral, this.noble) - } + private async createDevice(peripheral: Noble.Peripheral, id: string, model: string): Promise { + const ad = await Advertising.parse(peripheral, this.log.bind(this)) + if (ad && await this.filterAd(ad, id, model)) { + switch (ad.serviceData.model) { + case SwitchBotBLEModel.Bot: return new WoHand(peripheral, this.noble) + case SwitchBotBLEModel.Curtain: + case SwitchBotBLEModel.Curtain3: return new WoCurtain(peripheral, this.noble) + case SwitchBotBLEModel.Humidifier: return new WoHumi(peripheral, this.noble) + case SwitchBotBLEModel.Meter: + case SwitchBotBLEModel.MeterPlus: return new WoSensorTH(peripheral, this.noble) + case SwitchBotBLEModel.Hub2: return new WoHub2(peripheral, this.noble) + case SwitchBotBLEModel.OutdoorMeter: return new WoIOSensorTH(peripheral, this.noble) + case SwitchBotBLEModel.MotionSensor: return new WoPresence(peripheral, this.noble) + case SwitchBotBLEModel.ContactSensor: return new WoContact(peripheral, this.noble) + case SwitchBotBLEModel.ColorBulb: return new WoBulb(peripheral, this.noble) + case SwitchBotBLEModel.CeilingLight: + case SwitchBotBLEModel.CeilingLightPro: return new WoCeilingLight(peripheral, this.noble) + case SwitchBotBLEModel.StripLight: return new WoStrip(peripheral, this.noble) + case SwitchBotBLEModel.PlugMiniUS: + case SwitchBotBLEModel.PlugMiniJP: return new WoPlugMini(peripheral, this.noble) + case SwitchBotBLEModel.Lock: return new WoSmartLock(peripheral, this.noble) + case SwitchBotBLEModel.LockPro: return new WoSmartLockPro(peripheral, this.noble) + case SwitchBotBLEModel.BlindTilt: return new WoBlindTilt(peripheral, this.noble) + default: return new SwitchbotDevice(peripheral, this.noble) } - return device || null - } else { - return null } + return null } /** @@ -324,23 +228,17 @@ export class SwitchBotBLE extends EventEmitter { * @param {Ad} ad - The advertising data. * @param {string} id - The device id. * @param {string} model - The device model. - * @returns {boolean} - True if the advertising data matches the id and model, false otherwise. + * @returns {Promise} - True if the advertising data matches the id and model, false otherwise. */ - async filterAdvertising(ad: Ad, id: string, model: string): Promise { + private async filterAd(ad: Ad, id: string, model: string): Promise { if (!ad) { return false } - if (id) { - id = id.toLowerCase().replace(/:/g, '') - const ad_id = ad.address.toLowerCase().replace(/[^a-z0-9]/g, '') - if (ad_id !== id) { - return false - } + if (id && ad.address.toLowerCase().replace(/[^a-z0-9]/g, '') !== id.toLowerCase().replace(/:/g, '')) { + return false } - if (model) { - if (ad.serviceData.model !== model) { - return false - } + if (model && ad.serviceData.model !== model) { + return false } return true } @@ -351,90 +249,36 @@ export class SwitchBotBLE extends EventEmitter { * @param {Params} [params] - Optional parameters. * @returns {Promise} - Resolves when scanning starts successfully. */ - async startScan(params: Params = {}): Promise { - const promise = new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - params as Record, - { - model: { - required: false, - type: 'string', - enum: [ - SwitchBotBLEModel.Bot, - SwitchBotBLEModel.Curtain, - SwitchBotBLEModel.Curtain3, - SwitchBotBLEModel.Humidifier, - SwitchBotBLEModel.Meter, - SwitchBotBLEModel.MeterPlus, - SwitchBotBLEModel.Hub2, - SwitchBotBLEModel.OutdoorMeter, - SwitchBotBLEModel.MotionSensor, - SwitchBotBLEModel.ContactSensor, - SwitchBotBLEModel.ColorBulb, - SwitchBotBLEModel.CeilingLight, - SwitchBotBLEModel.CeilingLightPro, - SwitchBotBLEModel.StripLight, - SwitchBotBLEModel.PlugMiniUS, - SwitchBotBLEModel.PlugMiniJP, - SwitchBotBLEModel.Lock, - SwitchBotBLEModel.LockPro, - SwitchBotBLEModel.BlindTilt, - ], - }, - id: { required: false, type: 'string', min: 12, max: 17 }, - }, - false, - ) - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } + public async startScan(params: Params = {}): Promise { + await this.ready + await this.validate(params, { + model: { required: false, type: 'string', enum: Object.values(SwitchBotBLEModel) }, + id: { required: false, type: 'string', min: 12, max: 17 }, + }) - // Initialize the noble object - this._init() - .then(() => { - if (this.noble === null) { - return reject(new Error('noble object failed to initialize')) - } - // Determine the values of the parameters - const p = { - model: params.model || '', - id: params.id || '', - } - - // Set a handler for the 'discover' event - this.noble.on('discover', async (peripheral: Noble.Peripheral) => { - const ad = await Advertising.parse(peripheral, this.emitLog.bind(this)) - if (ad && await this.filterAdvertising(ad, p.id, p.model)) { - if ( - this.onadvertisement - && typeof this.onadvertisement === 'function' - ) { - this.onadvertisement(ad) - } - } - }) - - // Start scanning - this.noble.startScanningAsync( - PRIMARY_SERVICE_UUID_LIST, - true, - ).then(() => { - this.emitLog('info', 'Started Scanning for SwitchBot BLE devices.') - resolve() - }).catch((error: Error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) - }) - .catch((error) => { - this.emitLog('error', `startScanning error: ${JSON.stringify(error!.message)}`) - reject(error) - }) + await this.waitForPowerOn() + + if (!this.noble) { + throw new Error('noble object failed to initialize') + } + + const p = { model: params.model || '', id: params.id || '' } + + this.noble.on('discover', async (peripheral: Noble.Peripheral) => { + const ad = await Advertising.parse(peripheral, this.log.bind(this)) + if (ad && await this.filterAd(ad, p.id, p.model)) { + if (this.onadvertisement) { + this.onadvertisement(ad) + } + } }) - return promise + + try { + await this.noble.startScanningAsync(PRIMARY_SERVICE_UUID_LIST, true) + this.log('info', 'Started Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.log('error', `startScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } } /** @@ -442,14 +286,18 @@ export class SwitchBotBLE extends EventEmitter { * * @returns {Promise} - Resolves when scanning stops successfully. */ - async stopScan(): Promise { - if (this.noble === null) { + public async stopScan(): Promise { + if (!this.noble) { return } this.noble.removeAllListeners('discover') - this.noble.stopScanningAsync() - this.emitLog('info', 'Stopped Scanning for SwitchBot BLE devices.') + try { + await this.noble.stopScanningAsync() + this.log('info', 'Stopped Scanning for SwitchBot BLE devices.') + } catch (e: any) { + this.log('error', `stopScanningAsync error: ${JSON.stringify(e.message ?? e)}`) + } } /** @@ -458,27 +306,12 @@ export class SwitchBotBLE extends EventEmitter { * @param {number} msec - The time to wait in milliseconds. * @returns {Promise} - Resolves after the specified time. */ - async wait(msec: number): Promise { - return new Promise((resolve, reject) => { - // Check the parameters - const valid = parameterChecker.check( - { - msec, - }, - { - msec: { required: true, type: 'integer', min: 0 }, - }, - true, // Add the required argument - ) - - if (!valid) { - this.emitLog('error', `parameterChecker: ${JSON.stringify(parameterChecker.error!.message)}`) - reject(new Error(parameterChecker.error!.message)) - return - } - // Set a timer - setTimeout(resolve, msec) - }) + public async wait(msec: number): Promise { + if (typeof msec !== 'number' || msec < 0) { + throw new Error('Invalid parameter: msec must be a non-negative integer.') + } + + return new Promise(resolve => setTimeout(resolve, msec)) } } diff --git a/src/switchbot-openapi.ts b/src/switchbot-openapi.ts index 890a221b..13dc5c1d 100644 --- a/src/switchbot-openapi.ts +++ b/src/switchbot-openapi.ts @@ -6,7 +6,7 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http' import type { pushResponse } from './types/devicepush.js' import type { devices } from './types/deviceresponse.js' -import type { deviceStatus } from './types/devicestatus.js' +import type { deviceStatus, deviceStatusRequest } from './types/devicestatus.js' import type { deleteWebhookResponse, queryWebhookResponse, setupWebhookResponse, updateWebhookResponse } from './types/devicewebhookstatus.js' import { Buffer } from 'node:buffer' @@ -109,10 +109,10 @@ export class SwitchBotOpenAPI extends EventEmitter { * @param command - The command to send to the device. * @param parameter - The parameter for the command. * @param commandType - The type of the command, defaults to 'command'. - * @returns {Promise<{ response: pushResponse['body'], statusCode: number }>} A promise that resolves to an object containing the API response. + * @returns {Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'] }>} A promise that resolves to an object containing the API response. * @throws An error if the device control fails. */ - async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: number }> { + async controlDevice(deviceId: string, command: string, parameter: string, commandType: string = 'command'): Promise<{ response: pushResponse['body'], statusCode: pushResponse['statusCode'] }> { try { const { body, statusCode } = await request(`${this.baseURL}/devices/${deviceId}/commands`, { method: 'POST', @@ -137,10 +137,10 @@ export class SwitchBotOpenAPI extends EventEmitter { * Retrieves the status of a specific device. * * @param deviceId - The unique identifier of the device. - * @returns {Promise<{ response: deviceStatus, statusCode: number }>} A promise that resolves to the device status. + * @returns {Promise<{ response: deviceStatus, statusCode: deviceStatusRequest['statusCode'] }>} A promise that resolves to the device status. * @throws An error if the request fails. */ - async getDeviceStatus(deviceId: string): Promise<{ response: deviceStatus, statusCode: number }> { + async getDeviceStatus(deviceId: string): Promise<{ response: deviceStatus, statusCode: deviceStatusRequest['statusCode'] }> { try { const { body, statusCode } = await request(`${this.baseURL}/devices/${deviceId}/status`, { method: 'GET', @@ -213,7 +213,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('debug', `Received Webhook: ${JSON.stringify(body)}`) this.emit('webhookEvent', body) } catch (e: any) { - await this.emitLog('error', `Failed to handle webhook event data. Error:${e}`) + await this.emitLog('error', `Failed to handle webhook event data, Error: ${e.message ?? e}`) } }) response.writeHead(200, { 'Content-Type': 'text/plain' }) @@ -224,11 +224,11 @@ export class SwitchBotOpenAPI extends EventEmitter { response.end(`NG`) } } catch (e: any) { - await this.emitLog('error', `Failed to handle webhook event. Error:${e}`) + await this.emitLog('error', `Failed to handle webhook event, Error: ${e.message ?? e}`) } }).listen(port || 80) } catch (e: any) { - await this.emitLog('error', `Failed to create webhook listener. Error:${e.message}`) + await this.emitLog('error', `Failed to create webhook listener, Error: ${e.message ?? e}`) return } @@ -248,7 +248,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('error', `Failed to configure webhook. Existing webhook well be overridden. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`) } } catch (e: any) { - await this.emitLog('error', `Failed to configure webhook. Error: ${e.message}`) + await this.emitLog('error', `Failed to configure webhook, Error: ${e.message ?? e}`) } try { @@ -269,7 +269,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('error', `Failed to update webhook. HTTP:${statusCode} API:${response?.statusCode} message:${response?.message}`) } } catch (e: any) { - await this.emitLog('error', `Failed to update webhook. Error:${e.message}`) + await this.emitLog('error', `Failed to update webhook, Error: ${e.message ?? e}`) } try { @@ -288,7 +288,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('info', `Listening webhook on ${response?.body?.urls[0]}`) } } catch (e: any) { - await this.emitLog('error', `Failed to query webhook. Error:${e}`) + await this.emitLog('error', `Failed to query webhook, Error: ${e.message ?? e}`) } } @@ -318,7 +318,7 @@ export class SwitchBotOpenAPI extends EventEmitter { await this.emitLog('info', 'Unregistered webhook to close listening.') } } catch (e: any) { - await this.emitLog('error', `Failed to delete webhook. Error:${e.message}`) + await this.emitLog('error', `Failed to delete webhook, Error: ${e.message ?? e}`) } } } diff --git a/src/test/index.test.ts b/src/test/index.test.ts index aa43f6dd..85b82b70 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -3,43 +3,15 @@ import { describe, expect, it } from 'vitest' import * as index from '../index.js' describe('index module exports', () => { - it('should export switchbot', () => { - expect(index).toHaveProperty('switchbot') + it('should export switchbot-ble', () => { + expect(index.SwitchBotBLE).toBeDefined() }) it('should export switchbot-openapi', () => { - expect(index).toHaveProperty('switchbot-openapi') + expect(index.SwitchBotOpenAPI).toBeDefined() }) - it('should export bledevicestatus', () => { - expect(index).toHaveProperty('bledevicestatus') - }) - - it('should export devicelist', () => { - expect(index).toHaveProperty('devicelist') - }) - - it('should export devicepush', () => { - expect(index).toHaveProperty('devicepush') - }) - - it('should export deviceresponse', () => { - expect(index).toHaveProperty('deviceresponse') - }) - - it('should export devicestatus', () => { - expect(index).toHaveProperty('devicestatus') - }) - - it('should export devicewebhookstatus', () => { - expect(index).toHaveProperty('devicewebhookstatus') - }) - - it('should export irdevicelist', () => { - expect(index).toHaveProperty('irdevicelist') - }) - - it('should export types', () => { - expect(index).toHaveProperty('types') + it('should export SwitchbotDevice', () => { + expect(index.SwitchbotDevice).toBeDefined() }) }) diff --git a/src/test/switchbot-ble.test.ts b/src/test/switchbot-ble.test.ts new file mode 100644 index 00000000..76c24274 --- /dev/null +++ b/src/test/switchbot-ble.test.ts @@ -0,0 +1,52 @@ +import * as Noble from '@stoprocent/noble' +import { beforeEach, describe, expect, it } from 'vitest' + +import { SwitchBotBLE } from '../switchbot-ble.js' + +describe('switchBotBLE', () => { + let switchBot: SwitchBotBLE + + beforeEach(() => { + switchBot = new SwitchBotBLE({ noble: Noble }) + }) + + it('should initialize noble object', async () => { + await switchBot.ready + expect(switchBot.noble).toBeTruthy() + }) + + it('should validate parameters', async () => { + const params = { duration: 5000, model: 'Bot', id: '123456789012', quick: true } + await switchBot.validate(params, { + duration: { required: false, type: 'integer', min: 1, max: 60000 }, + model: { required: false, type: 'string', enum: ['Bot'] }, + id: { required: false, type: 'string', min: 12, max: 17 }, + quick: { required: false, type: 'boolean' }, + }) + }) + + it('should start and stop scanning', async () => { + let discoverListenerCount = 0 + const discoverListener = () => { + discoverListenerCount++ + } + switchBot.noble.on('discover', discoverListener) + await switchBot.startScan() + expect(discoverListenerCount).toBe(1) + await switchBot.stopScan() + switchBot.noble.removeListener('discover', discoverListener) + expect(discoverListenerCount).toBe(0) + }) + + it('should wait for specified time', async () => { + const start = Date.now() + await switchBot.wait(1000) + const end = Date.now() + expect(end - start).toBeGreaterThanOrEqual(1000) + }) + + it('should discover devices', async () => { + const devices = await switchBot.discover({ duration: 1000, quick: true }) + expect(devices).toBeInstanceOf(Array) + }) +}) diff --git a/src/test/switchbot.test.ts b/src/test/switchbot.test.ts deleted file mode 100644 index 546281c8..00000000 --- a/src/test/switchbot.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Buffer } from 'node:buffer' - -import sinon from 'sinon' -import { expect } from 'vitest' - -import { WoHumi } from '../device/wohumi.js' -import { SwitchBotBLE } from '../switchbot-ble.js' -import { SwitchBotBLEModel } from '../types/types.js' - -describe('switchBot', () => { - let switchBot: SwitchBotBLE - let nobleMock: any - - beforeEach(() => { - nobleMock = { - on: sinon.stub(), - startScanningAsync: sinon.stub().resolves(), - stopScanningAsync: sinon.stub().resolves(), - removeAllListeners: sinon.stub(), - _state: 'poweredOn', - once: sinon.stub(), - } - switchBot = new SwitchBotBLE({ noble: nobleMock }) - }) - - afterEach(() => { - sinon.restore() - }) - - it('should initialize noble object', async () => { - await switchBot.init({ noble: nobleMock }) - expect(switchBot.noble).toBe(nobleMock) - }) - - it('should discover devices', async () => { - const peripheralMock = { - id: 'mock-id', - uuid: 'mock-uuid', - address: 'mock-address', - addressType: 'public', - connectable: true, - advertisement: { - serviceData: [{ uuid: 'mock-uuid', data: Buffer.from([0x01, 0x02]) }], - localName: 'mock-localName', - txPowerLevel: -59, - manufacturerData: Buffer.from([0x01, 0x02, 0x03, 0x04]), - serviceUuids: ['mock-service-uuid'], - }, - rssi: -50, - services: [], - state: 'disconnected' as const, - mtu: 23, - connect: sinon.stub(), - connectAsync: sinon.stub().resolves(), - disconnect: sinon.stub(), - disconnectAsync: sinon.stub().resolves(), - updateRssi: sinon.stub(), - updateRssiAsync: sinon.stub().resolves(), - discoverServices: sinon.stub(), - discoverServicesAsync: sinon.stub().resolves(), - discoverSomeServicesAndCharacteristics: sinon.stub(), - discoverSomeServicesAndCharacteristicsAsync: sinon.stub().resolves(), - discoverAllServicesAndCharacteristics: sinon.stub(), - discoverAllServicesAndCharacteristicsAsync: sinon.stub().resolves(), - readHandle: sinon.stub(), - readHandleAsync: sinon.stub().resolves(), - writeHandle: sinon.stub(), - writeHandleAsync: sinon.stub().resolves(), - cancelConnect: sinon.stub(), - on: sinon.stub(), - once: sinon.stub(), - addListener: sinon.stub(), - removeListener: sinon.stub(), - removeAllListeners: sinon.stub(), - emit: sinon.stub(), - listeners: sinon.stub(), - eventNames: sinon.stub(), - listenerCount: sinon.stub(), - off: sinon.stub(), - setMaxListeners: sinon.stub(), - getMaxListeners: sinon.stub(), - rawListeners: sinon.stub(), - prependListener: sinon.stub(), - prependOnceListener: sinon.stub(), - } - const getDeviceObjectStub = sinon.stub(switchBot, 'getDeviceObject').resolves(new WoHumi(peripheralMock, nobleMock)) - nobleMock.on.withArgs('discover').yields(peripheralMock) - - const devices = await switchBot.discover({ duration: 1000, model: SwitchBotBLEModel.Humidifier }) - - expect(devices).toHaveLength(1) - expect(devices[0]).toBeInstanceOf(WoHumi) - expect(getDeviceObjectStub.calledOnce).toBe(true) - }) - - it('should handle noble state changes', async () => { - nobleMock._state = 'poweredOff' - const initPromise = switchBot._init() - - nobleMock.once.withArgs('stateChange').yields('poweredOn') - await initPromise - - expect(nobleMock.once.calledWith('stateChange')).toBe(true) - }) - - it('should filter advertising data correctly', async () => { - const ad = { - id: 'mock-id', - address: 'mock-address', - rssi: -50, - serviceData: { - model: SwitchBotBLEModel.Humidifier, - }, - } - const result = await switchBot.filterAdvertising(ad, 'mock-id', SwitchBotBLEModel.Humidifier) - expect(result).toBe(true) - }) -}) diff --git a/src/types/bledevicestatus.ts b/src/types/bledevicestatus.ts index 9fc52a79..3233df6e 100644 --- a/src/types/bledevicestatus.ts +++ b/src/types/bledevicestatus.ts @@ -17,8 +17,9 @@ export interface ad { } interface serviceData { - model: string - modelName: string + model: SwitchBotBLEModel + modelName: SwitchBotBLEModelName + modelFriendlyName: SwitchBotBLEModelFriendlyName } export type botServiceData = serviceData & { @@ -213,10 +214,11 @@ export type blindTiltServiceData = serviceData & { modelName: SwitchBotBLEModelName.BlindTilt modelFriendlyName: SwitchBotBLEModelFriendlyName.BlindTilt calibration: boolean - battery: number + battery: number | null inMotion: boolean tilt: number lightLevel: number + sequenceNumber: number } export type ceilingLightServiceData = serviceData & {