diff --git a/.eslintrc.js b/.eslintrc.js index f5e1ff6..8ec2f7f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { root: true, parserOptions: { - ecmaVersion: 2019, + ecmaVersion: 2022, project: ['./tsconfig.json'], }, env: { @@ -21,8 +21,7 @@ module.exports = { extends: [ 'airbnb-base', 'airbnb-typescript/base', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/strict', + 'plugin:@typescript-eslint/strict-type-checked', 'plugin:prettier/recommended', ], rules: { @@ -35,12 +34,14 @@ module.exports = { 'ts-check': 'allow-with-description', }, ], + '@typescript-no-unsafe-assignment': 'off', 'no-restricted-syntax': [ 'off', { selector: 'ForOfStatement', }, ], + 'no-void': 'off', 'tsdoc/syntax': 'off', // 'warn', }, }, diff --git a/examples/device.ts b/examples/device.ts index fb6da30..190090b 100755 --- a/examples/device.ts +++ b/examples/device.ts @@ -1,7 +1,13 @@ -import { Client, Sysinfo } from '..'; // 'tplink-smarthome-api' +import { Client } from '..'; // 'tplink-smarthome-api' const client = new Client(); -client.getDevice({ host: '10.0.0.60' }).then((device) => { - device.getSysInfo().then((sysInfo: Sysinfo) => console.log(sysInfo)); -}); +client + .getDevice({ host: '10.0.0.60' }) + .then(async (device) => { + const sysInfo = await device.getSysInfo(); + console.log(sysInfo); + }) + .catch((reason) => { + console.error(reason); + }); diff --git a/examples/events.ts b/examples/events.ts index 1c611a0..ba012b8 100755 --- a/examples/events.ts +++ b/examples/events.ts @@ -1,5 +1,10 @@ import util from 'util'; -import { Client, Device, LightState, RealtimeNormalized } from '..'; // 'tplink-smarthome-api' +import { + Client, + type Device, + type LightState, + type RealtimeNormalized, +} from '..'; // 'tplink-smarthome-api' const client = new Client(); @@ -18,15 +23,20 @@ const logEvent = function logEvent( // Client events `device-*` also have `bulb-*` and `plug-*` counterparts. // Use those if you want only events for those types and not all devices. -client.on('device-new', (device) => { +client.on('device-new', (device: Device) => { logEvent('device-new', device); // Poll device every 5 seconds setTimeout(function pollDevice() { - device.getInfo().then((data: unknown) => { - console.log(data); - setTimeout(pollDevice, 5000); - }); + device + .getInfo() + .then((data: unknown) => { + console.log(data); + setTimeout(pollDevice, 5000); + }) + .catch((reason: unknown) => { + console.error(reason); + }); }, 5000); // Device (Common) Events @@ -68,10 +78,10 @@ client.on('device-new', (device) => { logEvent('lightstate-update', device, lightstate); }); }); -client.on('device-online', (device) => { +client.on('device-online', (device: Device) => { logEvent('device-online', device); }); -client.on('device-offline', (device) => { +client.on('device-offline', (device: Device) => { logEvent('device-offline', device); }); diff --git a/examples/multi-plug.ts b/examples/multi-plug.ts index 913f4c5..4f8b820 100644 --- a/examples/multi-plug.ts +++ b/examples/multi-plug.ts @@ -46,33 +46,45 @@ const monitorEvents = function monitorEvents(device: Device) { // Poll device every 5 seconds setTimeout(function pollDevice() { - device.getInfo().then((data) => { - console.log(data); - setTimeout(pollDevice, 5000); - }); + device + .getInfo() + .then((data) => { + console.log(data); + setTimeout(pollDevice, 5000); + }) + .catch((reason) => { + console.error(reason); + }); }, 5000); }; -(async () => { - const device = await client.getDevice({ host: '10.0.1.136' }); +void (async () => { + try { + const device = await client.getDevice({ host: '10.0.1.136' }); - console.log(device.alias); + console.log(device.alias); - if (!('children' in device) || !device.children) { - console.log('device has no children'); - return; - } + if (!('children' in device)) { + console.log('device has no children'); + return; + } - device.children.forEach((child) => { - console.log(child); - }); + device.children.forEach((child) => { + console.log(child); + }); - await Promise.all( - Array.from(device.children.keys(), async (childId) => { - const childPlug = await client.getDevice({ host: '10.0.1.136', childId }); - monitorEvents(childPlug); - }), - ); + await Promise.all( + Array.from(device.children.keys(), async (childId) => { + const childPlug = await client.getDevice({ + host: '10.0.1.136', + childId, + }); + monitorEvents(childPlug); + }), + ); - monitorEvents(device); + monitorEvents(device); + } catch (err) { + console.error(err); + } })(); diff --git a/examples/plug-turn-on-off.ts b/examples/plug-turn-on-off.ts index 6751f0e..ade5c86 100644 --- a/examples/plug-turn-on-off.ts +++ b/examples/plug-turn-on-off.ts @@ -2,11 +2,16 @@ import { Client } from '..'; // 'tplink-smarthome-api' const client = new Client(); -client.getDevice({ host: '10.0.0.60' }).then((device) => { - console.log('Found device:', device.deviceType, device.alias); - if (device.deviceType === 'plug') { - console.log('Turning plug on, then off', device.alias); - device.setPowerState(true); - device.setPowerState(false); - } -}); +client + .getDevice({ host: '10.0.0.60' }) + .then(async (device) => { + console.log('Found device:', device.deviceType, device.alias); + if (device.deviceType === 'plug') { + console.log('Turning plug on, then off', device.alias); + await device.setPowerState(true); + await device.setPowerState(false); + } + }) + .catch((reason) => { + console.error(reason); + }); diff --git a/examples/turn-all-plugs-on.ts b/examples/turn-all-plugs-on.ts index bce36d8..ec4c0ab 100644 --- a/examples/turn-all-plugs-on.ts +++ b/examples/turn-all-plugs-on.ts @@ -1,13 +1,18 @@ -import { Client } from '..'; // 'tplink-smarthome-api' +import { Client, Plug } from '..'; // 'tplink-smarthome-api' const client = new Client(); // Search for all plugs and turn them on -client.on('plug-new', (plug) => { +client.on('plug-new', (plug: Plug) => { console.log('Found plug:', plug.alias); - plug.setPowerState(true).then(() => { - console.log('Plug', plug.alias, 'is now on'); - }); + plug + .setPowerState(true) + .then(() => { + console.log('Plug', plug.alias, 'is now on'); + }) + .catch((reason) => { + console.error(reason); + }); }); client.startDiscovery(); diff --git a/package-lock.json b/package-lock.json index 19b8c6c..75212f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "4.2.0", "license": "MIT", "dependencies": { - "commander": "^11.1.0", + "@commander-js/extra-typings": "~11.1.0", + "commander": "~11.1.0", "lodash.castarray": "^4.4.0", "lodash.clone": "^4.5.0", "lodash.defaultto": "^4.14.0", @@ -63,7 +64,7 @@ "sinon": "^17.0.1", "sinon-chai": "^3.7.0", "source-map-support": "^0.5.21", - "tplink-smarthome-simulator": "^4.0.0", + "tplink-smarthome-simulator": "^5.0.1", "ts-essentials": "9.4.1", "ts-node": "10.9.1", "typedoc": "^0.25.3", @@ -432,17 +433,25 @@ "node": ">=6.9.0" } }, + "node_modules/@commander-js/extra-typings": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz", + "integrity": "sha512-GuvZ38d23H+7Tz2C9DhzCepivsOsky03s5NI+KCy7ke1FNUvsJ2oO47scQ9YaGGhgjgNW5OYYNSADmbjcSoIhw==", + "peerDependencies": { + "commander": "11.1.x" + } + }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-7.3.8.tgz", - "integrity": "sha512-Dj8iSGQyfgIsCjmXk9D/SjV7EpbpQSogeaGcBM66H33pd0GyGmLhn3biRN+vqi/vqWmsp75rT3kd5MKa8X5W9Q==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-7.3.9.tgz", + "integrity": "sha512-ebfrf5Zaw33bcqT80Qrkv7IGT7GI/CDp15bSk2EUmdORzk1SCKZl6L4vUo3NLMmxVwYioS+OQmsW8E88sJNyGg==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.2", "@cspell/dict-aws": "^4.0.0", "@cspell/dict-bash": "^4.1.2", - "@cspell/dict-companies": "^3.0.26", - "@cspell/dict-cpp": "^5.0.8", + "@cspell/dict-companies": "^3.0.27", + "@cspell/dict-cpp": "^5.0.9", "@cspell/dict-cryptocurrencies": "^4.0.0", "@cspell/dict-csharp": "^4.0.2", "@cspell/dict-css": "^4.0.12", @@ -451,35 +460,36 @@ "@cspell/dict-docker": "^1.1.7", "@cspell/dict-dotnet": "^5.0.0", "@cspell/dict-elixir": "^4.0.3", - "@cspell/dict-en_us": "^4.3.9", + "@cspell/dict-en_us": "^4.3.11", "@cspell/dict-en-common-misspellings": "^1.0.2", "@cspell/dict-en-gb": "1.1.33", - "@cspell/dict-filetypes": "^3.0.1", + "@cspell/dict-filetypes": "^3.0.2", "@cspell/dict-fonts": "^4.0.0", - "@cspell/dict-fsharp": "^1.0.0", + "@cspell/dict-fsharp": "^1.0.1", "@cspell/dict-fullstack": "^3.1.5", "@cspell/dict-gaming-terms": "^1.0.4", "@cspell/dict-git": "^2.0.0", - "@cspell/dict-golang": "^6.0.3", + "@cspell/dict-golang": "^6.0.4", "@cspell/dict-haskell": "^4.0.1", "@cspell/dict-html": "^4.0.5", "@cspell/dict-html-symbol-entities": "^4.0.0", "@cspell/dict-java": "^5.0.6", - "@cspell/dict-k8s": "^1.0.1", + "@cspell/dict-k8s": "^1.0.2", "@cspell/dict-latex": "^4.0.0", "@cspell/dict-lorem-ipsum": "^4.0.0", "@cspell/dict-lua": "^4.0.2", + "@cspell/dict-makefile": "^1.0.0", "@cspell/dict-node": "^4.0.3", "@cspell/dict-npm": "^5.0.12", - "@cspell/dict-php": "^4.0.3", + "@cspell/dict-php": "^4.0.4", "@cspell/dict-powershell": "^5.0.2", "@cspell/dict-public-licenses": "^2.0.5", - "@cspell/dict-python": "^4.1.9", + "@cspell/dict-python": "^4.1.10", "@cspell/dict-r": "^2.0.1", "@cspell/dict-ruby": "^5.0.1", "@cspell/dict-rust": "^4.0.1", "@cspell/dict-scala": "^5.0.0", - "@cspell/dict-software-terms": "^3.3.6", + "@cspell/dict-software-terms": "^3.3.9", "@cspell/dict-sql": "^2.1.2", "@cspell/dict-svelte": "^1.0.2", "@cspell/dict-swift": "^2.0.1", @@ -491,30 +501,30 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-7.3.8.tgz", - "integrity": "sha512-FxYJWtDgxIQYxdP0RWwRV8nzLfxVx8D8D5L2sbbP/0NFczDbq/zWYep4nSAHJT10aUJrogsVUYwNwdkr562wKA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-7.3.9.tgz", + "integrity": "sha512-QHsem5OZXshFX+Wdlx3VpdPi9WS7KgoBMGGJ4zQZ3lp81Rb1tRj0Ij/98whq882QOmAVQfr+uOHANHLnyPr0LQ==", "dev": true, "dependencies": { - "@cspell/cspell-types": "7.3.8" + "@cspell/cspell-types": "7.3.9" }, "engines": { "node": ">=16" } }, "node_modules/@cspell/cspell-pipe": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-7.3.8.tgz", - "integrity": "sha512-/vKPfiHM5bJUkNX12w9j533Lm2JvvSMKUCChM2AxYjy6vL8prc/7ei++4g2xAWwRxLZPg2OfpDJS5EirZNBJdA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-7.3.9.tgz", + "integrity": "sha512-gKYTHcryKOaTmr6t+M5h1sZnQ42eHeumBJejovphipXfdivedUnuYyQrrQGFAlUKzfEOWcOPME1nm17xsaX5Ww==", "dev": true, "engines": { "node": ">=16" } }, "node_modules/@cspell/cspell-resolver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-7.3.8.tgz", - "integrity": "sha512-CeyQmhqZI5a+T7a6oiVN90TFlzU3qVVYqCaZ9grFrVOsmzY9ipH5gmqfgMavaBOqb0di/+VZS8d02suMOXcKLQ==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-7.3.9.tgz", + "integrity": "sha512-2slYAGvi7EFLKyJ5hrYBNaFT2iyOEQM1pEIzm+PDuhNJE/9wuBY5pBVqIgFSPz53vsQvW9GJThNY8h1/2EH3ZA==", "dev": true, "dependencies": { "global-dirs": "^3.0.1" @@ -524,18 +534,18 @@ } }, "node_modules/@cspell/cspell-service-bus": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-7.3.8.tgz", - "integrity": "sha512-3E7gwY6QILrZH83p69i9CERbRBEqeBiKCIKnAd7U2PbxfFqG/P47fqpnarzSWFwFpU92oyGsYry+wC8TEGISRQ==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-7.3.9.tgz", + "integrity": "sha512-VyfK3qWtJZag4Fe/x1Oh/tqCNVGKGlQ2ArX1fVdmTVGQtZcbXuMKdZI80t4b8SGtzGINHufAdakpu3xucX/FrQ==", "dev": true, "engines": { "node": ">=16" } }, "node_modules/@cspell/cspell-types": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-7.3.8.tgz", - "integrity": "sha512-hsOtaULDnawEL4pU0fga941GhvE8mbTbywrJBx+eGX3fnJsaUr8XQzCtnLsW2ko7WCLWFItNEhSSTPQHBFRLsw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-7.3.9.tgz", + "integrity": "sha512-p7s8yEV6ASz0HjiArH11yjNj3vXzK2Ep94GrpdtYJxSxFC2w1mXAVUaJB/5+jC4+1YeYsmcBFTXmZ1rGMyTv3g==", "dev": true, "engines": { "node": ">=16" @@ -733,6 +743,12 @@ "integrity": "sha512-eeC20Q+UnHcTVBK6pgwhSjGIVugO2XqU7hv4ZfXp2F9DxGx1RME0+1sKX4qAGhdFGwOBsEzb2fwUsAEP6Mibpg==", "dev": true }, + "node_modules/@cspell/dict-makefile": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-makefile/-/dict-makefile-1.0.0.tgz", + "integrity": "sha512-3W9tHPcSbJa6s0bcqWo6VisEDTSN5zOtDbnPabF7rbyjRpNo0uHXHRJQF8gAbFzoTzBBhgkTmrfSiuyQm7vBUQ==", + "dev": true + }, "node_modules/@cspell/dict-node": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@cspell/dict-node/-/dict-node-4.0.3.tgz", @@ -833,21 +849,21 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-7.3.8.tgz", - "integrity": "sha512-s8x7dH/ScfW0pFEIvNFo4JOR7YmvM2wZSHOykmWTJCQ8k2EQ/+uECPp6ZxkoJoukTz8sj+3KzF0fRl5mKxPd6g==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-7.3.9.tgz", + "integrity": "sha512-P6tAmDVhrW03hmhetxhBKlNTYwL2lk8ZehYQwSpXaLnaFrS3xrQvfUaJ3Mj9W2CIMzSYXlLmPO2FLRhXK2dnEw==", "dev": true, "dependencies": { - "import-meta-resolve": "^3.0.0" + "import-meta-resolve": "^3.1.1" }, "engines": { "node": ">=16" } }, "node_modules/@cspell/strong-weak-map": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-7.3.8.tgz", - "integrity": "sha512-qNnt2wG45wb8JP54mENarnQgxfSYKPp3zlYID/2przbMNmVJRqUlcIBOdLI6plCgGeNkzJTl3T9T1ATbnN+LLw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-7.3.9.tgz", + "integrity": "sha512-XKpw/p3+EN+PWiFAWc45RJPI9zQRkPSVdUFeZb0YLseWF/CkogScgIe4CLfMLITiVbP0X/FKk90+aTPfAU38kg==", "dev": true, "engines": { "node": ">=16" @@ -1378,33 +1394,33 @@ "dev": true }, "node_modules/@types/chai": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.9.tgz", - "integrity": "sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==", "dev": true }, "node_modules/@types/chai-as-promised": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.7.tgz", - "integrity": "sha512-APucaP5rlmTRYKtRA6FE5QPP87x76ejw5t5guRJ4y5OgMnwtsvigw7HHhKZlx2MGXLeZd6R/GNZR/IqDHcbtQw==", + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", "dev": true, "dependencies": { "@types/chai": "*" } }, "node_modules/@types/chai-subset": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.4.tgz", - "integrity": "sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", + "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", "dev": true, "dependencies": { "@types/chai": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", - "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "version": "8.44.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", + "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", "dev": true, "dependencies": { "@types/estree": "*", @@ -1412,15 +1428,15 @@ } }, "node_modules/@types/estree": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz", - "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/json5": { @@ -1430,108 +1446,108 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.200", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", - "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", + "version": "4.14.201", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==", "dev": true }, "node_modules/@types/lodash.castarray": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@types/lodash.castarray/-/lodash.castarray-4.4.8.tgz", - "integrity": "sha512-LBAyYcRc7YTogcuf2Xe8JYZEeWFOLAgVZ3jHAT9Cf3VAYoM7m7+p72YNsGxaKydDoO4Di6Ue1C9X2XNQaF5vSw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@types/lodash.castarray/-/lodash.castarray-4.4.9.tgz", + "integrity": "sha512-gesQ7ozv8DXMO+XmAFQpUTQMnAGE8P3CiGOMGc84TYFvayPSmncXbeZK4s5zg/Uw8PlAgv2AMH70G7MvrucK5w==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/lodash.clone": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/@types/lodash.clone/-/lodash.clone-4.5.8.tgz", - "integrity": "sha512-wG0XTl97omRBoXzH91Y2hLK4hLJpsOp5J/9xG3YCHhJLqNRy/Ka/gVhbdDBAOA0LHcVHokhZmtzcuiajPeH7ng==", + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clone/-/lodash.clone-4.5.9.tgz", + "integrity": "sha512-euFSUq+8csIliszqTBSMh7eU41/by1JPQhTNa2gq3dncxC2Z6s87bbzVB9UUBdYDbV+FV5whFDGxz6hOKTY/EQ==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/lodash.get": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.8.tgz", - "integrity": "sha512-XK+co6sBkJxh1vaVP8al6cAA17dX//RNCknGG8JhpHFJfxq/GXKAYB9NKheG22pu2xpWpxfFd65W08EhH2IFlg==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.9.tgz", + "integrity": "sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/lodash.groupby": { - "version": "4.6.8", - "resolved": "https://registry.npmjs.org/@types/lodash.groupby/-/lodash.groupby-4.6.8.tgz", - "integrity": "sha512-+VbBhRhzo6g6q5RdVQXlU1vwbYVodEkS9ZCVuqHtZvhlSu1muQLNYYR1yhyYwAcSz7gMDOHlWPnPvAoQqV4rlg==", + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.groupby/-/lodash.groupby-4.6.9.tgz", + "integrity": "sha512-z2xtCX2ko7GrqORnnYea4+ksT7jZNAvaOcLd6mP9M7J09RHvJs06W8BGdQQAX8ARef09VQLdeRilSOcfHlDQJQ==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/lodash.isequal": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.7.tgz", - "integrity": "sha512-UJQsb7aW8JU/h3fivQXVRDp9EKi98T9iQcVeTXBxcD4jApgGgbrET/0hVS6vH/YoYpqkcToMU5fSNPEiWVZgDg==", + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", + "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/minimist": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", - "integrity": "sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, "node_modules/@types/mocha": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.3.tgz", - "integrity": "sha512-RsOPImTriV/OE4A9qKjMtk2MnXiuLLbcO3nCXK+kvq4nr0iMfFgpjaX3MPLb6f7+EL1FGSelYvuJMV6REH+ZPQ==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz", + "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==", "dev": true }, "node_modules/@types/node": { - "version": "20.8.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/normalize-package-data": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz", - "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, "node_modules/@types/promise-queue": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@types/promise-queue/-/promise-queue-2.2.2.tgz", - "integrity": "sha512-SFLA8/UdR5eGtrP5f6KkLZS9meOMcLOu0kIzMbIU2ITDMkiEG2k0fQtCDbV4nZldRjCKA7n9WLN3tktlepmWtg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@types/promise-queue/-/promise-queue-2.2.3.tgz", + "integrity": "sha512-CuEQpGSYKvHr3SQ7C7WkluLg9CFjVORbn8YFRsQ5u6mqGbZVfSOv03ic9t95HtZuMchnlNqnIsQGFOpxqdhjTQ==", "dev": true }, "node_modules/@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "dev": true }, "node_modules/@types/sinon": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.0.tgz", - "integrity": "sha512-oN4AeDMFCeNZrAffCjhLcwwVymRZL2c9mljUmhPnd0eiM06d4ELDg0Q0TSvnZXrCIFlSA859qIdcfu1HapswPQ==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA==", "dev": true, "dependencies": { "@types/sinonjs__fake-timers": "*" } }, "node_modules/@types/sinon-chai": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.11.tgz", - "integrity": "sha512-1C5SBFzwn9hjiMr1xfqbULcSI9qXVpkGZT/LYbbd3jWiTo2MSvA+iFfwODlSoAXGeCgBw6S509dxy8zSIacr3Q==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", "dev": true, "dependencies": { "@types/chai": "*", @@ -1539,9 +1555,9 @@ } }, "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz", - "integrity": "sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -3048,23 +3064,23 @@ } }, "node_modules/cspell": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-7.3.8.tgz", - "integrity": "sha512-8AkqsBQAMsKYV5XyJLB6rBs5hgspL4+MPOg6mBKG2j5EvQgRVc6dIfAPWDNLpIeW2a3+7K5BIWqKHapKPeiknQ==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-7.3.9.tgz", + "integrity": "sha512-QzunjO9CmV5+98UfG4ONhvPtrcAC6Y2pEKeOrp5oPeyAI7HwgxmfsR3ybHRlMPAGcwKtDOurBKxM7jqXNwkzmA==", "dev": true, "dependencies": { - "@cspell/cspell-json-reporter": "7.3.8", - "@cspell/cspell-pipe": "7.3.8", - "@cspell/cspell-types": "7.3.8", - "@cspell/dynamic-import": "7.3.8", + "@cspell/cspell-json-reporter": "7.3.9", + "@cspell/cspell-pipe": "7.3.9", + "@cspell/cspell-types": "7.3.9", + "@cspell/dynamic-import": "7.3.9", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^11.1.0", - "cspell-gitignore": "7.3.8", - "cspell-glob": "7.3.8", - "cspell-io": "7.3.8", - "cspell-lib": "7.3.8", - "fast-glob": "^3.3.1", + "cspell-gitignore": "7.3.9", + "cspell-glob": "7.3.9", + "cspell-io": "7.3.9", + "cspell-lib": "7.3.9", + "fast-glob": "^3.3.2", "fast-json-stable-stringify": "^2.1.0", "file-entry-cache": "^7.0.1", "get-stdin": "^9.0.0", @@ -3084,14 +3100,14 @@ } }, "node_modules/cspell-dictionary": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-7.3.8.tgz", - "integrity": "sha512-gkq4t78eLR0xC3P0vDDHPeNY4iZRd5YE6Z8uDJ7RM4UaX/TSdVUN9KNFr34RnJ119NYVHujpL9+uW7wPSAe8Eg==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-7.3.9.tgz", + "integrity": "sha512-lkWfX5QNbs4yKqD9wa+G+NHRWmLgFdyposgJOyd/ojDbx99CDPMhMhg9pyMKdYl6Yt8kjMow58/i12EYvD8wnA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "7.3.8", - "@cspell/cspell-types": "7.3.8", - "cspell-trie-lib": "7.3.8", + "@cspell/cspell-pipe": "7.3.9", + "@cspell/cspell-types": "7.3.9", + "cspell-trie-lib": "7.3.9", "fast-equals": "^4.0.3", "gensequence": "^6.0.0" }, @@ -3106,12 +3122,12 @@ "dev": true }, "node_modules/cspell-gitignore": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-7.3.8.tgz", - "integrity": "sha512-vJzCOUEiw6/MwV/U4Ux3bgSdj9mXB+X5eHL+qzVoyFI7ArlvrkuGTL+iFJThQcS8McM3SGqtvaBNCiKBmAeCkA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-7.3.9.tgz", + "integrity": "sha512-DLuu+K2q4xYNL4DpLyysUeiGU/NYYoObzfOYiISzOKYpi3aFLiUaiyfF6xWGsahmlijif+8bwSsIMmcvGa5dgA==", "dev": true, "dependencies": { - "cspell-glob": "7.3.8", + "cspell-glob": "7.3.9", "find-up": "^5.0.0" }, "bin": { @@ -3122,9 +3138,9 @@ } }, "node_modules/cspell-glob": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-7.3.8.tgz", - "integrity": "sha512-wUZC6znyxEs0wlhzGfZ4XHkATPJyazJIFi/VvAdj+KHe7U8SoSgitJVDQqdgectI2y3MxR7lQdVLX9dONFh+7A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-7.3.9.tgz", + "integrity": "sha512-7PaTkCzJWjQex3men857v3ExF7Q10jbQkfD+wdln2te9iNFd+HEkstA173vb828D9yeib1q1of8oONr2SeGycg==", "dev": true, "dependencies": { "micromatch": "^4.0.5" @@ -3134,13 +3150,13 @@ } }, "node_modules/cspell-grammar": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-7.3.8.tgz", - "integrity": "sha512-nTjAlMAZAVSFhBd9U3MB9l5FfC5JCCr9DTOA2wWxusVOm+36MbSEH90ucLPkhPa9/+0HtbpDhqVMwXCZllRpsg==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-7.3.9.tgz", + "integrity": "sha512-s1QOPg4AxWE8XBewDQLe14j0uDyWGjREfm4dZFTrslAZUrQ8/df5s152M5LtgOEza33FrkKKE2axbGvgS9O7sQ==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "7.3.8", - "@cspell/cspell-types": "7.3.8" + "@cspell/cspell-pipe": "7.3.9", + "@cspell/cspell-types": "7.3.9" }, "bin": { "cspell-grammar": "bin.mjs" @@ -3150,12 +3166,12 @@ } }, "node_modules/cspell-io": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-7.3.8.tgz", - "integrity": "sha512-XrxPbaiek7EZh+26k9RYVz2wKclaMqM6mXBiu/kpFAHRHHfz91ado6xWvyxZ7UAxQ8ixEwZ+oz9TU+k21gHzyw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-7.3.9.tgz", + "integrity": "sha512-IbXOYaDxLg94uijv13kqb+6PQjEwGboQYtABuZs2+HuUVW89K2tE+fQcEhkAsrZ11sDj5lUqgEQj9omvknZSuA==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "7.3.8", + "@cspell/cspell-service-bus": "7.3.9", "node-fetch": "^2.7.0" }, "engines": { @@ -3163,26 +3179,26 @@ } }, "node_modules/cspell-lib": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-7.3.8.tgz", - "integrity": "sha512-2L770sI5DdsAKVzO3jxmfP2fz4LryW6dzL93BpN7WU+ebFC6rg4ioa5liOJV4WoDo2fNQMSeqfW4Aawu9zWR7A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-7.3.9.tgz", + "integrity": "sha512-eFYYs8XoYmdu78UxrPisD+hAoXOLaLzcevKf9+oDPDgJmHpkGoFgbIBnHMRIsAM1e+QDS6OlWG/rybhZTqanCQ==", "dev": true, "dependencies": { - "@cspell/cspell-bundled-dicts": "7.3.8", - "@cspell/cspell-pipe": "7.3.8", - "@cspell/cspell-resolver": "7.3.8", - "@cspell/cspell-types": "7.3.8", - "@cspell/dynamic-import": "7.3.8", - "@cspell/strong-weak-map": "7.3.8", + "@cspell/cspell-bundled-dicts": "7.3.9", + "@cspell/cspell-pipe": "7.3.9", + "@cspell/cspell-resolver": "7.3.9", + "@cspell/cspell-types": "7.3.9", + "@cspell/dynamic-import": "7.3.9", + "@cspell/strong-weak-map": "7.3.9", "clear-module": "^4.1.2", "comment-json": "^4.2.3", "configstore": "^6.0.0", "cosmiconfig": "8.0.0", - "cspell-dictionary": "7.3.8", - "cspell-glob": "7.3.8", - "cspell-grammar": "7.3.8", - "cspell-io": "7.3.8", - "cspell-trie-lib": "7.3.8", + "cspell-dictionary": "7.3.9", + "cspell-glob": "7.3.9", + "cspell-grammar": "7.3.9", + "cspell-io": "7.3.9", + "cspell-trie-lib": "7.3.9", "fast-equals": "^5.0.1", "find-up": "^6.3.0", "gensequence": "^6.0.0", @@ -3278,13 +3294,13 @@ } }, "node_modules/cspell-trie-lib": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-7.3.8.tgz", - "integrity": "sha512-UQx1Bazbyz2eQJ/EnMohINnUdZvAQL+OcQU3EPPbNWM1DWF4bJGgmFXKNCRYfJk6wtOZVXG5g5AZXx9KnHeN9A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-7.3.9.tgz", + "integrity": "sha512-aTWm2KYXjQ+MlM6kB37wmTV9RU8+fgZYkiFfMc48M0MhBc6XkHUibMGrFAS29gp+B70kWPxe+VHLmFIk9pRPyg==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "7.3.8", - "@cspell/cspell-types": "7.3.8", + "@cspell/cspell-pipe": "7.3.9", + "@cspell/cspell-types": "7.3.9", "gensequence": "^6.0.0" }, "engines": { @@ -3638,9 +3654,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.576", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", - "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "version": "1.4.579", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.579.tgz", + "integrity": "sha512-bJKvA+awBIzYR0xRced7PrQuRIwGQPpo6ZLP62GAShahU9fWpsNN2IP6BSP1BLDDSbxvBVRGAMWlvVVq3npmLA==", "dev": true }, "node_modules/emoji-regex": { @@ -5777,9 +5793,9 @@ "dev": true }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.1.tgz", - "integrity": "sha512-opCrKqbthmq3SKZ10mFMQG9dk3fTa3quaOLD35kJa5ejwZHd9xAr+kLuziiZz2cG32s4lMZxNdmdcEQnTDP4+g==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" @@ -8273,6 +8289,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", @@ -8981,19 +9007,20 @@ } }, "node_modules/tplink-smarthome-simulator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tplink-smarthome-simulator/-/tplink-smarthome-simulator-4.0.0.tgz", - "integrity": "sha512-ad/iWIw+6LqKSe577EFM270JO/7+80o6Vr0FEoOiO1cAh3UUfTReV4f3UVgDon8tmM5b0QBkN03VRjuhjzDzfw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/tplink-smarthome-simulator/-/tplink-smarthome-simulator-5.0.1.tgz", + "integrity": "sha512-LqrcjBoou3pPxSQEZdHXcGuH3kLF5E1he6MYC4OUgteTGHt5yGtMIgklPIZRyaGLyvAVbzREMXklkRyI6RKVHQ==", "dev": true, "dependencies": { "debug": "^4.3.4", "jsonparse": "^1.3.1", "lodash.defaultsdeep": "^4.6.1", "lodash.merge": "^4.6.2", - "tplink-smarthome-crypto": "^4.0.0" + "tplink-smarthome-crypto": "^4.0.0", + "typed-emitter": "^2.1.0" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/tr46": { @@ -9226,6 +9253,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "dev": true, + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 443e971..99cd9f3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "test:only": "cross-env NODE_ENV=test && nyc mocha --color" }, "dependencies": { - "commander": "^11.1.0", + "@commander-js/extra-typings": "~11.1.0", + "commander": "~11.1.0", "lodash.castarray": "^4.4.0", "lodash.clone": "^4.5.0", "lodash.defaultto": "^4.14.0", @@ -97,7 +98,7 @@ "sinon": "^17.0.1", "sinon-chai": "^3.7.0", "source-map-support": "^0.5.21", - "tplink-smarthome-simulator": "^4.0.0", + "tplink-smarthome-simulator": "^5.0.1", "ts-essentials": "9.4.1", "ts-node": "10.9.1", "typedoc": "^0.25.3", diff --git a/src/bulb/index.ts b/src/bulb/index.ts index 0dca168..7c71681 100644 --- a/src/bulb/index.ts +++ b/src/bulb/index.ts @@ -2,13 +2,28 @@ import isEqual from 'lodash.isequal'; import type { SendOptions } from '../client'; -import Device, { isBulbSysinfo } from '../device'; -import type { CommonSysinfo, DeviceConstructorOptions } from '../device'; -import Cloud from '../shared/cloud'; -import Emeter, { RealtimeNormalized } from '../shared/emeter'; -import Lighting, { LightState, LightStateInput } from './lighting'; -import Schedule from './schedule'; +import Device, { + isBulbSysinfo, + type CommonSysinfo, + type DeviceConstructorOptions, +} from '../device'; +import Cloud, { isCloudInfo, type CloudInfo } from '../shared/cloud'; +import type { Realtime, RealtimeNormalized } from '../shared/emeter'; +import Emeter from '../shared/emeter'; import Time from '../shared/time'; +import { + extractResponse, + hasErrCode, + isObjectLike, + objectHasKey, + type HasErrCode, +} from '../utils'; +import Lighting, { + isLightState, + type LightState, + type LightStateInput, +} from './lighting'; +import Schedule from './schedule'; function isLightStrip(sysinfo: BulbSysinfo) { return (sysinfo.length ?? 0) > 0; @@ -352,7 +367,7 @@ class Bulb extends Device { const response = await super.getSysInfo(sendOptions); if (!isBulbSysinfo(response)) { - throw new Error(`Unexpected Response: ${response}`); + throw new Error(`Unexpected Response: ${JSON.stringify(response)}`); } return this.sysInfo; @@ -382,13 +397,50 @@ class Bulb extends Device { `{"${this.apiModules.emeter}":{"get_realtime":{}},"${this.apiModules.lightingservice}":{"get_light_state":{}},"${this.apiModules.schedule}":{"get_next_action":{}},"system":{"get_sysinfo":{}},"${this.apiModules.cloud}":{"get_info":{}}}`, sendOptionsForGetInfo, ); - const data = JSON.parse(response); - this.setSysInfo(data.system.get_sysinfo); - this.cloud.info = data[this.apiModules.cloud].get_info; - this.emeter.setRealtime(data[this.apiModules.emeter].get_realtime); - this.schedule.nextAction = data[this.apiModules.schedule].get_next_action; - this.lighting.lightState = - data[this.apiModules.lightingservice].get_light_state; + const data: unknown = JSON.parse(response); + + const sysinfo = extractResponse( + data, + 'system.get_sysinfo', + isBulbSysinfo, + ); + this.setSysInfo(sysinfo); + + const cloudInfo = extractResponse( + data, + [this.apiModules.cloud, 'get_info'], + (c) => isCloudInfo(c) && hasErrCode(c), + ); + this.cloud.info = cloudInfo; + + const emeterKey = this.apiModules.emeter; + if ( + isObjectLike(data) && + objectHasKey(data, emeterKey) && + isObjectLike(data[emeterKey]) && + objectHasKey(data[emeterKey], 'get_realtime') && + // @ts-expect-error: limitation of TS type checking + isObjectLike(data[emeterKey].get_realtime) + ) { + // @ts-expect-error: limitation of TS type checking + const realtime = data[emeterKey].get_realtime as Realtime; + this.emeter.setRealtime(realtime); + } + + const scheduleNextAction = extractResponse( + data, + [this.apiModules.schedule, 'get_next_action'], + hasErrCode, + ); + this.schedule.nextAction = scheduleNextAction; + + const lightState = extractResponse( + data, + [this.apiModules.lightingservice, 'get_light_state'], + isLightState, + ); + this.lighting.lightState = lightState; + return { sysInfo: this.sysInfo, cloud: { info: this.cloud.info }, @@ -488,7 +540,6 @@ class Bulb extends Device { const { light_state: sysinfoLightState } = this._sysInfo; - if (sysinfoLightState == null) return; const powerOn = sysinfoLightState.on_off === 1; if (this.lastState.powerOn !== powerOn) { diff --git a/src/bulb/lighting.ts b/src/bulb/lighting.ts index 595ba2f..73dfcbf 100644 --- a/src/bulb/lighting.ts +++ b/src/bulb/lighting.ts @@ -1,12 +1,13 @@ import isEqual from 'lodash.isequal'; + import type { SendOptions } from '../client'; -import type Bulb from '.'; import { extractResponse, hasErrCode, isDefinedAndNotNull, isObjectLike, } from '../utils'; +import type Bulb from './index'; export interface LightState { /** @@ -113,7 +114,6 @@ export default class Lighting { } private emitEvents(): void { - if (!this.#lightState) return; const powerOn = this.#lightState.on_off === 1; if (this.lastState.powerOn !== powerOn) { @@ -141,7 +141,7 @@ export default class Lighting { * @throws {@link ResponseError} */ async getLightState(sendOptions?: SendOptions): Promise { - this.lightState = extractResponse( + this.lightState = extractResponse( await this.device.sendCommand( { [this.apiModuleName]: { get_light_state: {} }, @@ -151,7 +151,7 @@ export default class Lighting { ), '', isLightStateResponse, - ) as LightStateResponse; + ); return this.lightState; } @@ -191,7 +191,7 @@ export default class Lighting { if (isDefinedAndNotNull(brightness)) state.brightness = brightness; if (isDefinedAndNotNull(color_temp)) state.color_temp = color_temp; - const response = extractResponse( + const response = extractResponse( await this.device.sendCommand( { [this.apiModuleName]: { [this.setLightStateMethodName]: state }, @@ -201,7 +201,7 @@ export default class Lighting { ), '', isLightStateResponse, - ) as LightStateResponse; + ); // The light strip in particular returns more detail with get(), so only // apply the subset that is returned with set() diff --git a/src/bulb/schedule.ts b/src/bulb/schedule.ts index caa0567..6dfc908 100644 --- a/src/bulb/schedule.ts +++ b/src/bulb/schedule.ts @@ -1,6 +1,6 @@ /* eslint camelcase: ["off"] */ -import type Bulb from '.'; +import type Bulb from './index'; import Schedule, { createScheduleRule, ScheduleRule } from '../shared/schedule'; import type { ScheduleRuleInputTime } from '../shared/schedule'; import type { LightState } from './lighting'; diff --git a/src/cli.ts b/src/cli.ts index c2f525c..738b8d6 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,19 +1,35 @@ #!/usr/bin/env node /* eslint-disable no-console */ +import { Command } from '@commander-js/extra-typings'; import castArray from 'lodash.castarray'; -import { program } from 'commander'; import type { LogLevelDesc } from 'loglevel'; import * as tplinkCrypto from 'tplink-smarthome-crypto'; import type { PickProperties } from 'ts-essentials'; import util from 'util'; -import { Client, ResponseError } from '.'; -import { SendOptions, AnyDevice } from './client'; +import { type AnyDevice, type SendOptions } from './client'; +import { Client, ResponseError } from './index'; let logLevel: LogLevelDesc; -function outputError(err: Error | unknown): void { +function toInt(s: string): number { + return parseInt(s, 10); +} + +const program = new Command() + .option('-D, --debug', 'turn on debug level logging', () => { + logLevel = 'debug'; + }) + .option('-t, --timeout ', 'timeout (ms)', toInt, 10000) + .option('-u, --udp', 'send via UDP') + .option( + '-c, --color [on]', + 'output will be styled with ANSI color codes', + 'on', + ); + +function outputError(err: unknown): void { if (err instanceof ResponseError) { console.log('Response Error:'); console.log(err.response); @@ -51,7 +67,7 @@ function search( console.log(`startDiscovery(${util.inspect(commandParams)})`); getClient() .startDiscovery(commandParams) - .on('device-new', (device) => { + .on('device-new', (device: AnyDevice) => { console.log( `${device.model} ${device.deviceType} ${device.type} ${device.host} ${device.port} ${device.macNormalized} ${device.deviceId} ${device.alias}`, ); @@ -69,7 +85,7 @@ function search( async function send( host: string, - port: number, + port: number | undefined, payload: string, ): Promise { try { @@ -89,8 +105,8 @@ async function send( async function sendCommand( host: string, - port: number, - childId: string, + port: number | undefined, + childId: string | undefined, payload: string, ): Promise { try { @@ -100,7 +116,11 @@ async function sendCommand( childId ? `childId: ${childId}` : '' } via ${client.defaultSendOptions.transport}...`, ); - const device = await client.getDevice({ host, port, childId }); + const device = await client.getDevice({ + host, + port, + childId, + }); const results = await device.sendCommand(payload); console.log('response:'); console.dir(results, { colors: program.opts().color === 'on', depth: 10 }); @@ -111,7 +131,7 @@ async function sendCommand( async function sendCommandDynamic( host: string, - port: number, + port: number | undefined, // eslint-disable-next-line @typescript-eslint/ban-types command: Exclude, undefined>, commandParams: Array = [], @@ -133,6 +153,7 @@ async function sendCommandDynamic( // eslint-disable-next-line @typescript-eslint/no-explicit-any const func = device[command] as (...args: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const results = await func.apply(device, [ ...commandParams, { ...sendOptions }, @@ -145,7 +166,7 @@ async function sendCommandDynamic( } } -async function details(host: string, port: number): Promise { +async function details(host: string, port: number | undefined): Promise { try { console.log(`Getting details from ${host}:${port || ''}...`); const device = await getClient().getDevice({ host, port }); @@ -168,17 +189,16 @@ async function details(host: string, port: number): Promise { } } -async function blink( +function blink( host: string, - port: number, + port: number | undefined, times: number, rate: number, -): Promise { - console.log(`Sending blink commands to ${host}:${port || ''}...`); - getClient() +): Promise { + console.log(`Sending blink commands to ${host}:${port ?? ''}...`); + return getClient() .getDevice({ host, port }) .then((device) => { - // @ts-expect-error: ignoring for now, need to implement blink on bulb return device.blink(times, rate).then(() => { console.log('Blinking complete'); }); @@ -188,9 +208,9 @@ async function blink( }); } -async function getScanInfo( +function getScanInfo( host: string, - port: number, + port: number | undefined, refresh?: boolean, timeoutInSeconds?: number, ) { @@ -209,10 +229,6 @@ async function getScanInfo( }); } -function toInt(s: string): number { - return parseInt(s, 10); -} - function toBoolean(s: string): boolean { return s === 'true' || s === '1'; } @@ -242,82 +258,89 @@ function setParamTypes( return params; } -program - .option('-D, --debug', 'turn on debug level logging', () => { - logLevel = 'debug'; - }) - .option('-t, --timeout ', 'timeout (ms)', toInt, 10000) - .option('-u, --udp', 'send via UDP') - .option( - '-c, --color [on]', - 'output will be styled with ANSI color codes', - 'on', - ); - program .command('search [params]') .description('Search for devices') .option('--broadcast
', 'broadcast address', '255.255.255.255') - .option('-s, --sysinfo', 'output sysInfo') + .option('-s, --sysinfo', 'output sysInfo', false) .option( '-b, --breakout-children', 'output children (multi-outlet plugs)', - true, + false, ) .action((params, options) => { let paramsObj; if (params) { console.dir(params); - paramsObj = JSON.parse(params); + paramsObj = JSON.parse(params) as Parameters[0]; } search( options.sysinfo, - options.breakoutChildren || false, + options.breakoutChildren, program.opts().timeout, options.broadcast, paramsObj, ); }); +function parseHost(hostString: string): [string, number | undefined] { + const [hostOnly, port] = hostString.split(':'); + if (hostOnly == null || hostOnly.length === 0) + throw new Error('host is required'); + if (port != null && port.length > 0) { + return [hostOnly, toInt(port)]; + } + return [hostOnly, undefined]; +} + program .command('send ') .description('Send payload to device (using Client.send)') .action((host, payload) => { - const [hostOnly, port] = host.split(':'); - send(hostOnly, port, payload); + const [hostOnly, port] = parseHost(host); + send(hostOnly, port, payload).catch((err) => { + outputError(err); + }); }); program .command('sendCommand ') .description('Send payload to device (using Device#sendCommand)') - .option('--childId [childId]', 'childId') + .option('--childId ', 'childId') .action((host, payload, options) => { - const [hostOnly, port] = host.split(':'); - sendCommand(hostOnly, port, options.childId, payload); + const [hostOnly, port] = parseHost(host); + sendCommand(hostOnly, port, options.childId, payload).catch((err) => { + outputError(err); + }); }); program.command('details ').action((host) => { - const [hostOnly, port] = host.split(':'); - details(hostOnly, port); + const [hostOnly, port] = parseHost(host); + details(hostOnly, port).catch((err) => { + outputError(err); + }); }); program - .command('blink [times] [rate]') + .command('blink') + .argument('') + .argument('[times]', '', toInt) + .argument('[rate]', '', toInt) .action((host, times = 5, rate = 500) => { - const [hostOnly, port] = host.split(':'); - blink(hostOnly, port, times, rate); + const [hostOnly, port] = parseHost(host); + blink(hostOnly, port, times, rate).catch((err) => { + outputError(err); + }); }); program - .command('getScanInfo [refresh] [timeoutInSeconds]') + .command('getScanInfo') + .argument('') + .argument('[refresh]', '', toBoolean) + .argument('[timeoutInSeconds]', '', toInt) .action((host, refresh = true, timeoutInSeconds = 5) => { - const [hostOnly, port] = host.split(':'); - getScanInfo( - hostOnly, - port, - refresh !== undefined ? toBoolean(refresh) : undefined, - timeoutInSeconds, - ); + const [hostOnly, port] = parseHost(host); + getScanInfo(hostOnly, port, refresh, timeoutInSeconds); }); type CommandSetup = { @@ -368,26 +391,26 @@ for (const command of commandSetup) { .description( `Send ${command.name} to device (using Device#${command.name})`, ) - .option('-t, --timeout [timeout]', 'timeout (ms)', toInt, 10000); + .option('-t, --timeout ', 'timeout (ms)', toInt, 10000); if (command.supportsChildId) { - cmd.option('-c, --childId [childId]', 'childId'); + cmd.option('-c, --childId ', 'childId'); } - cmd.action((...args) => { - // commander provides last parameter as reference to current command - // remove it with slice - const [host, arg2, arg3] = args.slice(0, -1); - const [hostOnly, port] = host.split(':'); - - // signatures will be either: - // host, options (arg2), params (arg3) - // host, params (arg2) - const options = arg3 === undefined ? arg2 : arg3; - const params = arg3 === undefined ? undefined : arg2; + cmd.action(function action(this: Command) { + const [host, ...params] = this.args; + const [hostOnly, port] = parseHost(host as string); + const options = this.opts() as { timeout?: number; childId?: string }; const commandParams = setParamTypes(params, command); - const { childId, ...sendOptions } = options; + // // @ts-expect-error: childId is added conditionally and is optional + const childId = options.childId || undefined; + + let sendOptions; + if (options.timeout != null) { + sendOptions = { timeout: options.timeout }; + } + sendCommandDynamic( hostOnly, port, @@ -395,36 +418,50 @@ for (const command of commandSetup) { commandParams, sendOptions, childId, - ); + ).catch((err) => { + outputError(err); + }); }); } program - .command('encrypt [firstKey=0xAB]') + .command('encrypt') + .argument('') + .argument('') + .argument('[firstKey=0xAB]', '', toInt) .action((outputEncoding, input, firstKey = 0xab) => { const outputBuf = tplinkCrypto.encrypt(input, firstKey); - console.log(outputBuf.toString(outputEncoding)); + console.log(outputBuf.toString(outputEncoding as BufferEncoding)); }); program - .command('encryptWithHeader [firstKey=0xAB]') + .command('encryptWithHeader') + .argument('') + .argument('') + .argument('[firstKey=0xAB]', '', toInt) .action((outputEncoding, input, firstKey = 0xab) => { const outputBuf = tplinkCrypto.encryptWithHeader(input, firstKey); - console.log(outputBuf.toString(outputEncoding)); + console.log(outputBuf.toString(outputEncoding as BufferEncoding)); }); program - .command('decrypt [firstKey=0xAB]') + .command('decrypt') + .argument('') + .argument('') + .argument('[firstKey=0xAB]', '', toInt) .action((inputEncoding, input, firstKey = 0xab) => { - const inputBuf = Buffer.from(input, inputEncoding); + const inputBuf = Buffer.from(input, inputEncoding as BufferEncoding); const outputBuf = tplinkCrypto.decrypt(inputBuf, firstKey); console.log(outputBuf.toString()); }); program - .command('decryptWithHeader [firstKey=0xAB]') + .command('decryptWithHeader') + .argument('') + .argument('') + .argument('[firstKey=0xAB]', '', toInt) .action((inputEncoding, input, firstKey = 0xab) => { - const inputBuf = Buffer.from(input, inputEncoding); + const inputBuf = Buffer.from(input, inputEncoding as BufferEncoding); const outputBuf = tplinkCrypto.decryptWithHeader(inputBuf, firstKey); console.log(outputBuf.toString()); }); diff --git a/src/client.ts b/src/client.ts index c117cc9..a251793 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,21 +1,19 @@ -import { Socket, createSocket, RemoteInfo } from 'dgram'; +import { RemoteInfo, Socket, createSocket } from 'dgram'; import { EventEmitter } from 'events'; import util from 'util'; import type log from 'loglevel'; -import { encrypt, decrypt } from 'tplink-smarthome-crypto'; +import { decrypt, encrypt } from 'tplink-smarthome-crypto'; import type { MarkOptional } from 'ts-essentials'; -import Device, { isBulbSysinfo, isPlugSysinfo } from './device'; -import type { Sysinfo } from './device'; import Bulb from './bulb'; -import Plug, { hasSysinfoChildren } from './plug'; -import createLogger from './logger'; -import type { Logger } from './logger'; +import Device, { isBulbSysinfo, isPlugSysinfo, type Sysinfo } from './device'; +import createLogger, { type Logger } from './logger'; import TcpConnection from './network/tcp-connection'; import UdpConnection from './network/udp-connection'; +import Plug, { hasSysinfoChildren } from './plug'; +import type { Realtime } from './shared/emeter'; import { compareMac, isObjectLike } from './utils'; -import { Realtime } from './shared/emeter'; const discoveryMsgBuf = encrypt('{"system":{"get_sysinfo":{}}}'); @@ -374,7 +372,7 @@ export default class Client extends EventEmitter implements ClientEventEmitter { sendOptions, ); - const responseObj = JSON.parse(response); + const responseObj: unknown = JSON.parse(response); if (isSysinfoResponse(responseObj)) { return responseObj.system.get_sysinfo; } @@ -390,14 +388,17 @@ export default class Client extends EventEmitter implements ClientEventEmitter { // Add device- / plug- / bulb- to eventName let ret = false; if (args[0] instanceof Device) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument if (super.emit(`device-${eventName}`, ...args)) { ret = true; } if (args[0].deviceType !== 'device') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument if (super.emit(`${args[0].deviceType}-${eventName}`, ...args)) { ret = true; } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument } else if (super.emit(eventName, ...args)) { ret = true; } @@ -572,8 +573,10 @@ export default class Client extends EventEmitter implements ClientEventEmitter { let response: DiscoveryResponse; let sysInfo: Sysinfo; try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment response = JSON.parse(decryptedMsg); sysInfo = response.system.get_sysinfo; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (sysInfo == null) throw new Error('system.get_sysinfo is null or undefined'); if (!isObjectLike(sysInfo)) @@ -609,6 +612,7 @@ export default class Client extends EventEmitter implements ClientEventEmitter { else if ('ethernet_mac' in sysInfo) mac = sysInfo.ethernet_mac; else mac = ''; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (macAddresses && macAddresses.length > 0) { if (!compareMac(mac, macAddresses)) { this.log.debug( @@ -619,6 +623,7 @@ export default class Client extends EventEmitter implements ClientEventEmitter { } } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (excludeMacAddresses && excludeMacAddresses.length > 0) { if (compareMac(mac, excludeMacAddresses)) { this.log.debug( diff --git a/src/device/index.ts b/src/device/index.ts index 2c6a1a9..5a0452e 100644 --- a/src/device/index.ts +++ b/src/device/index.ts @@ -1,23 +1,22 @@ -import castArray from 'lodash.castarray'; import { EventEmitter } from 'events'; +import castArray from 'lodash.castarray'; import type log from 'loglevel'; import type { BulbSysinfo } from '../bulb'; -import type Client from '../client'; -import type { SendOptions } from '../client'; +import type { default as Client, SendOptions } from '../client'; // eslint-disable-line import/no-named-default import type { Logger } from '../logger'; -import Netif from './netif'; import TcpConnection from '../network/tcp-connection'; import UdpConnection from '../network/udp-connection'; -import type { Realtime, RealtimeNormalized } from '../shared/emeter'; import type { PlugEventEmitter, PlugSysinfo } from '../plug'; +import type { Realtime, RealtimeNormalized } from '../shared/emeter'; import { + extractResponse, isObjectLike, processResponse, - extractResponse, processSingleCommandResponse, - HasErrCode, + type HasErrCode, } from '../utils'; +import Netif from './netif'; type HasAtLeastOneProperty = { [key: string]: unknown; @@ -218,7 +217,7 @@ export default abstract class Device * Cached value of `sysinfo.alias`. */ get alias(): string { - return this.sysInfo !== undefined ? this.sysInfo.alias : ''; + return this.sysInfo.alias; } /** @@ -258,10 +257,8 @@ export default abstract class Device * Cached value of `sysinfo.[type|mic_type]`. */ get type(): string { - if ('type' in this.sysInfo && this.sysInfo.type !== undefined) - return this.sysInfo.type; - if ('mic_type' in this.sysInfo && this.sysInfo.mic_type !== undefined) - return this.sysInfo.mic_type; + if ('type' in this.sysInfo) return this.sysInfo.type; + if ('mic_type' in this.sysInfo) return this.sysInfo.mic_type; return ''; } @@ -300,15 +297,9 @@ export default abstract class Device * Cached value of `sysinfo.[mac|mic_mac|ethernet_mac]`. */ get mac(): string { - if ('mac' in this.sysInfo && this.sysInfo.mac !== undefined) - return this.sysInfo.mac; - if ('mic_mac' in this.sysInfo && this.sysInfo.mic_mac !== undefined) - return this.sysInfo.mic_mac; - if ( - 'ethernet_mac' in this.sysInfo && - this.sysInfo.ethernet_mac !== undefined - ) - return this.sysInfo.ethernet_mac; + if ('mac' in this.sysInfo) return this.sysInfo.mac; + if ('mic_mac' in this.sysInfo) return this.sysInfo.mic_mac; + if ('ethernet_mac' in this.sysInfo) return this.sysInfo.ethernet_mac; return ''; } @@ -392,9 +383,8 @@ export default abstract class Device }; if (childIds) { - const childIdsArray = castArray(childIds).map( - this.normalizeChildId, - this, + const childIdsArray = castArray(childIds).map((childId) => + this.normalizeChildId(childId), ); payload.context = { child_ids: childIdsArray }; } @@ -437,20 +427,28 @@ export default abstract class Device childIds: string[] | string | undefined = this.childId, sendOptions?: SendOptions, ): Promise { - // TODO allow certain err codes (particularly emeter for non HS110 devices) - const commandObj = - typeof command === 'string' ? JSON.parse(command) : command; + // TODO: allow certain err codes (particularly emeter for non HS110 devices) + const commandObj = ( + typeof command === 'string' ? JSON.parse(command) : command + ) as { + [key: string]: { + [key: string]: unknown; + context?: { childIds: string[] }; + }; + }; if (childIds) { - const childIdsArray = castArray(childIds).map( - this.normalizeChildId, - this, + const childIdsArray = castArray(childIds).map((childId) => + this.normalizeChildId(childId), ); commandObj.context = { child_ids: childIdsArray }; } const response = await this.send(commandObj, sendOptions); - const results = processResponse(commandObj, JSON.parse(response)); + const results = processResponse( + commandObj, + JSON.parse(response) as unknown, + ); return results; } @@ -474,7 +472,7 @@ export default abstract class Device */ async getSysInfo(sendOptions?: SendOptions): Promise { this.log.debug('[%s] device.getSysInfo()', this.alias); - const response = extractResponse( + const response = extractResponse( await this.sendCommand( '{"system":{"get_sysinfo":{}}}', undefined, @@ -482,7 +480,7 @@ export default abstract class Device ), '', isSysinfo, - ) as Sysinfo; + ); this.setSysInfo(response); return this.sysInfo; diff --git a/src/device/netif.ts b/src/device/netif.ts index f7f34ea..a56fcd7 100644 --- a/src/device/netif.ts +++ b/src/device/netif.ts @@ -1,6 +1,6 @@ import clone from 'lodash.clone'; import type { SendOptions } from '../client'; -import type Device from '.'; +import type Device from './index'; export default class Netif { constructor( diff --git a/src/logger.ts b/src/logger.ts index 18e31f5..cca8e3b 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -20,20 +20,32 @@ export default function logger({ if (isDefinedAndNotNull(level)) loglevelLogger.setLevel(level); const log = { - /* eslint-disable @typescript-eslint/no-explicit-any */ - debug: (...msg: any[]): void => loglevelLogger.debug(...msg), - info: (...msg: any[]): void => loglevelLogger.info(...msg), - warn: (...msg: any[]): void => loglevelLogger.warn(...msg), - error: (...msg: any[]): void => loglevelLogger.error(...msg), - /* eslint-enable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */ + debug: (...msg: any[]): void => { + loglevelLogger.debug(...msg); + }, + info: (...msg: any[]): void => { + loglevelLogger.info(...msg); + }, + warn: (...msg: any[]): void => { + loglevelLogger.warn(...msg); + }, + error: (...msg: any[]): void => { + loglevelLogger.error(...msg); + }, + /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */ }; // if logger passed in, call logger functions instead of our loglevel functions if (isDefinedAndNotNull(logger)) { - levels.forEach((loggerLevel: LogLevelMethodNames) => { + levels.forEach((loggerLevel) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (logger[loggerLevel] !== undefined) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - log[loggerLevel] = (...msg: any[]): void => logger[loggerLevel](...msg); + log[loggerLevel] = (...msg: any[]): void => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + logger[loggerLevel](...msg); + }; } }); } diff --git a/src/network/tcp-socket.ts b/src/network/tcp-socket.ts index 7dda28b..c795a12 100644 --- a/src/network/tcp-socket.ts +++ b/src/network/tcp-socket.ts @@ -14,6 +14,7 @@ export default class TcpSocket extends TplinkSocket { // eslint-disable-next-line @typescript-eslint/no-explicit-any logDebug(...args: any[]): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.log.debug(`[${this.socketId}] TcpSocket${args.shift()}`, ...args); } @@ -50,11 +51,11 @@ export default class TcpSocket extends TplinkSocket { if (socket === undefined) throw new Error('send called without creating socket'); - let deviceDataBuf: Buffer; + let deviceDataBuf: Buffer | undefined; let segmentCount = 0; let decryptedMsg: string; - let timer: NodeJS.Timeout; + let timer: NodeJS.Timeout | undefined; const setSocketTimeout = (socketTimeout: number): void => { if (timer != null) clearTimeout(timer); if (socketTimeout > 0) { @@ -129,7 +130,7 @@ export default class TcpSocket extends TplinkSocket { ); } try { - return resolve(decryptedMsg); + resolve(decryptedMsg); } catch (err) { this.log.error( `Error parsing JSON: From: [${socket.remoteAddress} ${ @@ -143,7 +144,7 @@ export default class TcpSocket extends TplinkSocket { } } catch (err) { this.logDebug(': socket:close error'); - return reject(err); + reject(err); } }); diff --git a/src/network/tplink-connection.ts b/src/network/tplink-connection.ts index 900c8d6..1806133 100644 --- a/src/network/tplink-connection.ts +++ b/src/network/tplink-connection.ts @@ -2,10 +2,9 @@ import { EventEmitter } from 'events'; import Queue from 'promise-queue'; import type Client from '../client'; - +import type { Logger } from '../logger'; import TcpSocket from './tcp-socket'; import UdpSocket from './udp-socket'; -import type { Logger } from '../logger'; /** * @hidden @@ -30,9 +29,17 @@ export default abstract class TplinkConnection extends EventEmitter { this.host, this.port, ); - this.queue.add(async () => { - this.close(); - }); + this.queue + .add(() => { + this.close(); + return Promise.resolve(); + }) + .catch((err) => { + this.log.debug( + `TplinkConnection(${this.description}): timeout.close()`, + err, + ); + }); }); } @@ -69,7 +76,7 @@ export default abstract class TplinkConnection extends EventEmitter { this.port = port; this.host = host; - let socket: TcpSocket | UdpSocket; + let socket: TcpSocket | UdpSocket | undefined; return this.queue.add(async () => { try { socket = await this.getSocket(useSharedSocket); diff --git a/src/network/udp-connection.ts b/src/network/udp-connection.ts index 31b07a7..87c2a71 100644 --- a/src/network/udp-connection.ts +++ b/src/network/udp-connection.ts @@ -63,7 +63,7 @@ export default class UdpConnection extends TplinkConnection { sharedSocketTimeout: number; }, ): Promise { - if (useSharedSocket && sharedSocketTimeout != null) { + if (useSharedSocket) { this.setTimeout(sharedSocketTimeout); } @@ -82,7 +82,7 @@ export default class UdpConnection extends TplinkConnection { if (this.sharedSocket && this.sharedSocket.isBound) { this.log.debug( - `TplinkConnection(${this.description}).close() closing shared socket`, + `UdpConnection(${this.description}).close() closing shared socket`, ); this.sharedSocket.close(); } diff --git a/src/network/udp-socket.ts b/src/network/udp-socket.ts index ef56343..eb0b32f 100644 --- a/src/network/udp-socket.ts +++ b/src/network/udp-socket.ts @@ -1,8 +1,8 @@ import dgram from 'dgram'; -import { encrypt, decrypt } from 'tplink-smarthome-crypto'; +import { decrypt, encrypt } from 'tplink-smarthome-crypto'; -import TplinkSocket from './tplink-socket'; import { replaceControlCharacters } from '../utils'; +import TplinkSocket from './tplink-socket'; /** * @hidden @@ -14,6 +14,7 @@ export default class UdpSocket extends TplinkSocket { // eslint-disable-next-line @typescript-eslint/no-explicit-any logDebug(...args: any[]): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.log.debug(`[${this.socketId}] UdpSocket${args.shift()}`, ...args); } @@ -53,7 +54,7 @@ export default class UdpSocket extends TplinkSocket { if (socket === undefined) throw new Error('send called without creating socket'); - let timer: NodeJS.Timeout; + let timer: NodeJS.Timeout | undefined; const setSocketTimeout = (socketTimeout: number): void => { if (timer != null) clearTimeout(timer); if (socketTimeout > 0) { @@ -84,7 +85,7 @@ export default class UdpSocket extends TplinkSocket { `: socket:data message:${replaceControlCharacters(decryptedMsg)}`, ); - return resolve(decryptedMsg); + resolve(decryptedMsg); } catch (err) { this.log.error( `Error processing UDP message: From:[%j] SO_RCVBUF:[%d]${'\n'} msg:[%o]${'\n'} decrypted:[${replaceControlCharacters( @@ -94,7 +95,7 @@ export default class UdpSocket extends TplinkSocket { socket.getRecvBufferSize(), msg, ); - return reject(err); + reject(err); } }); diff --git a/src/plug/away.ts b/src/plug/away.ts index 7778610..d98fef9 100644 --- a/src/plug/away.ts +++ b/src/plug/away.ts @@ -1,12 +1,12 @@ -import type Plug from '.'; import type { SendOptions } from '../client'; import { + ScheduleRuleInputTime, createRule, hasRuleListWithRuleIds, - ScheduleRuleInputTime, + type HasRuleListWithRuleIds, } from '../shared/schedule'; -import type { HasRuleListWithRuleIds } from '../shared/schedule'; -import { extractResponse, HasErrCode, hasErrCode } from '../utils'; +import { extractResponse, hasErrCode, type HasErrCode } from '../utils'; +import type Plug from './index'; export type AwayRule = { name?: string; diff --git a/src/plug/dimmer.ts b/src/plug/dimmer.ts index feda062..5a11322 100644 --- a/src/plug/dimmer.ts +++ b/src/plug/dimmer.ts @@ -1,5 +1,5 @@ import type { SendOptions } from '../client'; -import type Plug from '.'; +import type Plug from './index'; export interface DimmerTransitionInput { /** @@ -353,12 +353,10 @@ export default class Dimmer { this.lastState, ); - if (brightness !== undefined) { - if (this.lastState.brightness !== brightness) { - this.lastState.brightness = brightness; - this.device.emit('brightness-change', brightness); - } - this.device.emit('brightness-update', brightness); + if (this.lastState.brightness !== brightness) { + this.lastState.brightness = brightness; + this.device.emit('brightness-change', brightness); } + this.device.emit('brightness-update', brightness); } } diff --git a/src/plug/index.ts b/src/plug/index.ts index 4b58441..696c03e 100644 --- a/src/plug/index.ts +++ b/src/plug/index.ts @@ -1,27 +1,26 @@ /* eslint-disable no-underscore-dangle */ import type { SendOptions } from '../client'; -import Device, { isPlugSysinfo } from '../device'; -import type { - CommonSysinfo, - DeviceConstructorOptions, - Sysinfo, +import Device, { + isPlugSysinfo, + type CommonSysinfo, + type DeviceConstructorOptions, + type Sysinfo, } from '../device'; -import Away from './away'; -import Cloud, { isCloudInfo } from '../shared/cloud'; -import type { CloudInfo } from '../shared/cloud'; -import Dimmer from './dimmer'; +import Cloud, { isCloudInfo, type CloudInfo } from '../shared/cloud'; import Emeter, { RealtimeNormalized } from '../shared/emeter'; -import Schedule from './schedule'; -import Timer from './timer'; import Time from '../shared/time'; import { + ResponseError, extractResponse, hasErrCode, - HasErrCode, isDefinedAndNotNull, isObjectLike, - ResponseError, + type HasErrCode, } from '../utils'; +import Away from './away'; +import Dimmer from './dimmer'; +import Schedule from './schedule'; +import Timer from './timer'; type PlugChild = { id: string; alias: string; state: number }; @@ -47,10 +46,10 @@ export function hasSysinfoChildren( candidate: Sysinfo, ): candidate is Sysinfo & Required { return ( - isObjectLike(candidate) && 'children' in candidate && candidate.children !== undefined && - isObjectLike(candidate.children) && + // eslint rule false positive + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition candidate.children.length > 0 ); } @@ -279,10 +278,8 @@ export default class Plug extends Device implements PlugEventEmitter { if (isDefinedAndNotNull(childId)) this.setChildId(childId); - if (this.sysInfo) { - this.lastState.inUse = this.inUse; - this.lastState.relayState = this.relayState; - } + this.lastState.inUse = this.inUse; + this.lastState.relayState = this.relayState; } override get sysInfo(): PlugSysinfo { @@ -323,9 +320,8 @@ export default class Plug extends Device implements PlugEventEmitter { } else if (children instanceof Map) { this.#children = children; } - if (this.#childId && this.#children) { - if (this.#childId !== undefined) this.setChildId(this.#childId); - } + + if (this.#childId !== undefined) this.setChildId(this.#childId); } /** @@ -337,7 +333,7 @@ export default class Plug extends Device implements PlugEventEmitter { private setChildId(childId: string): void { this.#childId = this.normalizeChildId(childId); - if (this.#childId && this.#children) { + if (this.#childId) { this.#child = this.#children.get(this.#childId); } if (this.#childId && this.#child == null) { @@ -352,7 +348,6 @@ export default class Plug extends Device implements PlugEventEmitter { if (this.#childId && this.#child !== undefined) { return this.#child.alias; } - if (this.sysInfo === undefined) return ''; return this.sysInfo.alias; } @@ -415,7 +410,7 @@ export default class Plug extends Device implements PlugEventEmitter { if (this.#childId && this.#child !== undefined) { return this.#child.state === 1; } - if (this.#children && this.#children.size > 0) { + if (this.#children.size > 0) { return ( Array.from(this.#children.values()).findIndex((child) => { return child.state === 1; @@ -430,7 +425,7 @@ export default class Plug extends Device implements PlugEventEmitter { this.#child.state = relayState ? 1 : 0; return; } - if (this.#children && this.#children.size > 0) { + if (this.#children.size > 0) { for (const child of this.#children.values()) { child.state = relayState ? 1 : 0; } @@ -453,7 +448,7 @@ export default class Plug extends Device implements PlugEventEmitter { */ get supportsEmeter(): boolean { return this.sysInfo.feature && typeof this.sysInfo.feature === 'string' - ? this.sysInfo.feature.indexOf('ENE') >= 0 + ? this.sysInfo.feature.includes('ENE') : false; } @@ -467,7 +462,7 @@ export default class Plug extends Device implements PlugEventEmitter { const response = await super.getSysInfo(sendOptions); if (!isPlugSysinfo(response)) { - throw new Error(`Unexpected Response: ${response}`); + throw new Error(`Unexpected Response: ${JSON.stringify(response)}`); } return this.sysInfo; } @@ -518,11 +513,11 @@ export default class Plug extends Device implements PlugEventEmitter { } } - const sysinfo = extractResponse( + const sysinfo = extractResponse( data, 'system.get_sysinfo', isPlugSysinfo, - ) as PlugSysinfo; + ); this.setSysInfo(sysinfo); const cloudInfo = extractResponse( @@ -542,11 +537,11 @@ export default class Plug extends Device implements PlugEventEmitter { this.emeter.setRealtime(data.emeter.get_realtime); } - const scheduleNextAction = extractResponse( + const scheduleNextAction = extractResponse( data, 'schedule.get_next_action', hasErrCode, - ) as HasErrCode; + ); this.schedule.nextAction = scheduleNextAction; return { diff --git a/src/plug/schedule.ts b/src/plug/schedule.ts index e62d9c3..d74f040 100644 --- a/src/plug/schedule.ts +++ b/src/plug/schedule.ts @@ -1,8 +1,11 @@ -import type Plug from '.'; -import Schedule, { createScheduleRule } from '../shared/schedule'; -import type { ScheduleRule, ScheduleRuleInputTime } from '../shared/schedule'; import type { SendOptions } from '../client'; +import Schedule, { + createScheduleRule, + type ScheduleRule, + type ScheduleRuleInputTime, +} from '../shared/schedule'; import { isDefinedAndNotNull } from '../utils'; +import type Plug from './index'; export type PlugScheduleRule = Omit & { sact?: number; diff --git a/src/shared/emeter.ts b/src/shared/emeter.ts index 6d170d5..bc77d3d 100644 --- a/src/shared/emeter.ts +++ b/src/shared/emeter.ts @@ -1,9 +1,9 @@ import type { AnyDevice, SendOptions } from '../client'; import { extractResponse, - isObjectLike, - HasErrCode, hasErrCode, + isObjectLike, + type HasErrCode, } from '../utils'; export type RealtimeV1 = { @@ -64,12 +64,10 @@ export default class Emeter { } }; - if (realtime != null) { - normalize('current', 'current_ma', 1000); - normalize('power', 'power_mw', 1000); - normalize('total', 'total_wh', 1000); - normalize('voltage', 'voltage_mv', 1000); - } + normalize('current', 'current_ma', 1000); + normalize('power', 'power_mw', 1000); + normalize('total', 'total_wh', 1000); + normalize('voltage', 'voltage_mv', 1000); this.#realtime = normRealtime; this.device.emit('emeter-realtime-update', this.#realtime); diff --git a/src/shared/schedule.ts b/src/shared/schedule.ts index 37f6e85..101a082 100644 --- a/src/shared/schedule.ts +++ b/src/shared/schedule.ts @@ -4,9 +4,9 @@ import type { AnyDevice, SendOptions } from '../client'; import { extractResponse, hasErrCode, - HasErrCode, isDefinedAndNotNull, isObjectLike, + type HasErrCode, } from '../utils'; type ScheduleDateStart = { @@ -65,7 +65,8 @@ export function hasRuleListWithRuleIds( isObjectLike(candidate.rule_list) && Array.isArray(candidate.rule_list) && candidate.rule_list.every( - (rule) => 'id' in rule && typeof rule.id === 'string', + (rule) => + isObjectLike(rule) && 'id' in rule && typeof rule.id === 'string', ) ); } @@ -77,7 +78,8 @@ function isScheduleRules(candidate: unknown): candidate is ScheduleRules { isObjectLike(candidate.rule_list) && Array.isArray(candidate.rule_list) && candidate.rule_list.every( - (rule) => 'id' in rule && typeof rule.id === 'string', + (rule) => + isObjectLike(rule) && 'id' in rule && typeof rule.id === 'string', ) ); } @@ -105,9 +107,13 @@ function createScheduleDate( } else if (date === 'sunrise') { min = 0; time_opt = 1; + // We want to validate + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (date === 'sunset') { min = 0; time_opt = 2; + } else { + throw new Error('invalid date'); } if (startOrEnd === 'end') { @@ -223,7 +229,7 @@ export default abstract class Schedule { async getNextAction( sendOptions?: SendOptions, ): Promise { - this.nextAction = extractResponse( + this.nextAction = extractResponse( await this.device.sendCommand( { [this.apiModuleName]: { get_next_action: {} }, @@ -233,7 +239,7 @@ export default abstract class Schedule { ), '', isScheduleNextActionResponse, - ) as ScheduleNextActionResponse; + ); return this.nextAction; } @@ -245,7 +251,7 @@ export default abstract class Schedule { * @throws {@link ResponseError} */ async getRules(sendOptions?: SendOptions): Promise { - return extractResponse( + return extractResponse( await this.device.sendCommand( { [this.apiModuleName]: { get_rules: {} }, @@ -255,7 +261,7 @@ export default abstract class Schedule { ), '', isScheduleRulesResponse, - ) as ScheduleRulesResponse; + ); } /** @@ -291,7 +297,7 @@ export default abstract class Schedule { rule: object, sendOptions?: SendOptions, ): Promise<{ id: string }> { - return extractResponse( + return extractResponse<{ id: string }>( await this.device.sendCommand( { [this.apiModuleName]: { add_rule: rule }, @@ -303,7 +309,7 @@ export default abstract class Schedule { (candidate) => { return isObjectLike(candidate) && typeof candidate.id === 'string'; }, - ) as { id: string }; + ); } /** diff --git a/src/utils.ts b/src/utils.ts index f167b1f..9dd5691 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,10 @@ export function isObjectLike( return typeof candidate === 'object' && candidate !== null; } +export function objectHasKey(obj: T, key: PropertyKey): key is keyof T { + return Object.prototype.hasOwnProperty.call(obj, key); +} + /** * Represents an error result received from a TP-Link device. * @@ -144,21 +148,29 @@ export function processSingleCommandResponse( command: string, response: string, ): HasErrCode { - let responseObj; + let responseObj: unknown; try { responseObj = JSON.parse(response); + if (!isObjectLike(responseObj)) throw new Error(); } catch (err) { throw new ResponseError('Could not parse response', response, command); } - if (responseObj[module] === undefined) { + if ( + !(module in responseObj) || + responseObj[module] === undefined || + !isObjectLike(responseObj[module]) + ) { throw new ResponseError('Module not found in response', response, command); } - if (responseObj[module][method] === undefined) { + + const moduleResponse = responseObj[module] as Record; + if (!(method in moduleResponse) || moduleResponse[method] === undefined) { throw new ResponseError('Method not found in response', response, command, [ module, ]); } - const methodResponse = responseObj[module][method]; + + const methodResponse = moduleResponse[method]; if (!hasErrCode(methodResponse)) { throw new ResponseError('err_code missing', response, command, [module]); } @@ -174,8 +186,15 @@ export function processSingleCommandResponse( */ export function processResponse( command: Record, - response: Record, + response: unknown, ): Record { + if (!isObjectLike(response)) + throw new ResponseError( + 'Response not object', + JSON.stringify(response), + JSON.stringify(command), + ); + const multipleResponses = Object.keys(response).length > 1; const commandResponses = flattenResponses(command, response); @@ -254,19 +273,21 @@ export function processResponse( */ export function extractResponse( response: unknown, - path: string, + path: string | string[], typeGuardFn: (arg0: unknown) => boolean, ): T { - const ret = path.length > 0 ? get(response, path) : response; + const ret: unknown = path.length > 0 ? get(response, path) : response; if (ret === undefined || !isObjectLike(ret)) { throw new Error( - `Could not find path:"${path}" in ${JSON.stringify(response)}`, + `Could not find path:"${path.toString()}" in ${JSON.stringify(response)}`, ); } if (!typeGuardFn(ret)) throw new TypeError( - `Unexpected object path:"${path}" in ${JSON.stringify(response)}`, + `Unexpected object path:"${path.toString()}" in ${JSON.stringify( + response, + )}`, ); return ret as T; } diff --git a/test/bulb/index.ts b/test/bulb/index.ts index 63ff5cd..9c75260 100644 --- a/test/bulb/index.ts +++ b/test/bulb/index.ts @@ -21,17 +21,7 @@ describe('Bulb', function () { const ctx: { device?: AnyDevice } = {}; let bulb: Bulb; - before('Bulb', async function () { - if (!testDevice.getDevice) { - this.skip(); - } - }); - beforeEach('Bulb', async function () { - // before() doesn't skip nested describes - if (!testDevice.getDevice) { - this.skip(); - } await retry(async () => { // this is in beforeEach since many of the tests may overwrite some properties bulb = (await testDevice.getDevice( diff --git a/test/device/index.js b/test/device/index.js index b846918..b427a98 100644 --- a/test/device/index.js +++ b/test/device/index.js @@ -378,15 +378,15 @@ describe('Device', function () { device.sysInfo.ethernet_mac, ); device.sysInfo.mac = 'My Test mac'; - device.sysInfo.mic_mac = undefined; - device.sysInfo.ethernet_mac = undefined; + delete device.sysInfo.mic_mac; + delete device.sysInfo.ethernet_mac; expect(device.mac).to.eql(device.sysInfo.mac); - device.sysInfo.mac = undefined; + delete device.sysInfo.mac; device.sysInfo.mic_mac = 'My Test mic_mac'; - device.sysInfo.ethernet_mac = undefined; + delete device.sysInfo.ethernet_mac; expect(device.mac).to.eql(device.sysInfo.mic_mac); - device.sysInfo.mac = undefined; - device.sysInfo.mic_mac = undefined; + delete device.sysInfo.mac; + delete device.sysInfo.mic_mac; device.sysInfo.ethernet_mac = 'My Test ethernet_mac'; expect(device.mac).to.eql(device.sysInfo.ethernet_mac); }); @@ -395,15 +395,15 @@ describe('Device', function () { describe('#macNormalized get', function () { it('should return normalized mac from cached sysInfo', function () { device.sysInfo.mac = 'My Test mac'; - device.sysInfo.mic_mac = undefined; - device.sysInfo.ethernet_mac = undefined; + delete device.sysInfo.mic_mac; + delete device.sysInfo.ethernet_mac; expect(device.macNormalized).to.eql('MYTESTMAC'); - device.sysInfo.mac = undefined; + delete device.sysInfo.mac; device.sysInfo.mic_mac = 'My Test mic_mac'; - device.sysInfo.ethernet_mac = undefined; + delete device.sysInfo.ethernet_mac; expect(device.macNormalized).to.eql('MYTESTMICMAC'); - device.sysInfo.mac = undefined; - device.sysInfo.mic_mac = undefined; + delete device.sysInfo.mac; + delete device.sysInfo.mic_mac; device.sysInfo.ethernet_mac = 'My Test ethernet_mac'; expect(device.macNormalized).to.eql('MYTESTETHERNETMAC'); }); diff --git a/test/setup/discovery-devices.ts b/test/setup/discovery-devices.ts index f157978..205c0de 100644 --- a/test/setup/discovery-devices.ts +++ b/test/setup/discovery-devices.ts @@ -1,5 +1,5 @@ import type { Client } from '../../src'; -import { AnyDevice } from '../../src/client'; +import type { AnyDevice } from '../../src/client'; export default async function getDiscoveryDevices( client: Client, @@ -20,7 +20,7 @@ export default async function getDiscoveryDevices( for (const device of client.devices.values()) { discoveredTestDevices.push(device); } - return resolve(discoveredTestDevices); + resolve(discoveredTestDevices); }, discoveryTimeout); }); } diff --git a/test/setup/index.ts b/test/setup/index.ts index b36ed50..429bb30 100644 --- a/test/setup/index.ts +++ b/test/setup/index.ts @@ -30,17 +30,6 @@ export function retry( ); } -function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -export function delayError(fn: () => Promise, ms: number): () => void { - return () => - fn().catch((e: Error) => delay(ms).then(() => Promise.reject(e))); -} - export async function createUnresponsiveDevice( transport: 'tcp' | 'udp', ): Promise<{ port: number; host: string; close: () => void }> { diff --git a/test/setup/simulated-devices.ts b/test/setup/simulated-devices.ts index dfba448..58c7c7e 100644 --- a/test/setup/simulated-devices.ts +++ b/test/setup/simulated-devices.ts @@ -23,9 +23,11 @@ async function simulatorToDevice( }); } -export const startUdpServer = simulator.UdpServer.start; +export const startUdpServer = () => simulator.UdpServer.start(); -export const stopUdpServer = simulator.UdpServer.stop; +export const stopUdpServer = () => { + simulator.UdpServer.stop(); +}; const simulatorDevices: SimulatorDevice[] = []; diff --git a/test/setup/test-device-setup.ts b/test/setup/test-device-setup.ts index 5890ddb..3b9c983 100644 --- a/test/setup/test-device-setup.ts +++ b/test/setup/test-device-setup.ts @@ -25,7 +25,7 @@ const clientDefaultOptions = ((): ConstructorParameters[0] => { if (config.useSimulator) { // set low timeout for simulator return { - defaultSendOptions: { timeout: 250 }, + defaultSendOptions: { timeout: 500 }, ...clientOptions, }; } @@ -244,7 +244,6 @@ export async function testDeviceCleanup(): Promise { if ( !('getDevice' in testDevices.plugWithChildren) && device.deviceType === 'plug' && - device.children && device.children.size > 0 ) { testDeviceDecorator( @@ -306,7 +305,7 @@ export async function testDeviceCleanup(): Promise { Object.entries(testDevices) .reduce((acc: [string, TestDevice | undefined][], [key, val]) => { if (Array.isArray(val)) { - const d = val.map((v) => [`${key}[]`, v]) as [string, TestDevice][]; + const d = val.map<[string, TestDevice]>((v) => [`${key}[]`, v]); d.forEach((a) => acc.push(a)); } else { acc.push([key, val]); @@ -317,4 +316,6 @@ export async function testDeviceCleanup(): Promise { ); run(); -})(); +})().catch((e) => { + console.error(e); +}); diff --git a/test/setup/test-device.ts b/test/setup/test-device.ts index 34cd74b..5160a94 100644 --- a/test/setup/test-device.ts +++ b/test/setup/test-device.ts @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ import type { MarkOptional, MarkRequired } from 'ts-essentials'; -import { Client, Plug } from '../../src'; -import { AnyDevice } from '../../src/client'; +import type { Client, Plug } from '../../src'; +import type { AnyDevice } from '../../src/client'; import { isObjectLike } from '../../src/utils'; export type TestDevice = { @@ -91,88 +91,86 @@ export function testDeviceDecorator( testDevice.parent = parent; testDevice.childId = childId; - if (device) { - testDevice.deviceType = device.deviceType; - testDevice.mac = device.mac; - testDevice.deviceOptions = { - host: device.host, - port: device.port, - }; - if (childId !== undefined) { - // @ts-expect-error: childId is only on plugs, but harmless - testDevice.deviceOptions.childId = childId; - } + testDevice.deviceType = device.deviceType; + testDevice.mac = device.mac; + testDevice.deviceOptions = { + host: device.host, + port: device.port, + }; + if (childId !== undefined) { + // @ts-expect-error: childId is only on plugs, but harmless + testDevice.deviceOptions.childId = childId; + } + + testDevice.getDevice = function getDevice( + deviceOptions?: Parameters[0], + sendOptions?: Parameters[1], + ): ReturnType { + return client.getDevice( + { + ...testDevice.deviceOptions, + defaultSendOptions: sendOptions, + ...deviceOptions, + } as Parameters[0], // TODO: I don't like this cast but only way I could get it to work + sendOptions, + ); + }; - testDevice.getDevice = function getDevice( - deviceOptions?: Parameters[0], - sendOptions?: Parameters[1], - ): ReturnType { - return client.getDevice( - { - ...testDevice.deviceOptions, - defaultSendOptions: sendOptions, - ...deviceOptions, - } as Parameters[0], // TODO: I don't like this cast but only way I could get it to work - sendOptions, - ); - }; + if ('children' in device && device.children.size > 0) { + if (testDevice.childId === undefined) { + testDevice.children = ((): MarkRequired< + TestDevice, + 'getDevice' | 'childId' | 'parent' + >[] => { + return Array.from(device.children.keys()).map((key) => { + return createTestDevice(device, client, { + name: testDevice.name, + model: testDevice.model, + hardwareVersion: testDevice.hardwareVersion, + isSimulated: testDevice.isSimulated, + parent: testDevice as TestDevice, + childId: key, + }) as MarkRequired; + }); + })(); + } else { + testDevice.getOtherChildren = function getOtherChildren(): MarkRequired< + TestDevice, + 'getDevice' | 'childId' | 'parent' + >[] { + if (!(parent !== undefined && parent.children !== undefined)) + throw new TypeError(); + return parent.children.filter((oc) => oc.childId !== this.childId); + }; - if ('children' in device && device.children.size > 0) { - if (testDevice.childId === undefined) { - testDevice.children = ((): MarkRequired< - TestDevice, - 'getDevice' | 'childId' | 'parent' - >[] => { - return Array.from(device.children.keys()).map((key) => { - return createTestDevice(device, client, { - name: testDevice.name, - model: testDevice.model, - hardwareVersion: testDevice.hardwareVersion, - isSimulated: testDevice.isSimulated, - parent: testDevice as TestDevice, - childId: key, - }) as MarkRequired; - }); - })(); - } else { - testDevice.getOtherChildren = function getOtherChildren(): MarkRequired< - TestDevice, - 'getDevice' | 'childId' | 'parent' - >[] { - if (!(parent !== undefined && parent.children !== undefined)) - throw new TypeError(); - return parent.children.filter((oc) => oc.childId !== this.childId); + testDevice.getOtherChildrenState = + async function getOtherChildrenState(): Promise< + Array<{ + childId: string; + relayState: boolean; + alias: string; + }> + > { + if ( + !( + 'getOtherChildren' in testDevice && + testDevice.getOtherChildren !== undefined + ) + ) { + throw new Error(); + } + return Promise.all( + testDevice.getOtherChildren().map(async (childDevice) => { + const d = (await childDevice.getDevice()) as Plug; + if (d.childId === undefined) throw new TypeError(); + return { + childId: d.childId, + relayState: d.relayState, + alias: d.alias, + }; + }), + ); }; - - testDevice.getOtherChildrenState = - async function getOtherChildrenState(): Promise< - Array<{ - childId: string; - relayState: boolean; - alias: string; - }> - > { - if ( - !( - 'getOtherChildren' in testDevice && - testDevice.getOtherChildren !== undefined - ) - ) { - throw new Error(); - } - return Promise.all( - testDevice.getOtherChildren().map(async (childDevice) => { - const d = (await childDevice.getDevice()) as Plug; - if (d.childId === undefined) throw new TypeError(); - return { - childId: d.childId, - relayState: d.relayState, - alias: d.alias, - }; - }), - ); - }; - } } } diff --git a/tsconfig.examples.json b/tsconfig.examples.json deleted file mode 100644 index cef3911..0000000 --- a/tsconfig.examples.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "outDir": "./build/examples" - }, - "include": ["examples/**/*"] -}