From 9a74b015dc46caee062737d66bc4313ecbe7f6d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:44:52 +0000 Subject: [PATCH 01/70] chore: promote to dev --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62963f26ff..eb789a6412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "zigbee2mqtt", - "version": "1.42.0", + "version": "1.42.0-dev", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index f36df0b9bf..cb3b655406 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zigbee2mqtt", - "version": "1.42.0", + "version": "1.42.0-dev", "description": "Zigbee to MQTT bridge using Zigbee-herdsman", "main": "index.js", "repository": { From 1fdc427358d82f81c557e678b2e3e8f006f5d1a6 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 1 Dec 2024 20:04:54 +0100 Subject: [PATCH 02/70] chore: update update_deps.yml --- .github/workflows/update_deps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update_deps.yml b/.github/workflows/update_deps.yml index ef98dd4a66..113dc7977b 100644 --- a/.github/workflows/update_deps.yml +++ b/.github/workflows/update_deps.yml @@ -30,5 +30,5 @@ jobs: with: commit-message: 'fix(ignore): update dependencies' branch: 'deps/all' - title: Update dependencies + title: 'fix(ignore): update dependencies' token: ${{ secrets.GH_TOKEN }} From 98b977030ec73e5a75c28f4f779fce9189b12d13 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 11 Oct 2024 22:12:31 +0200 Subject: [PATCH 03/70] fix(ignore): Changes for improved adapter discovery (#24147) * fix: Comment serial config in example configuration * Link to issue * Update link * Update example config * Feedback --- data/configuration.example.yaml | 14 ++++++++++---- lib/controller.ts | 6 +++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/data/configuration.example.yaml b/data/configuration.example.yaml index 4af9322448..1a300ffb24 100644 --- a/data/configuration.example.yaml +++ b/data/configuration.example.yaml @@ -14,10 +14,16 @@ mqtt: # user: my_user # password: my_password -# Serial settings -serial: - # Location of CC2531 USB sniffer - port: /dev/ttyACM0 +# Serial settings, only required when Zigbee2MQTT fails to start with: +# USB adapter discovery error (No valid USB adapter found). +# Specify valid 'adapter' and 'port' in your configuration. +# serial: +# # Location of the adapter +# # USB adapters - use format "port: /dev/serial/by-id/XXX" +# # Ethernet adapters - use format "port: tcp://192.168.1.12:6638" +# port: /dev/serial/by-id/usb-Texas_Instruments_TI_CC2531_USB_CDC___0X00124B0018ED3DDF-if00 +# # Adapter type, allowed values: `zstack`, `ember`, `deconz`, `zigate` or `zboss` +# adapter: zstack # Advanced settings advanced: diff --git a/lib/controller.ts b/lib/controller.ts index cd2a2f3f09..c88c71b0bd 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -161,10 +161,14 @@ export class Controller { startResult = await this.zigbee.start(); this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected); } catch (error) { - logger.error('Failed to start zigbee'); + logger.error('Failed to start zigbee-herdsman'); logger.error('Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html for possible solutions'); logger.error('Exiting...'); logger.error((error as Error).stack!); + /* istanbul ignore if */ + if ((error as Error).message.includes('USB adapter discovery error (No valid USB adapter found)')) { + logger.error('If this happens after updating to Zigbee2MQTT 2.0.0, see https://github.com/Koenkk/zigbee2mqtt/discussions/24199'); + } return await this.exit(1); } From 3220b27c8a91fc65912d299db22b8ac489b929b2 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 11 Oct 2024 22:16:57 +0200 Subject: [PATCH 04/70] fix!: HA permit join switch: disable automatically after 254 seconds (#24245) --- lib/extension/homeassistant.ts | 3 ++- test/homeassistant.test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 63c085aefe..3c3a7cac81 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -2206,7 +2206,8 @@ export default class HomeAssistant extends Extension { state_topic_postfix: 'info', value_template: '{{ value_json.permit_join | lower }}', command_topic: `${baseTopic}/request/permit_join`, - payload_on: 'true', + state_on: 'true', + payload_on: '{"value": true, "time": 254}', payload_off: 'false', }, }, diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 8475290db8..caed4fe3bc 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -2801,7 +2801,8 @@ describe('HomeAssistant extension', () => { state_topic: 'zigbee2mqtt/bridge/info', value_template: '{{ value_json.permit_join | lower }}', command_topic: 'zigbee2mqtt/bridge/request/permit_join', - payload_on: 'true', + state_on: 'true', + payload_on: '{"value": true, "time": 254}', payload_off: 'false', origin: origin, device: devicePayload, From b6186623f0bcf9d88de1772aebd8a9eaead21a4c Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 14 Oct 2024 22:01:14 +0200 Subject: [PATCH 05/70] feat!: The big cleanup (#24200) * Remove `deviceGroupMembership` * Fix controller.ts * Remove `legacy_api` logic from non-legacy extensions. * Fix network map coverage. * Remove all `legacy` extensions. * Remove `legacy_availability_payload`, `legacy_api`. Remove legacy from configure. * Fix prettier. * Remove some leftovers * Renamed `updateAvailable` to `update_available` * Remove emitPublishAvailability * Remove `configuration.yaml` * Switch to pnpm * Remove direct mqtt-packet dependency. * fix pretty * fix `getDependencyVersion` * fix pnpm publish * fix(ignore): fix pnpm publish * Remove deprecated bridge `config/*` * Improve update script * Improve update.sh --------- Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com> --- .github/workflows/ci.yml | 31 +- .github/workflows/release_please.yml | 2 +- .github/workflows/update_dep.yml | 7 +- .github/workflows/update_deps.yml | 13 +- .prettierignore | 2 +- CONTRIBUTING.md | 2 +- README.md | 6 +- data/configuration.yaml | 20 - docker/Dockerfile | 8 +- index.js | 2 +- lib/controller.ts | 33 +- lib/eventBus.ts | 7 - lib/extension/availability.ts | 3 +- lib/extension/bind.ts | 38 +- lib/extension/bridge.ts | 69 - lib/extension/configure.ts | 16 +- lib/extension/groups.ts | 101 +- lib/extension/homeassistant.ts | 8 +- lib/extension/legacy/bridgeLegacy.ts | 424 - lib/extension/legacy/deviceGroupMembership.ts | 67 - lib/extension/legacy/report.ts | 204 - lib/extension/legacy/softReset.ts | 61 - lib/extension/networkMap.ts | 16 +- lib/extension/otaUpdate.ts | 116 +- lib/extension/publish.ts | 73 +- lib/model/device.ts | 4 - lib/mqtt.ts | 9 +- lib/types/types.d.ts | 14 +- lib/util/settings.schema.json | 29 - lib/util/settings.ts | 40 +- lib/util/utils.ts | 26 +- lib/zigbee.ts | 4 - package-lock.json | 8610 ----------------- pnpm-lock.yaml | 5968 ++++++++++++ scripts/install.sh | 75 - test/availability.test.js | 110 +- test/bind.test.js | 220 +- test/bridge.test.js | 168 +- test/configure.test.js | 20 - test/controller.test.js | 97 +- test/frontend.test.js | 3 +- test/group.test.js | 191 - test/homeassistant.test.js | 127 +- test/legacy/bridgeLegacy.test.js | 538 - test/legacy/report.test.js | 315 - test/networkMap.test.js | 485 +- test/otaUpdate.test.js | 138 +- test/publish.test.js | 103 +- test/settings.test.js | 29 +- update.sh | 21 +- 50 files changed, 6459 insertions(+), 12214 deletions(-) delete mode 100644 data/configuration.yaml delete mode 100644 lib/extension/legacy/bridgeLegacy.ts delete mode 100644 lib/extension/legacy/deviceGroupMembership.ts delete mode 100644 lib/extension/legacy/report.ts delete mode 100644 lib/extension/legacy/softReset.ts delete mode 100644 package-lock.json create mode 100644 pnpm-lock.yaml delete mode 100644 scripts/install.sh delete mode 100644 test/legacy/bridgeLegacy.test.js delete mode 100644 test/legacy/report.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f9f9c3611..655c971d11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,21 +17,24 @@ jobs: token: ${{secrets.GH_TOKEN}} - uses: actions/checkout@v4 if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false + - uses: pnpm/action-setup@v4 + with: + version: 9 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ - cache: npm + cache: pnpm - name: Install dependencies - run: npm ci + run: pnpm i --frozen-lockfile - name: Build - run: npm run build + run: pnpm run build - name: Lint run: | - npm run pretty:check - npm run eslint + pnpm run pretty:check + pnpm run eslint - name: Test - run: npm run test-with-coverage + run: pnpm run test-with-coverage - name: Docker login if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin @@ -74,7 +77,7 @@ jobs: . - name: 'release: Publish to npm' if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' - run: npm publish + run: pnpm publish --no-git-checks env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }} - name: 'dev: Trigger zigbee2mqtt/hassio-zigbee2mqtt build' @@ -139,15 +142,17 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 - uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node }} - registry-url: https://registry.npmjs.org/ - cache: 'npm' + node-version: 20 + cache: pnpm - name: Install dependencies # --ignore-scripts prevents the serialport build which often fails on Windows - run: npm ci --ignore-scripts + run: pnpm i --frozen-lockfile --ignore-scripts - name: Build - run: npm run build + run: pnpm run build - name: Test - run: npm run test-with-coverage + run: pnpm run test-with-coverage diff --git a/.github/workflows/release_please.yml b/.github/workflows/release_please.yml index d4beda98c6..8e7473a72c 100644 --- a/.github/workflows/release_please.yml +++ b/.github/workflows/release_please.yml @@ -52,7 +52,7 @@ jobs: MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"') wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md cd z2m - npm ci + pnpm i --frozen-lockfile node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md env: GH_TOKEN: ${{secrets.GH_TOKEN}} diff --git a/.github/workflows/update_dep.yml b/.github/workflows/update_dep.yml index 9d5b980a7e..5c750bad9c 100644 --- a/.github/workflows/update_dep.yml +++ b/.github/workflows/update_dep.yml @@ -17,11 +17,14 @@ jobs: with: ref: dev token: ${{ secrets.GH_TOKEN }} + - uses: pnpm/action-setup@v4 + with: + version: 9 - uses: actions/setup-node@v4 with: node-version: 20 - cache: npm - - run: npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact + cache: pnpm + - run: pnpm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact - uses: peter-evans/create-pull-request@v7 id: cpr with: diff --git a/.github/workflows/update_deps.yml b/.github/workflows/update_deps.yml index 113dc7977b..e5e8ff1a10 100644 --- a/.github/workflows/update_deps.yml +++ b/.github/workflows/update_deps.yml @@ -18,14 +18,17 @@ jobs: with: ref: dev token: ${{ secrets.GH_TOKEN }} + - uses: pnpm/action-setup@v4 + with: + version: 9 - uses: actions/setup-node@v4 with: node-version: 20 - cache: npm - # connect-gzip-static@4.0.0 requires Node 20 >= - - run: npx npm-check-updates -u -x connect-gzip-static - - run: rm -f package-lock.json - - run: npm install + cache: pnpm + - run: | + pnpm up --latest + # connect-gzip-static@4.0.0 requires Node 20 >= + pnpm i connect-gzip-static@3.0.1 - uses: peter-evans/create-pull-request@v7 with: commit-message: 'fix(ignore): update dependencies' diff --git a/.prettierignore b/.prettierignore index 77107a753f..7e70d52abd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,2 @@ -package-lock.json +pnpm-lock.yaml CHANGELOG.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f2e422786..49153bd5da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,5 +4,5 @@ Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is wr - Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch. - Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this. -- You can run the tests locally by executing `npm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `npm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `npm run eslint`. +- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test-with-coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`. - When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html). diff --git a/README.md b/README.md index 3a5ae499a8..612e5ae4fe 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,9 @@ Zigbee2MQTT is made up of three modules, each developed in its own Github projec ### Developing -Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `npm run build`. For faster development instead of running `npm run build` you can run `npm run build-watch` in another terminal session, this will recompile as you change files. -In first time before building you need to run `npm install --include=dev` -Before submitting changes run `npm run test-with-coverage`, `npm run pretty:check` and `npm run eslint` +Zigbee2MQTT uses TypeScript (partially for now). Therefore after making changes to files in the `lib/` directory you need to recompile Zigbee2MQTT. This can be done by executing `pnpm run build`. For faster development instead of running `pnpm run build` you can run `pnpm run build-watch` in another terminal session, this will recompile as you change files. +In first time before building you need to run `pnpm install --include=dev` +Before submitting changes run `pnpm run test-with-coverage`, `pnpm run pretty:check` and `pnpm run eslint` ## Supported devices diff --git a/data/configuration.yaml b/data/configuration.yaml deleted file mode 100644 index 3ed7c1ce00..0000000000 --- a/data/configuration.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Home Assistant integration (MQTT discovery) -homeassistant: false - -# allow new devices to join -permit_join: true - -# MQTT settings -mqtt: - # MQTT base topic for zigbee2mqtt MQTT messages - base_topic: zigbee2mqtt - # MQTT server URL - server: 'mqtt://localhost' - # MQTT server authentication, uncomment if required: - # user: my_user - # password: my_password - -# Serial settings -serial: - # Location of CC2531 USB sniffer - port: /dev/ttyACM0 diff --git a/docker/Dockerfile b/docker/Dockerfile index c4118343bf..369e430c8b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,11 +9,11 @@ FROM base as dependencies_and_build COPY package*.json tsconfig.json index.js ./ COPY lib ./lib -RUN apk add --no-cache --virtual .buildtools make gcc g++ python3 linux-headers git npm && \ - npm ci --production --no-audit --no-optional --no-update-notifier && \ +RUN apk add make gcc g++ python3 linux-headers git npm && \ + npm install -g pnpm && \ + pnpm install --frozen-lockfile --no-optional && \ # Serialport needs to be rebuild for Alpine https://serialport.io/docs/9.x.x/guide-installation#alpine-linux - npm rebuild --build-from-source && \ - apk del .buildtools + pnpm rebuild # Release FROM base as release diff --git a/index.js b/index.js index c728bb8f60..bfc1eecedd 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,7 @@ async function build(reason) { env.NODE_OPTIONS = '--max_old_space_size=256'; } - exec('npm run build', {env, cwd: __dirname}, async (err, stdout, stderr) => { + exec('pnpm run build', {env, cwd: __dirname}, async (err, stdout, stderr) => { if (err) { process.stdout.write(', failed\n'); diff --git a/lib/controller.ts b/lib/controller.ts index c88c71b0bd..60dcde1733 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -19,10 +19,6 @@ import ExtensionExternalExtension from './extension/externalExtension'; import ExtensionFrontend from './extension/frontend'; import ExtensionGroups from './extension/groups'; import ExtensionHomeAssistant from './extension/homeassistant'; -import ExtensionBridgeLegacy from './extension/legacy/bridgeLegacy'; -import ExtensionDeviceGroupMembership from './extension/legacy/deviceGroupMembership'; -import ExtensionReport from './extension/legacy/report'; -import ExtensionSoftReset from './extension/legacy/softReset'; import ExtensionNetworkMap from './extension/networkMap'; import ExtensionOnEvent from './extension/onEvent'; import ExtensionOTAUpdate from './extension/otaUpdate'; @@ -41,15 +37,11 @@ const AllExtensions = [ ExtensionPublish, ExtensionReceive, ExtensionNetworkMap, - ExtensionSoftReset, ExtensionHomeAssistant, ExtensionConfigure, - ExtensionDeviceGroupMembership, - ExtensionBridgeLegacy, ExtensionBridge, ExtensionGroups, ExtensionBind, - ExtensionReport, ExtensionOnEvent, ExtensionOTAUpdate, ExtensionExternalConverters, @@ -108,13 +100,11 @@ export class Controller { new ExtensionBridge(...this.extensionArgs), new ExtensionPublish(...this.extensionArgs), new ExtensionReceive(...this.extensionArgs), - new ExtensionDeviceGroupMembership(...this.extensionArgs), new ExtensionConfigure(...this.extensionArgs), new ExtensionNetworkMap(...this.extensionArgs), new ExtensionGroups(...this.extensionArgs), new ExtensionBind(...this.extensionArgs), new ExtensionOTAUpdate(...this.extensionArgs), - new ExtensionReport(...this.extensionArgs), new ExtensionExternalExtension(...this.extensionArgs), new ExtensionAvailability(...this.extensionArgs), ]; @@ -123,10 +113,6 @@ export class Controller { this.extensions.push(new ExtensionFrontend(...this.extensionArgs)); } - if (settings.get().advanced.legacy_api) { - this.extensions.push(new ExtensionBridgeLegacy(...this.extensionArgs)); - } - if (settings.get().external_converters.length) { this.extensions.push(new ExtensionExternalConverters(...this.extensionArgs)); } @@ -134,11 +120,6 @@ export class Controller { if (settings.get().homeassistant) { this.extensions.push(new ExtensionHomeAssistant(...this.extensionArgs)); } - - /* istanbul ignore next */ - if (settings.get().advanced.soft_reset_timeout !== 0) { - this.extensions.push(new ExtensionSoftReset(...this.extensionArgs)); - } } async start(): Promise { @@ -156,9 +137,8 @@ export class Controller { } // Start zigbee - let startResult; try { - startResult = await this.zigbee.start(); + await this.zigbee.start(); this.eventBus.onAdapterDisconnected(this, this.onZigbeeAdapterDisconnected); } catch (error) { logger.error('Failed to start zigbee-herdsman'); @@ -172,15 +152,6 @@ export class Controller { return await this.exit(1); } - // Disable some legacy options on new network creation - if (startResult === 'reset') { - settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false); - settings.set(['advanced', 'legacy_api'], false); - settings.set(['advanced', 'legacy_availability_payload'], false); - settings.set(['device_options', 'legacy'], false); - await this.enableDisableExtension(false, 'BridgeLegacy'); - } - // Log zigbee clients on startup let deviceCount = 0; @@ -356,7 +327,7 @@ export class Controller { // Filter mqtt message attributes utils.filterProperties(entity.options.filtered_attributes, message); - if (Object.entries(message).length) { + if (!utils.objectIsEmpty(message)) { const output = settings.get().advanced.output; if (output === 'attribute_and_json' || output === 'json') { await this.mqtt.publish(entity.name, stringify(message), options); diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 13110547c9..d0b5adfda0 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -56,13 +56,6 @@ export default class EventBus { this.on('permitJoinChanged', callback, key); } - public emitPublishAvailability(): void { - this.emitter.emit('publishAvailability'); - } - public onPublishAvailability(key: ListenerKey, callback: () => void): void { - this.on('publishAvailability', callback, key); - } - public emitEntityRenamed(data: eventdata.EntityRenamed): void { this.emitter.emit('deviceRenamed', data); } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index f13552bd27..a32771b788 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -140,7 +140,6 @@ export default class Availability extends Extension { this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device)); this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged); - this.eventBus.onPublishAvailability(this, this.publishAvailabilityForAllEntities); this.eventBus.onGroupMembersChanged(this, (data) => this.publishAvailability(data.group, false)); // Publish initial availability await this.publishAvailabilityForAllEntities(); @@ -189,7 +188,7 @@ export default class Availability extends Extension { } const topic = `${entity.name}/availability`; - const payload = utils.availabilityPayload(available ? 'online' : 'offline', settings.get()); + const payload = JSON.stringify({state: available ? 'online' : 'offline'}); this.availabilityCache[entity.ID] = available; await this.mqtt.publish(topic, payload, {retain: true, qos: 1}); diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 1ddbaeefc3..c917f5270c 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -14,8 +14,6 @@ import * as settings from '../util/settings'; import utils from '../util/utils'; import Extension from './extension'; -const LEGACY_API = settings.get().advanced.legacy_api; -const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`); const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`); const ALL_CLUSTER_CANDIDATES: readonly ClusterName[] = [ 'genScenes', @@ -226,12 +224,7 @@ export default class Bind extends Extension { let clusters: ParsedMQTTMessage['clusters'] | undefined; let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false; - if (LEGACY_API && data.topic.match(LEGACY_TOPIC_REGEX)) { - const topic = data.topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, ''); - type = topic.split('/')[0] as ParsedMQTTMessage['type']; - sourceKey = topic.replace(`${type}/`, ''); - targetKey = data.message; - } else if (data.topic.match(TOPIC_REGEX)) { + if (data.topic.match(TOPIC_REGEX)) { type = data.topic.endsWith('unbind') ? 'unbind' : 'bind'; const message: DataMessage = JSON.parse(data.message); sourceKey = message.from; @@ -312,25 +305,9 @@ export default class Bind extends Extension { logger.info( `Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from '${source.name}' to '${target.name}'`, ); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_${type}`, message: {from: source.name, to: target.name, cluster}}), - ); - } } catch (error) { failedClusters.push(cluster); logger.error(`Failed to ${type} cluster '${cluster}' from '${source.name}' to '${target.name}' (${error})`); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - await this.mqtt.publish( - 'bridge/log', - stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name, cluster}}), - ); - } } } } @@ -338,11 +315,6 @@ export default class Bind extends Extension { if (attemptedClusters.length === 0) { logger.error(`Nothing to ${type} from '${source.name}' to '${target.name}'`); error = `Nothing to ${type}`; - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - await this.mqtt.publish('bridge/log', stringify({type: `device_${type}_failed`, message: {from: source.name, to: target.name}})); - } } else if (failedClusters.length === attemptedClusters.length) { error = `Failed to ${type}`; } @@ -359,13 +331,9 @@ export default class Bind extends Extension { } } - const triggeredViaLegacyApi = data.topic.match(LEGACY_TOPIC_REGEX); + const response = utils.getResponse(message, responseData, error); - if (!triggeredViaLegacyApi) { - const response = utils.getResponse(message, responseData, error); - - await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response)); - } + await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response)); if (error) { logger.error(error); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 6187e53619..d5d1549d30 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -70,11 +70,6 @@ export default class Bridge extends Extension { health_check: this.healthCheck, coordinator_check: this.coordinatorCheck, options: this.bridgeOptions, - // Below are deprecated - 'config/last_seen': this.configLastSeen, - 'config/homeassistant': this.configHomeAssistant, - 'config/elapsed': this.configElapsed, - 'config/log_level': this.configLogLevel, }; const debugToMQTTFrontend = settings.get().advanced.log_debug_to_mqtt_frontend; @@ -374,58 +369,6 @@ export default class Bridge extends Extension { return utils.getResponse(message, response); } - // Deprecated - @bind async configLastSeen(message: KeyValue | string): Promise { - const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local']; - const value = this.getValue(message); - if (typeof value !== 'string' || !allowed.includes(value)) { - throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`); - } - - settings.set(['advanced', 'last_seen'], value); - await this.publishInfo(); - return utils.getResponse(message, {value}); - } - - // Deprecated - @bind async configHomeAssistant(message: string | KeyValue): Promise { - const allowed = [true, false]; - const value = this.getValue(message); - if (typeof value !== 'boolean' || !allowed.includes(value)) { - throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`); - } - - settings.set(['homeassistant'], value); - await this.enableDisableExtension(value, 'HomeAssistant'); - await this.publishInfo(); - return utils.getResponse(message, {value}); - } - - // Deprecated - @bind async configElapsed(message: KeyValue | string): Promise { - const allowed = [true, false]; - const value = this.getValue(message); - if (typeof value !== 'boolean' || !allowed.includes(value)) { - throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`); - } - - settings.set(['advanced', 'elapsed'], value); - await this.publishInfo(); - return utils.getResponse(message, {value}); - } - - // Deprecated - @bind async configLogLevel(message: KeyValue | string): Promise { - const value = this.getValue(message) as settings.LogLevel; - if (typeof value !== 'string' || !settings.LOG_LEVELS.includes(value)) { - throw new Error(`'${value}' is not an allowed value, allowed: ${settings.LOG_LEVELS}`); - } - - logger.setLevel(value); - await this.publishInfo(); - return utils.getResponse(message, {value}); - } - @bind async touchlinkIdentify(message: KeyValue | string): Promise { if (typeof message !== 'object' || message.ieee_address === undefined || message.channel === undefined) { throw new Error('Invalid payload'); @@ -472,18 +415,6 @@ export default class Bridge extends Extension { * Utils */ - getValue(message: KeyValue | string): string | boolean | number { - if (typeof message === 'object') { - if (message.value === undefined) { - throw new Error('No value given'); - } - - return message.value; - } else { - return message; - } - } - async changeEntityOptions(entityType: 'device' | 'group', message: KeyValue | string): Promise { if (typeof message !== 'object' || message.id === undefined || message.options === undefined) { throw new Error(`Invalid payload`); diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 3068906340..cda7e74023 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -16,7 +16,6 @@ export default class Configure extends Extension { private configuring = new Set(); private attempts: {[s: string]: number} = {}; private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`; - private legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`; @bind private async onReconfigure(data: eventdata.Reconfigure): Promise { // Disabling reporting unbinds some cluster which could be bound by configure, re-setup. @@ -29,20 +28,7 @@ export default class Configure extends Extension { } @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - if (data.topic === this.legacyTopic) { - const device = this.zigbee.resolveEntity(data.message); - if (!device || !(device instanceof Device)) { - logger.error(`Device '${data.message}' does not exist`); - return; - } - - if (!device.definition || !device.definition.configure) { - logger.warning(`Skipping configure of '${device.name}', device does not require this.`); - return; - } - - await this.configure(device, 'mqtt_message', true); - } else if (data.topic === this.topic) { + if (data.topic === this.topic) { const message = utils.parseJSON(data.message, data.message); const ID = typeof message === 'object' && message.id !== undefined ? message.id : message; let error: string | undefined; diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index c462359b6a..114774c35a 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -14,8 +14,6 @@ import utils, {isLightExpose} from '../util/utils'; import Extension from './extension'; const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`); -const LEGACY_TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`); -const LEGACY_TOPIC_REGEX_REMOVE_ALL = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`); const STATE_PROPERTIES: Readonly boolean>> = { state: () => true, @@ -37,13 +35,11 @@ interface ParsedMQTTMessage { error?: string; groupKey?: string; deviceKey?: string; - triggeredViaLegacyApi: boolean; skipDisableReporting: boolean; resolvedEntityEndpoint?: zh.Endpoint; } export default class Groups extends Extension { - private legacyApi = settings.get().advanced.legacy_api; private lastOptimisticState: {[s: string]: KeyValue} = {}; override async start(): Promise { @@ -260,60 +256,12 @@ export default class Groups extends Extension { let error: ParsedMQTTMessage['error'] | undefined; let groupKey: ParsedMQTTMessage['groupKey'] | undefined; let deviceKey: ParsedMQTTMessage['deviceKey'] | undefined; - let triggeredViaLegacyApi: ParsedMQTTMessage['triggeredViaLegacyApi'] = false; let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false; /* istanbul ignore else */ const topicRegexMatch = data.topic.match(TOPIC_REGEX); - const legacyTopicRegexRemoveAllMatch = data.topic.match(LEGACY_TOPIC_REGEX_REMOVE_ALL); - const legacyTopicRegexMatch = data.topic.match(LEGACY_TOPIC_REGEX); - if (this.legacyApi && (legacyTopicRegexMatch || legacyTopicRegexRemoveAllMatch)) { - triggeredViaLegacyApi = true; - - if (legacyTopicRegexMatch) { - resolvedEntityGroup = this.zigbee.resolveEntity(legacyTopicRegexMatch[1]) as Group; - type = legacyTopicRegexMatch[2] as ParsedMQTTMessage['type']; - - if (!resolvedEntityGroup || !(resolvedEntityGroup instanceof Group)) { - logger.error(`Group '${legacyTopicRegexMatch[1]}' does not exist`); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: `group doesn't exists`}; - - await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); - } - - return undefined; - } - } else { - type = 'remove_all'; - } - - const parsedEntity = this.zigbee.resolveEntityAndEndpoint(data.message); - resolvedEntityDevice = parsedEntity.entity as Device; - - if (!resolvedEntityDevice || !(resolvedEntityDevice instanceof Device)) { - logger.error(`Device '${data.message}' does not exist`); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const message = {friendly_name: data.message, group: legacyTopicRegexMatch![1], error: "entity doesn't exists"}; - - await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); - } - - return undefined; - } - - resolvedEntityEndpoint = parsedEntity.endpoint; - - if (parsedEntity.endpointID && !resolvedEntityEndpoint) { - logger.error(`Device '${parsedEntity.ID}' does not have endpoint '${parsedEntity.endpointID}'`); - return undefined; - } - } else if (topicRegexMatch) { + if (topicRegexMatch) { type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all'; const message = JSON.parse(data.message); deviceKey = message.device; @@ -353,7 +301,6 @@ export default class Groups extends Extension { error, groupKey, deviceKey, - triggeredViaLegacyApi, skipDisableReporting, resolvedEntityEndpoint, }; @@ -366,16 +313,7 @@ export default class Groups extends Extension { return; } - const { - resolvedEntityGroup, - resolvedEntityDevice, - type, - triggeredViaLegacyApi, - groupKey, - deviceKey, - skipDisableReporting, - resolvedEntityEndpoint, - } = parsed; + const {resolvedEntityGroup, resolvedEntityDevice, type, groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint} = parsed; let error = parsed.error; const changedGroups: Group[] = []; @@ -404,26 +342,12 @@ export default class Groups extends Extension { await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh); settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys); changedGroups.push(resolvedEntityGroup); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - - await this.mqtt.publish('bridge/log', stringify({type: `device_group_add`, message})); - } } else if (type === 'remove') { assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing'); logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`); await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh); settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys); changedGroups.push(resolvedEntityGroup); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const message = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name}; - - await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove`, message})); - } } else { // remove_all logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`); @@ -436,13 +360,6 @@ export default class Groups extends Extension { for (const settingsGroup of settings.getGroups()) { settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const message = {friendly_name: resolvedEntityDevice.name}; - - await this.mqtt.publish('bridge/log', stringify({type: `device_group_remove_all`, message})); - } } } } catch (e) { @@ -451,17 +368,15 @@ export default class Groups extends Extension { } } - if (!triggeredViaLegacyApi) { - const message = utils.parseJSON(data.message, data.message); - const responseData: KeyValue = {device: deviceKey}; + const message = utils.parseJSON(data.message, data.message); + const responseData: KeyValue = {device: deviceKey}; - if (groupKey) { - responseData.group = groupKey; - } - - await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error))); + if (groupKey) { + responseData.group = groupKey; } + await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error))); + if (error) { logger.error(error); } else { diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 3c3a7cac81..4e964d2b7e 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -542,9 +542,6 @@ export default class HomeAssistant extends Extension { await this.discover(e); } }, utils.seconds(discoverWait)); - - // Send availability messages, this is required if the legacy_availability_payload option has been changed. - this.eventBus.emitPublishAvailability(); } private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered { @@ -1693,7 +1690,7 @@ export default class HomeAssistant extends Extension { if (isDevice && entity.options.disabled) { // Mark disabled device always as unavailable payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ "offline" }}')); - } else if (!settings.get().advanced.legacy_availability_payload) { + } else { payload.availability.forEach((a: KeyValue) => (a.value_template = '{{ value_json.state }}')); } } else { @@ -2069,7 +2066,6 @@ export default class HomeAssistant extends Extension { const discovery: DiscoveryEntry[] = []; const bridge = new Bridge(coordinatorIeeeAddress, coordinatorVersion, discovery); const baseTopic = `${settings.get().mqtt.base_topic}/${bridge.name}`; - const legacyAvailability = settings.get().advanced.legacy_availability_payload; discovery.push( // Binary sensors. @@ -2083,7 +2079,7 @@ export default class HomeAssistant extends Extension { entity_category: 'diagnostic', state_topic: true, state_topic_postfix: 'state', - value_template: !legacyAvailability ? '{{ value_json.state }}' : '{{ value }}', + value_template: '{{ value_json.state }}', payload_on: 'online', payload_off: 'offline', availability: false, diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts deleted file mode 100644 index 4bf05934d4..0000000000 --- a/lib/extension/legacy/bridgeLegacy.ts +++ /dev/null @@ -1,424 +0,0 @@ -import assert from 'assert'; - -import bind from 'bind-decorator'; -import stringify from 'json-stable-stringify-without-jsonify'; - -import logger from '../../util/logger'; -import * as settings from '../../util/settings'; -import utils from '../../util/utils'; -import Extension from '../extension'; - -const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`); - -export default class BridgeLegacy extends Extension { - private lastJoinedDeviceName?: string; - // @ts-expect-error initialized in `start` - private supportedOptions: {[s: string]: (topic: string, message: string) => Promise | void}; - - override async start(): Promise { - this.supportedOptions = { - permit_join: this.permitJoin, - last_seen: this.lastSeen, - elapsed: this.elapsed, - reset: this.reset, - log_level: this.logLevel, - devices: this.devices, - groups: this.groups, - 'devices/get': this.devices, - rename: this.rename, - rename_last: this.renameLast, - remove: this.remove, - force_remove: this.forceRemove, - ban: this.ban, - device_options: this.deviceOptions, - add_group: this.addGroup, - remove_group: this.removeGroup, - force_remove_group: this.removeGroup, - whitelist: this.whitelist, - 'touchlink/factory_reset': this.touchlinkFactoryReset, - }; - - this.eventBus.onDeviceJoined(this, (data) => this.onZigbeeEvent_('deviceJoined', data, data.device)); - this.eventBus.onDeviceInterview(this, (data) => this.onZigbeeEvent_('deviceInterview', data, data.device)); - this.eventBus.onDeviceAnnounce(this, (data) => this.onZigbeeEvent_('deviceAnnounce', data, data.device)); - this.eventBus.onDeviceLeave(this, (data) => this.onZigbeeEvent_('deviceLeave', data, undefined)); - this.eventBus.onMQTTMessage(this, this.onMQTTMessage); - - await this.publish(); - } - - @bind async whitelist(topic: string, message: string): Promise { - try { - const entity = settings.getDevice(message); - assert(entity, `Entity '${message}' does not exist`); - settings.addDeviceToPasslist(entity.ID.toString()); - logger.info(`Whitelisted '${entity.friendly_name}'`); - await this.mqtt.publish('bridge/log', stringify({type: 'device_whitelisted', message: {friendly_name: entity.friendly_name}})); - } catch (error) { - logger.error(`Failed to whitelist '${message}' '${error}'`); - } - } - - @bind deviceOptions(topic: string, message: string): void { - let json = null; - try { - json = JSON.parse(message); - } catch { - logger.error('Failed to parse message as JSON'); - return; - } - - if (json.friendly_name === undefined || json.options === undefined) { - logger.error('Invalid JSON message, should contain "friendly_name" and "options"'); - return; - } - - const entity = settings.getDevice(json.friendly_name); - assert(entity, `Entity '${json.friendly_name}' does not exist`); - settings.changeEntityOptions(entity.ID.toString(), json.options); - logger.info(`Changed device specific options of '${json.friendly_name}' (${stringify(json.options)})`); - } - - @bind async permitJoin(topic: string, message: string): Promise { - await this.zigbee.permitJoin(message.toLowerCase() === 'true'); - await this.publish(); - } - - @bind async reset(): Promise { - try { - await this.zigbee.reset('soft'); - logger.info('Soft reset ZNP'); - } catch { - logger.error('Soft reset failed'); - } - } - - @bind lastSeen(topic: string, message: string): void { - const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local']; - if (!allowed.includes(message)) { - logger.error(`${message} is not an allowed value, possible: ${allowed}`); - return; - } - - settings.set(['advanced', 'last_seen'], message); - logger.info(`Set last_seen to ${message}`); - } - - @bind elapsed(topic: string, message: string): void { - const allowed = ['true', 'false']; - if (!allowed.includes(message)) { - logger.error(`${message} is not an allowed value, possible: ${allowed}`); - return; - } - - settings.set(['advanced', 'elapsed'], message === 'true'); - logger.info(`Set elapsed to ${message}`); - } - - @bind async logLevel(topic: string, message: string): Promise { - const level = message.toLowerCase() as settings.LogLevel; - if (settings.LOG_LEVELS.includes(level)) { - logger.info(`Switching log level to '${level}'`); - logger.setLevel(level); - } else { - logger.error(`Could not set log level to '${level}'. Allowed level: '${settings.LOG_LEVELS.join(',')}'`); - } - - await this.publish(); - } - - @bind async devices(topic: string): Promise { - const coordinator = await this.zigbee.getCoordinatorVersion(); - const devices: KeyValue[] = []; - - for (const device of this.zigbee.devicesIterator()) { - const payload: KeyValue = { - ieeeAddr: device.ieeeAddr, - type: device.zh.type, - networkAddress: device.zh.networkAddress, - }; - - if (device.zh.type !== 'Coordinator') { - const definition = device.definition; - payload.model = definition ? definition.model : device.zh.modelID; - payload.vendor = definition ? definition.vendor : '-'; - payload.description = definition ? definition.description : '-'; - payload.friendly_name = device.name; - payload.manufacturerID = device.zh.manufacturerID; - payload.manufacturerName = device.zh.manufacturerName; - payload.powerSource = device.zh.powerSource; - payload.modelID = device.zh.modelID; - payload.hardwareVersion = device.zh.hardwareVersion; - payload.softwareBuildID = device.zh.softwareBuildID; - payload.dateCode = device.zh.dateCode; - payload.lastSeen = device.zh.lastSeen; - } else { - payload.friendly_name = 'Coordinator'; - payload.softwareBuildID = coordinator.type; - payload.dateCode = coordinator.meta.revision.toString(); - payload.lastSeen = Date.now(); - } - - devices.push(payload); - } - - if (topic.split('/').pop() == 'get') { - await this.mqtt.publish(`bridge/config/devices`, stringify(devices), {}, settings.get().mqtt.base_topic, false, false); - } else { - await this.mqtt.publish('bridge/log', stringify({type: 'devices', message: devices})); - } - } - - @bind async groups(): Promise { - const payload = settings.getGroups().map((g) => { - return {...g, ID: Number(g.ID)}; - }); - - await this.mqtt.publish('bridge/log', stringify({type: 'groups', message: payload})); - } - - @bind async rename(topic: string, message: string): Promise { - const invalid = `Invalid rename message format expected {"old": "friendly_name", "new": "new_name"} got ${message}`; - - let json = null; - try { - json = JSON.parse(message); - } catch { - logger.error(invalid); - return; - } - - // Validate message - if (!json.new || !json.old) { - logger.error(invalid); - return; - } - - await this._renameInternal(json.old, json.new); - } - - @bind async renameLast(topic: string, message: string): Promise { - if (!this.lastJoinedDeviceName) { - logger.error(`Cannot rename last joined device, no device has joined during this session`); - return; - } - - await this._renameInternal(this.lastJoinedDeviceName, message); - } - - async _renameInternal(from: string, to: string): Promise { - try { - const isGroup = settings.getGroup(from) != undefined; - settings.changeFriendlyName(from, to); - logger.info(`Successfully renamed - ${from} to ${to} `); - const entity = this.zigbee.resolveEntity(to); - if (entity?.isDevice()) { - this.eventBus.emitEntityRenamed({homeAssisantRename: false, from, to, entity}); - } - - await this.mqtt.publish('bridge/log', stringify({type: `${isGroup ? 'group' : 'device'}_renamed`, message: {from, to}})); - } catch { - logger.error(`Failed to rename - ${from} to ${to}`); - } - } - - @bind async addGroup(topic: string, message: string): Promise { - let id = null; - let name = null; - try { - // json payload with id and friendly_name - const json = JSON.parse(message); - if (json.id !== undefined) { - id = json.id; - name = `group_${id}`; - } - if (json.friendly_name !== undefined) { - name = json.friendly_name; - } - } catch { - // just friendly_name - name = message; - } - - if (name == null) { - logger.error('Failed to add group, missing friendly_name!'); - return; - } - - const group = settings.addGroup(name, id); - this.zigbee.createGroup(group.ID); - await this.mqtt.publish('bridge/log', stringify({type: `group_added`, message: name})); - logger.info(`Added group '${name}'`); - } - - @bind async removeGroup(topic: string, message: string): Promise { - const name = message; - const entity = this.zigbee.resolveEntity(message) as Group; - assert(entity && entity.isGroup(), `Group '${message}' does not exist`); - - if (topic.includes('force')) { - entity.zh.removeFromDatabase(); - } else { - await entity.zh.removeFromNetwork(); - } - settings.removeGroup(message); - - await this.mqtt.publish('bridge/log', stringify({type: `group_removed`, message})); - logger.info(`Removed group '${name}'`); - } - - @bind async forceRemove(topic: string, message: string): Promise { - await this.removeForceRemoveOrBan('force_remove', message); - } - - @bind async remove(topic: string, message: string): Promise { - await this.removeForceRemoveOrBan('remove', message); - } - - @bind async ban(topic: string, message: string): Promise { - await this.removeForceRemoveOrBan('ban', message); - } - - @bind async removeForceRemoveOrBan(action: string, message: string): Promise { - const entity = this.zigbee.resolveEntity(message.trim()) as Device; - const lookup: KeyValue = { - ban: ['banned', 'Banning', 'ban'], - force_remove: ['force_removed', 'Force removing', 'force remove'], - remove: ['removed', 'Removing', 'remove'], - }; - - if (!entity) { - logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`); - - await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); - return; - } - - const ieeeAddr = entity.ieeeAddr; - const name = entity.name; - - const cleanup = async (): Promise => { - // Fire event - this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'}); - - // Remove from configuration.yaml - settings.removeDevice(entity.ieeeAddr); - - // Remove from state - this.state.remove(ieeeAddr); - - logger.info(`Successfully ${lookup[action][0]} ${entity.name}`); - await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}`, message})); - }; - - try { - logger.info(`${lookup[action][1]} '${entity.name}'`); - if (action === 'force_remove') { - entity.zh.removeFromDatabase(); - } else { - await entity.zh.removeFromNetwork(); - } - - await cleanup(); - } catch (error) { - logger.error(`Failed to ${lookup[action][2]} ${entity.name} (${error})`); - - logger.error(`See https://www.zigbee2mqtt.io/guide/usage/mqtt_topics_and_messages.html#zigbee2mqtt-bridge-request for more info`); - - await this.mqtt.publish('bridge/log', stringify({type: `device_${lookup[action][0]}_failed`, message})); - } - - if (action === 'ban') { - settings.blockDevice(ieeeAddr); - } - } - - @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const {topic, message} = data; - const match = topic.match(configRegex); - - if (!match) { - return; - } - - const option = match[1]; - - if (this.supportedOptions[option] === undefined) { - return; - } - - await this.supportedOptions[option](topic, message); - - return; - } - - async publish(): Promise { - const info = await utils.getZigbee2MQTTVersion(); - const coordinator = await this.zigbee.getCoordinatorVersion(); - const topic = `bridge/config`; - const payload = { - version: info.version, - commit: info.commitHash, - coordinator, - network: await this.zigbee.getNetworkParameters(), - log_level: logger.getLevel(), - permit_join: this.zigbee.getPermitJoin(), - }; - - await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}); - } - - async onZigbeeEvent_(type: string, data: KeyValue, resolvedEntity: Device | undefined): Promise { - if (resolvedEntity) { - /* istanbul ignore else */ - if (type === 'deviceJoined') { - this.lastJoinedDeviceName = resolvedEntity.name; - await this.mqtt.publish('bridge/log', stringify({type: `device_connected`, message: {friendly_name: resolvedEntity.name}})); - } else if (type === 'deviceInterview') { - if (data.status === 'successful') { - if (resolvedEntity.isSupported) { - const {vendor, description, model} = resolvedEntity.definition!; // checked by `isSupported` - const log = {friendly_name: resolvedEntity.name, model, vendor, description, supported: true}; - await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta: log})); - } else { - const meta = {friendly_name: resolvedEntity.name, supported: false}; - await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_successful', meta})); - } - } else if (data.status === 'failed') { - const meta = {friendly_name: resolvedEntity.name}; - await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_failed', meta})); - } else { - /* istanbul ignore else */ - if (data.status === 'started') { - const meta = {friendly_name: resolvedEntity.name}; - await this.mqtt.publish('bridge/log', stringify({type: `pairing`, message: 'interview_started', meta})); - } - } - } else if (type === 'deviceAnnounce') { - const meta = {friendly_name: resolvedEntity.name}; - await this.mqtt.publish('bridge/log', stringify({type: `device_announced`, message: 'announce', meta})); - } - } else { - /* istanbul ignore else */ - if (type === 'deviceLeave') { - const name = data.ieeeAddr; - const meta = {friendly_name: name}; - await this.mqtt.publish('bridge/log', stringify({type: `device_removed`, message: 'left_network', meta})); - } - } - } - - @bind async touchlinkFactoryReset(): Promise { - logger.info('Starting touchlink factory reset...'); - await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_started', meta: {status: 'started'}})); - const result = await this.zigbee.touchlinkFactoryResetFirst(); - - if (result) { - logger.info('Successfully factory reset device through Touchlink'); - await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_success', meta: {status: 'success'}})); - } else { - logger.warning('Failed to factory reset device through Touchlink'); - await this.mqtt.publish('bridge/log', stringify({type: `touchlink`, message: 'reset_failed', meta: {status: 'failed'}})); - } - } -} diff --git a/lib/extension/legacy/deviceGroupMembership.ts b/lib/extension/legacy/deviceGroupMembership.ts deleted file mode 100644 index f3b618e7d6..0000000000 --- a/lib/extension/legacy/deviceGroupMembership.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* istanbul ignore file */ - -import assert from 'assert'; - -import bind from 'bind-decorator'; - -import Device from '../../model/device'; -import logger from '../../util/logger'; -import * as settings from '../../util/settings'; -import Extension from '../extension'; - -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`); - -export default class DeviceGroupMembership extends Extension { - override async start(): Promise { - this.eventBus.onMQTTMessage(this, this.onMQTTMessage); - } - - @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const match = data.topic.match(topicRegex); - - if (!match) { - return; - } - - const parsed = this.zigbee.resolveEntityAndEndpoint(match[1]); - const device = parsed?.entity as Device; - - if (!device || !(device instanceof Device)) { - logger.error(`Device '${match[1]}' does not exist`); - return; - } - - const endpoint = parsed.endpoint; - - if (parsed.endpointID && !endpoint) { - logger.error(`Device '${parsed.ID}' does not have endpoint '${parsed.endpointID}'`); - return; - } - - assert(endpoint !== undefined); - const response = await endpoint.command(`genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {}); - - if (!response) { - logger.warning(`Couldn't get group membership of ${device.ieeeAddr}`); - return; - } - - let {grouplist} = response; - - grouplist = grouplist.map((gid: string) => { - const g = settings.getGroup(gid); - return g ? g.friendly_name : gid; - }); - - const msgGroupList = `${device.ieeeAddr} is in groups [${grouplist}]`; - let msgCapacity; - if (response.capacity === 254) { - msgCapacity = 'it can be a part of at least 1 more group'; - } else { - msgCapacity = `its remaining group capacity is ${response.capacity === 255 ? 'unknown' : response.capacity}`; - } - logger.info(`${msgGroupList} and ${msgCapacity}`); - - await this.publishEntityState(device, {group_list: grouplist, group_capacity: response.capacity}); - } -} diff --git a/lib/extension/legacy/report.ts b/lib/extension/legacy/report.ts deleted file mode 100644 index 5e01328075..0000000000 --- a/lib/extension/legacy/report.ts +++ /dev/null @@ -1,204 +0,0 @@ -import * as zhc from 'zigbee-herdsman-converters'; - -import logger from '../../util/logger'; -import * as settings from '../../util/settings'; -import utils from '../../util/utils'; -import Extension from '../extension'; - -const defaultConfiguration = { - minimumReportInterval: 3, - maximumReportInterval: 300, - reportableChange: 1, -}; - -const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM'); - -const devicesNotSupportingReporting = [ - zhc.definitions.find((d) => d.model === 'CC2530.ROUTER'), - zhc.definitions.find((d) => d.model === 'BASICZBR3'), - zhc.definitions.find((d) => d.model === 'ZM-CSW032-D'), - zhc.definitions.find((d) => d.model === 'TS0001'), - zhc.definitions.find((d) => d.model === 'TS0115'), -]; - -const reportKey = 1; - -const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemperature: boolean; colorXY: boolean}> => { - if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') === undefined) { - await endpoint.read('lightingColorCtrl', ['colorCapabilities']); - } - - const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') as number; - return { - colorTemperature: (value & (1 << 4)) > 0, - colorXY: (value & (1 << 3)) > 0, - }; -}; - -const clusters: { - [s: string]: { - attribute: string; - minimumReportInterval: number; - maximumReportInterval: number; - reportableChange: number; - condition?: (endpoint: zh.Endpoint) => Promise; - }[]; -} = { - genOnOff: [{attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0}], - genLevelCtrl: [{attribute: 'currentLevel', ...defaultConfiguration}], - lightingColorCtrl: [ - { - attribute: 'colorTemperature', - ...defaultConfiguration, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorTemperature, - }, - { - attribute: 'currentX', - ...defaultConfiguration, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, - }, - { - attribute: 'currentY', - ...defaultConfiguration, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, - }, - ], - closuresWindowCovering: [ - {attribute: 'currentPositionLiftPercentage', ...defaultConfiguration}, - {attribute: 'currentPositionTiltPercentage', ...defaultConfiguration}, - ], -}; - -export default class Report extends Extension { - private queue: Set = new Set(); - private failed: Set = new Set(); - private enabled = settings.get().advanced.report; - - shouldIgnoreClusterForDevice(cluster: string, definition?: zhc.Definition): boolean { - if (definition === ZNLDP12LM && cluster === 'closuresWindowCovering') { - // Device announces it but doesn't support it - // https://github.com/Koenkk/zigbee2mqtt/issues/2611 - return true; - } - - return false; - } - - async setupReporting(device: Device): Promise { - if (this.queue.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return; - this.queue.add(device.ieeeAddr); - - const term1 = this.enabled ? 'Setup' : 'Disable'; - const term2 = this.enabled ? 'setup' : 'disabled'; - - try { - for (const ep of device.zh.endpoints) { - for (const [cluster, configuration] of Object.entries(clusters)) { - if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, device.definition)) { - logger.debug(`${term1} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); - - const items = []; - for (const entry of configuration) { - if (entry.condition == undefined || (await entry.condition(ep))) { - const toAdd = {...entry}; - if (!this.enabled) toAdd.maximumReportInterval = 0xffff; - items.push(toAdd); - delete items[items.length - 1].condition; - } - } - - if (this.enabled) { - await ep.bind(cluster, this.zigbee.firstCoordinatorEndpoint()); - } else { - await ep.unbind(cluster, this.zigbee.firstCoordinatorEndpoint()); - } - - await ep.configureReporting(cluster, items); - logger.info(`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); - } - } - } - - if (this.enabled) { - device.zh.meta.reporting = reportKey; - } else { - delete device.zh.meta.reporting; - this.eventBus.emitReconfigure({device}); - } - - this.eventBus.emitDevicesChanged(); - } catch (error) { - logger.error(`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${(error as Error).stack}`); - - this.failed.add(device.ieeeAddr); - } - - device.zh.save(); - this.queue.delete(device.ieeeAddr); - } - - shouldSetupReporting(device: Device, messageType?: string): boolean { - if (!device || !device.zh || !device.definition) return false; - - // Handle messages of type endDeviceAnnce and devIncoming. - // This message is typically send when a device comes online after being powered off - // Ikea TRADFRI tend to forget their reporting after powered off. - // Re-setup reporting. - // Only resetup reporting if configuredReportings was not populated yet, - // else reconfigure is done in zigbee-herdsman-converters ikea.js/bulbOnEvent - // configuredReportings are saved since Zigbee2MQTT 1.17.0 - // https://github.com/Koenkk/zigbee2mqtt/issues/966 - if ( - this.enabled && - messageType === 'deviceAnnounce' && - device.isIkeaTradfri() && - device.zh.endpoints.filter((e) => e.configuredReportings.length === 0).length === device.zh.endpoints.length - ) { - return true; - } - - // These do not support reporting. - // https://github.com/Koenkk/zigbee-herdsman/issues/110 - if ( - device.zh.manufacturerName === 'Philips' && - /* istanbul ignore next */ - (device.zh.softwareBuildID === '5.127.1.26581' || device.zh.softwareBuildID === '5.130.1.30000') - ) { - return false; - } - - if (device.zh.interviewing === true) return false; - if (device.zh.type !== 'Router' || device.zh.powerSource === 'Battery') return false; - // Gledopto devices don't support reporting. - if (devicesNotSupportingReporting.includes(device.definition) || device.definition.vendor === 'Gledopto') return false; - - if (this.enabled && device.zh.meta.reporting !== undefined && device.zh.meta.reporting === reportKey) { - return false; - } - - if (!this.enabled && device.zh.meta.reporting === undefined) { - return false; - } - - return true; - } - - override async start(): Promise { - for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) { - if (this.shouldSetupReporting(device, undefined)) { - await this.setupReporting(device); - } - } - - this.eventBus.onDeviceAnnounce(this, (data) => this.onZigbeeEvent_('deviceAnnounce', data.device)); - this.eventBus.onDeviceMessage(this, (data) => this.onZigbeeEvent_('dummy', data.device)); - this.eventBus.onDeviceJoined(this, (data) => this.onZigbeeEvent_('dummy', data.device)); - this.eventBus.onDeviceNetworkAddressChanged(this, (data) => this.onZigbeeEvent_('dummy', data.device)); - } - - async onZigbeeEvent_(type: string, device: Device): Promise { - if (this.shouldSetupReporting(device, type)) { - await this.setupReporting(device); - } - } -} diff --git a/lib/extension/legacy/softReset.ts b/lib/extension/legacy/softReset.ts deleted file mode 100644 index c58ce3dd4e..0000000000 --- a/lib/extension/legacy/softReset.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* istanbul ignore file */ - -import logger from '../../util/logger'; -// DEPRECATED -import * as settings from '../../util/settings'; -import utils from '../../util/utils'; -import Extension from '../extension'; - -/** - * This extensions soft resets the ZNP after a certain timeout. - */ -export default class SoftReset extends Extension { - private timer?: NodeJS.Timeout; - private timeout = utils.seconds(settings.get().advanced.soft_reset_timeout); - - override async start(): Promise { - logger.debug(`Soft reset timeout set to ${this.timeout / 1000} seconds`); - this.resetTimer(); - this.eventBus.onDeviceMessage(this, () => this.resetTimer()); - this.eventBus.onDeviceAnnounce(this, () => this.resetTimer()); - this.eventBus.onDeviceNetworkAddressChanged(this, () => this.resetTimer()); - this.eventBus.onDeviceJoined(this, () => this.resetTimer()); - this.eventBus.onDeviceInterview(this, () => this.resetTimer()); - } - - private clearTimer(): void { - clearTimeout(this.timer); - this.timer = undefined; - } - - private resetTimer(): void { - if (this.timeout === 0) { - return; - } - - this.clearTimer(); - this.timer = setTimeout(() => this.handleTimeout(), this.timeout); - } - - private async handleTimeout(): Promise { - logger.warning('Soft reset timeout triggered'); - - try { - await this.zigbee.reset('soft'); - logger.warning('Soft reset ZNP due to timeout'); - } catch (error) { - logger.warning(`Soft reset failed, trying stop/start (${(error as Error).message})`); - - await this.zigbee.stop(); - logger.warning('Zigbee stopped'); - - try { - await this.zigbee.start(); - } catch (error) { - logger.error(`Failed to restart! (${(error as Error).message})`); - } - } - - this.resetTimer(); - } -} diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index e88e128d4c..426f20d456 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -38,9 +38,6 @@ interface Topology { * This extension creates a network map */ export default class NetworkMap extends Extension { - private legacyApi = settings.get().advanced.legacy_api; - private legacyTopic = `${settings.get().mqtt.base_topic}/bridge/networkmap`; - private legacyTopicRoutes = `${settings.get().mqtt.base_topic}/bridge/networkmap/routes`; private topic = `${settings.get().mqtt.base_topic}/bridge/request/networkmap`; private supportedFormats: {[s: string]: (topology: Topology) => KeyValue | string} = { raw: this.raw, @@ -53,17 +50,6 @@ export default class NetworkMap extends Extension { } @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - /* istanbul ignore else */ - if (this.legacyApi) { - if ((data.topic === this.legacyTopic || data.topic === this.legacyTopicRoutes) && this.supportedFormats[data.message] !== undefined) { - const includeRoutes = data.topic === this.legacyTopicRoutes; - const topology = await this.networkScan(includeRoutes); - let converted = this.supportedFormats[data.message](topology); - converted = data.message === 'raw' ? stringify(converted) : converted; - await this.mqtt.publish(`bridge/networkmap/${data.message}`, converted as string, {}); - } - } - if (data.topic === this.topic) { const message = utils.parseJSON(data.message, data.message); try { @@ -242,6 +228,7 @@ export default class NetworkMap extends Extension { logger.debug((error as Error).stack!); } + /* istanbul ignore else */ if (includeRoutes) { try { const result = await requestWithRetry(async () => await device.zh.routingTable()); @@ -329,6 +316,7 @@ export default class NetworkMap extends Extension { const routingTable = routingTables.get(device); + /* istanbul ignore else */ if (routingTable) { for (const entry of routingTable.table) { if (entry.status === 'ACTIVE' && entry.nextHop === neighbor.networkAddress) { diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 85587ab410..bc4184678b 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -28,8 +28,6 @@ function isValidUrl(url: string): boolean { type UpdateState = 'updating' | 'idle' | 'available'; interface UpdatePayload { - update_available?: boolean; - update: { progress?: number; remaining?: number; @@ -39,13 +37,11 @@ interface UpdatePayload { }; } -const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`); const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)`, 'i'); export default class OTAUpdate extends Extension { private inProgress = new Set(); private lastChecked: {[s: string]: number} = {}; - private legacyApi = settings.get().advanced.legacy_api; override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); @@ -80,8 +76,12 @@ export default class OTAUpdate extends Extension { } private removeProgressAndRemainingFromState(device: Device): void { - delete this.state.get(device).update?.progress; - delete this.state.get(device).update?.remaining; + const deviceState = this.state.get(device); + + if (deviceState.update) { + delete deviceState.update.progress; + delete deviceState.update.remaining; + } } @bind private async onZigbeeEvent(data: eventdata.DeviceMessage): Promise { @@ -110,18 +110,11 @@ export default class OTAUpdate extends Extension { logger.debug(`Failed to check if update available for '${data.device.name}' (${error})`); } - const payload = this.getEntityPublishPayload(data.device, availableResult ?? 'idle'); - await this.publishEntityState(data.device, payload); + await this.publishEntityState(data.device, this.getEntityPublishPayload(data.device, availableResult ?? 'idle')); if (availableResult?.available) { const message = `Update available for '${data.device.name}'`; logger.info(message); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: 'available', device: data.device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta})); - } } } @@ -166,24 +159,19 @@ export default class OTAUpdate extends Extension { }, }; - if (progress != undefined) { + if (progress !== undefined) { payload.update.progress = progress; } - if (remaining != undefined) { + if (remaining !== undefined) { payload.update.remaining = Math.round(remaining); } - /* istanbul ignore else */ - if (this.legacyApi) { - payload.update_available = typeof state === 'string' ? state === 'available' : state.available; - } - return payload; } @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - if ((!this.legacyApi || !data.topic.match(legacyTopicRegex)) && !data.topic.match(topicRegex)) { + if (!data.topic.match(topicRegex)) { return; } @@ -191,7 +179,7 @@ export default class OTAUpdate extends Extension { const ID = (typeof message === 'object' && message['id'] !== undefined ? message.id : message) as string; const device = this.zigbee.resolveEntity(ID); const type = data.topic.substring(data.topic.lastIndexOf('/') + 1); - const responseData: {id: string; updateAvailable?: boolean; from?: KeyValue | null; to?: KeyValue | null} = {id: ID}; + const responseData: {id: string; update_available?: boolean; from?: KeyValue | null; to?: KeyValue | null} = {id: ID}; let error: string | undefined; let errorStack: string | undefined; @@ -199,12 +187,6 @@ export default class OTAUpdate extends Extension { error = `Device '${ID}' does not exist`; } else if (!device.definition || !device.definition.ota) { error = `Device '${device.name}' does not support OTA updates`; - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `not_supported`, device: device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); - } } else if (this.inProgress.has(device.ieeeAddr)) { error = `Update or check for update already in progress for '${device.name}'`; } else { @@ -214,53 +196,25 @@ export default class OTAUpdate extends Extension { const msg = `Checking if update available for '${device.name}'`; logger.info(msg); - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `checking_if_available`, device: device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); - } - try { const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, undefined); const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`; logger.info(msg); - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = { - status: availableResult.available ? 'available' : 'not_available', - device: device.name, - }; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); - } - - const payload = this.getEntityPublishPayload(device, availableResult); - await this.publishEntityState(device, payload); + await this.publishEntityState(device, this.getEntityPublishPayload(device, availableResult)); this.lastChecked[device.ieeeAddr] = Date.now(); - responseData.updateAvailable = availableResult.available; + responseData.update_available = availableResult.available; } catch (e) { error = `Failed to check if update available for '${device.name}' (${(e as Error).message})`; errorStack = (e as Error).stack; - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `check_failed`, device: device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); - } } } else { // type === 'update' const msg = `Updating '${device.name}' to latest firmware`; logger.info(msg); - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_in_progress`, device: device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); - } - try { - const onProgress = async (progress: number, remaining: number): Promise => { + const onProgress = async (progress: number, remaining: number | null): Promise => { let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`; if (remaining) { msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`; @@ -268,26 +222,17 @@ export default class OTAUpdate extends Extension { logger.info(msg); - const payload = this.getEntityPublishPayload(device, 'updating', progress, remaining); - await this.publishEntityState(device, payload); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_progress`, device: device.name, progress}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: msg, meta})); - } + await this.publishEntityState(device, this.getEntityPublishPayload(device, 'updating', progress, remaining ?? undefined)); }; const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate'); const fileVersion = await device.definition.ota.updateToLatest(device.zh, onProgress); logger.info(`Finished update of '${device.name}'`); this.removeProgressAndRemainingFromState(device); - const payload = this.getEntityPublishPayload(device, { - available: false, - currentFileVersion: fileVersion, - otaFileVersion: fileVersion, - }); - await this.publishEntityState(device, payload); + await this.publishEntityState( + device, + this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}), + ); const to = await this.readSoftwareBuildIDAndDateCode(device); const [fromS, toS] = [stringify(from_), stringify(to)]; logger.info(`Device '${device.name}' was updated from '${fromS}' to '${toS}'`); @@ -299,38 +244,21 @@ export default class OTAUpdate extends Extension { */ this.eventBus.emitReconfigure({device}); this.eventBus.emitDevicesChanged(); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_succeeded`, device: device.name, from: from_, to}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message, meta})); - } } catch (e) { logger.debug(`Update of '${device.name}' failed (${e})`); error = `Update of '${device.name}' failed (${(e as Error).message})`; errorStack = (e as Error).stack; this.removeProgressAndRemainingFromState(device); - const payload = this.getEntityPublishPayload(device, 'available'); - await this.publishEntityState(device, payload); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_failed`, device: device.name}; - await this.mqtt.publish('bridge/log', stringify({type: `ota_update`, message: error, meta})); - } + await this.publishEntityState(device, this.getEntityPublishPayload(device, 'available')); } } this.inProgress.delete(device.ieeeAddr); } - const triggeredViaLegacyApi = data.topic.match(legacyTopicRegex); - - if (!triggeredViaLegacyApi) { - const response = utils.getResponse(message, responseData, error); - await this.mqtt.publish(`bridge/response/device/ota_update/${type}`, stringify(response)); - } + const response = utils.getResponse(message, responseData, error); + await this.mqtt.publish(`bridge/response/device/ota_update/${type}`, stringify(response)); if (error) { logger.error(error); diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index 925bc0bd76..56d535eaf6 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -1,11 +1,8 @@ -import assert from 'assert'; +import type * as zhc from 'zigbee-herdsman-converters'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; -import * as zhc from 'zigbee-herdsman-converters'; -import * as philips from 'zigbee-herdsman-converters/lib/philips'; - import Device from '../model/device'; import Group from '../model/group'; import logger from '../util/logger'; @@ -23,23 +20,6 @@ loadTopicGetSetRegex(); const STATE_VALUES: ReadonlyArray = ['on', 'off', 'toggle', 'open', 'close', 'stop', 'lock', 'unlock']; const SCENE_CONVERTER_KEYS: ReadonlyArray = ['scene_store', 'scene_add', 'scene_remove', 'scene_remove_all', 'scene_rename']; -// Legacy: don't provide default converters anymore, this is required by older z2m installs not saving group members -const DEFAULT_GROUP_CONVERTERS: ReadonlyArray = [ - zhc.toZigbee.light_onoff_brightness, - zhc.toZigbee.light_color_colortemp, - philips.tz.effect, // Support Hue effects for groups - zhc.toZigbee.ignore_transition, - zhc.toZigbee.cover_position_tilt, - zhc.toZigbee.thermostat_occupied_heating_setpoint, - zhc.toZigbee.tint_scene, - zhc.toZigbee.light_brightness_move, - zhc.toZigbee.light_brightness_step, - zhc.toZigbee.light_colortemp_step, - zhc.toZigbee.light_colortemp_move, - zhc.toZigbee.light_hue_saturation_move, - zhc.toZigbee.light_hue_saturation_step, -]; - interface ParsedTopic { ID: string; endpoint: string | undefined; @@ -96,36 +76,6 @@ export default class Publish extends Extension { } } - async legacyLog(payload: KeyValue): Promise { - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - await this.mqtt.publish('bridge/log', stringify(payload)); - } - } - - legacyRetrieveState( - re: Device | Group, - converter: zhc.Tz.Converter, - result: zhc.Tz.ConvertSetResult, - target: zh.Endpoint | zh.Group, - key: string, - meta: zhc.Tz.Meta, - ): void { - // It's possible for devices to get out of sync when writing an attribute that's not reportable. - // So here we re-read the value after a specified timeout, this timeout could for example be the - // transition time of a color change or for forcing a state read for devices that don't - // automatically report a new state when set. - // When reporting is requested for a device (report: true in device-specific settings) we won't - // ever issue a read here, as we assume the device will properly report changes. - // Only do this when the retrieve_state option is enabled for this device. - // retrieve_state == deprecated - if (re instanceof Device && result && result.readAfterWriteTime !== undefined && re.options.retrieve_state) { - const convertGet = converter.convertGet; - assert(convertGet !== undefined, 'Converter has `readAfterWriteTime` but no `convertGet`'); - setTimeout(() => convertGet(target, key, meta), result.readAfterWriteTime); - } - } - updateMessageHomeAssistant(message: KeyValue, entityState: KeyValue): void { /** * Home Assistant always publishes 'state', even when e.g. only setting @@ -154,7 +104,6 @@ export default class Publish extends Extension { const re = this.zigbee.resolveEntity(parsedTopic.ID); if (!re) { - await this.legacyLog({type: `entity_not_found`, message: {friendly_name: parsedTopic.ID}}); logger.error(`Entity '${parsedTopic.ID}' is unknown`); return; } @@ -194,14 +143,7 @@ export default class Publish extends Extension { re.zh.members.map((e) => [e.getDevice().ieeeAddr, this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr)!)]), ) : undefined; - let converters: ReadonlyArray; - - if (Array.isArray(definition)) { - const c = new Set(definition.map((d) => d.toZigbee).flat()); - converters = c.size === 0 ? DEFAULT_GROUP_CONVERTERS : Array.from(c); - } else { - converters = definition?.toZigbee; - } + const converters = this.getDefinitionConverters(definition); this.updateMessageHomeAssistant(message, entityState); @@ -329,8 +271,6 @@ export default class Publish extends Extension { addToToPublish(this.zigbee.resolveEntity(ieeeAddr)!, state); } } - - this.legacyRetrieveState(re, converter, result, localTarget, key, meta); } else if (parsedTopic.type === 'get' && converter.convertGet) { logger.debug(`Publishing get '${parsedTopic.type}' '${key}' to '${re.name}'`); await converter.convertGet(localTarget, key, meta); @@ -342,7 +282,6 @@ export default class Publish extends Extension { const message = `Publish '${parsedTopic.type}' '${key}' to '${re.name}' failed: '${error}'`; logger.error(message); logger.debug((error as Error).stack!); - await this.legacyLog({type: `zigbee_publish_error`, message, meta: {friendly_name: re.name}}); } usedConverters[endpointOrGroupID].push(converter); @@ -362,4 +301,12 @@ export default class Publish extends Extension { this.eventBus.emitScenesChanged({entity: re}); } } + + private getDefinitionConverters(definition: zhc.Definition | zhc.Definition[]): ReadonlyArray { + if (Array.isArray(definition)) { + return definition.length ? Array.from(new Set(definition.map((d) => d.toZigbee).flat())) : []; + } else { + return definition?.toZigbee; + } + } } diff --git a/lib/model/device.ts b/lib/model/device.ts index e995c9f391..2c8f124834 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -117,10 +117,6 @@ export default class Device { return names; } - isIkeaTradfri(): boolean { - return this.zh.manufacturerID === 4476; - } - isDevice(): this is Device { return true; } diff --git a/lib/mqtt.ts b/lib/mqtt.ts index fc9f46ef8c..3e6245634d 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -1,5 +1,3 @@ -import type {QoS} from 'mqtt-packet'; - import fs from 'fs'; import bind from 'bind-decorator'; @@ -35,7 +33,7 @@ export default class MQTT { const options: mqtt.IClientOptions = { will: { topic: `${settings.get().mqtt.base_topic}/bridge/state`, - payload: Buffer.from(utils.availabilityPayload('offline', settings.get())), + payload: Buffer.from(JSON.stringify({state: 'offline'})), retain: settings.get().mqtt.force_disable_retain ? false : true, qos: 1, }, @@ -84,7 +82,6 @@ export default class MQTT { this.client = mqtt.connect(mqttSettings.server, options); // https://github.com/Koenkk/zigbee2mqtt/issues/9822 this.client.stream.setMaxListeners(0); - this.eventBus.onPublishAvailability(this, this.publishStateOnline); this.client.on('connect', async () => { // Set timer at interval to check if connected to MQTT server. @@ -123,13 +120,13 @@ export default class MQTT { } @bind async publishStateOnline(): Promise { - await this.publish('bridge/state', utils.availabilityPayload('online', settings.get()), {retain: true, qos: 0}); + await this.publish('bridge/state', JSON.stringify({state: 'online'}), {retain: true, qos: 0}); } async disconnect(): Promise { clearTimeout(this.connectionTimer); clearTimeout(this.republishRetainedTimer); - await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()), {retain: true, qos: 0}); + await this.publish('bridge/state', JSON.stringify({state: 'offline'}), {retain: true, qos: 0}); this.eventBus.removeListeners(this); logger.info('Disconnecting from MQTT server'); this.client?.end(); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 8aa313c7ff..aee04cd7b7 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -5,7 +5,6 @@ import type TypeGroup from 'lib/model/group'; import type TypeMQTT from 'lib/mqtt'; import type TypeState from 'lib/state'; import type TypeZigbee from 'lib/zigbee'; -import type {QoS} from 'mqtt-packet'; import type * as zhc from 'zigbee-herdsman-converters'; import type { CoordinatorVersion as ZHCoordinatorVersion, @@ -31,6 +30,7 @@ declare global { type Device = TypeDevice; type State = TypeState; type Extension = TypeExtension; + type QoS = 0 | 1 | 2; // Types type ExternalDefinition = zhc.Definition & {homeassistant: unknown}; @@ -189,8 +189,6 @@ declare global { groups: {[s: string]: OptionalProps, 'devices'>}; device_options: KeyValue; advanced: { - legacy_api: boolean; - legacy_availability_payload: boolean; log_rotation: boolean; log_symlink_current: boolean; log_output: ('console' | 'file' | 'syslog')[]; @@ -215,14 +213,6 @@ declare global { timestamp_format: string; output: 'json' | 'attribute' | 'attribute_and_json'; transmit_power?: number; - // Everything below is deprecated - availability_timeout?: number; - availability_blocklist: string[]; - availability_passlist: string[]; - availability_blacklist: string[]; - availability_whitelist: string[]; - soft_reset_timeout: number; - report: boolean; }; } @@ -231,7 +221,6 @@ declare global { retention?: number; availability?: boolean | {timeout: number}; optimistic?: boolean; - retrieve_state?: boolean; debounce?: number; debounce_ignore?: string[]; throttle?: number; @@ -258,7 +247,6 @@ declare global { filtered_attributes?: string[]; filtered_cache?: string[]; filtered_optimistic?: string[]; - retrieve_state?: boolean; homeassistant?: KeyValue; friendly_name: string; description?: string; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index afe0dbf96e..defa569e0e 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -451,20 +451,6 @@ "type": "object", "title": "Advanced", "properties": { - "legacy_api": { - "type": "boolean", - "title": "Legacy API", - "requiresRestart": true, - "description": "Disables the legacy api (false = disable)", - "default": true - }, - "legacy_availability_payload": { - "type": "boolean", - "title": "Legacy availability payload", - "requiresRestart": true, - "description": "Payload to be used for device availability and bridge/state topics. true = text, false = JSON", - "default": true - }, "log_rotation": { "type": "boolean", "title": "Log rotation", @@ -765,21 +751,6 @@ "description": "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discovered", "default": true }, - "soft_reset_timeout": { - "type": "number", - "minimum": 0, - "requiresRestart": true, - "title": "Soft reset timeout (deprecated)", - "description": "Soft reset ZNP after timeout", - "readOnly": true - }, - "report": { - "type": "boolean", - "title": "Reporting", - "requiresRestart": true, - "readOnly": true, - "description": "Enables report feature (deprecated)" - }, "baudrate": { "type": "number", "title": "Baudrate (deprecated)", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 588fb98cf7..7ecdd7a469 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -19,8 +19,6 @@ objectAssignDeep(schema, schemaJson); delete schema.properties.advanced.properties.homeassistant_legacy_entity_attributes; delete schema.properties.advanced.properties.homeassistant_legacy_triggers; delete schema.properties.advanced.properties.homeassistant_status_topic; - delete schema.properties.advanced.properties.soft_reset_timeout; - delete schema.properties.advanced.properties.report; delete schema.properties.advanced.properties.baudrate; delete schema.properties.advanced.properties.rtscts; delete schema.properties.advanced.properties.ikea_ota_use_test_url; @@ -83,8 +81,6 @@ const defaults: RecursivePartial = { }, device_options: {}, advanced: { - legacy_api: true, - legacy_availability_payload: true, log_rotation: true, log_symlink_current: false, log_output: ['console', 'file'], @@ -108,13 +104,6 @@ const defaults: RecursivePartial = { network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], timestamp_format: 'YYYY-MM-DD HH:mm:ss', output: 'json', - // Everything below is deprecated - availability_blocklist: [], - availability_passlist: [], - availability_blacklist: [], - availability_whitelist: [], - soft_reset_timeout: 0, - report: false, }, }; @@ -166,7 +155,7 @@ function loadSettingsWithDefaults(): void { objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s); } - if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) { + if (_settingsWithDefaults.availability) { const defaults = {}; const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {}; // @ts-expect-error ignore typing @@ -367,19 +356,6 @@ export function validate(): string[] { } } - const checkAvailabilityList = (list: string[], type: string): void => { - list.forEach((e) => { - if (!getDevice(e)) { - errors.push(`Non-existing entity '${e}' specified in '${type}'`); - } - }); - }; - - checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist'); - checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist'); - checkAvailabilityList(settingsWithDefaults.advanced.availability_blocklist, 'availability_blocklist'); - checkAvailabilityList(settingsWithDefaults.advanced.availability_passlist, 'availability_passlist'); - return errors; } @@ -631,20 +607,6 @@ export function addDevice(ID: string): DeviceOptionsWithId { return getDevice(ID)!; // valid from creation above } -export function addDeviceToPasslist(ID: string): void { - const settings = getInternalSettings(); - if (!settings.passlist) { - settings.passlist = []; - } - - if (settings.passlist.includes(ID)) { - throw new Error(`Device '${ID}' already in passlist`); - } - - settings.passlist.push(ID); - write(); -} - export function blockDevice(ID: string): void { const settings = getInternalSettings(); if (!settings.blocklist) { diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 2748ec881a..bde76c8a69 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -77,10 +77,8 @@ async function getZigbee2MQTTVersion(includeCommitHash = true): Promise<{commitH } async function getDependencyVersion(depend: string): Promise<{version: string}> { - const modulePath = path.dirname(require.resolve(depend)); - const packageJSONPath = path.join(modulePath.slice(0, modulePath.indexOf(depend) + depend.length), 'package.json'); - const packageJSON = await import(packageJSONPath); - const version = packageJSON.version; + const packageJsonPath = require.resolve(`${depend}/package.json`); + const version = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version; return {version}; } @@ -306,23 +304,10 @@ function isAvailabilityEnabledForEntity(entity: Device | Group, settings: Settin return !!entity.options.availability; } - // availability_timeout = deprecated - if (!(settings.advanced.availability_timeout || settings.availability)) { + if (!settings.availability) { return false; } - const passlist = settings.advanced.availability_passlist.concat(settings.advanced.availability_whitelist); - - if (passlist.length > 0) { - return passlist.includes(entity.name) || passlist.includes(entity.ieeeAddr); - } - - const blocklist = settings.advanced.availability_blacklist.concat(settings.advanced.availability_blocklist); - - if (blocklist.length > 0) { - return !blocklist.includes(entity.name) && !blocklist.includes(entity.ieeeAddr); - } - return true; } @@ -342,10 +327,6 @@ function isZHGroup(obj: unknown): obj is zh.Group { return obj?.constructor.name.toLowerCase() === 'group'; } -function availabilityPayload(state: 'online' | 'offline', settings: Settings): string { - return settings.advanced.legacy_availability_payload ? state : JSON.stringify({state}); -} - const hours = (hours: number): number => 1000 * 60 * 60 * hours; const minutes = (minutes: number): number => 1000 * 60 * minutes; const seconds = (seconds: number): number => 1000 * seconds; @@ -460,7 +441,6 @@ export default { sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, - availabilityPayload, getAllFiles, filterProperties, flatten, diff --git a/lib/zigbee.ts b/lib/zigbee.ts index 4ead28e382..5786cdc713 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -219,10 +219,6 @@ export default class Zigbee { return await this.herdsman.getNetworkParameters(); } - async reset(type: 'soft' | 'hard'): Promise { - await this.herdsman.reset(type); - } - async stop(): Promise { logger.info('Stopping zigbee-herdsman...'); await this.herdsman.stop(); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index eb789a6412..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,8610 +0,0 @@ -{ - "name": "zigbee2mqtt", - "version": "1.42.0-dev", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "zigbee2mqtt", - "version": "1.42.0", - "license": "GPL-3.0", - "dependencies": { - "ajv": "^8.17.1", - "bind-decorator": "^1.0.11", - "debounce": "^2.2.0", - "express-static-gzip": "^2.2.0", - "fast-deep-equal": "^3.1.3", - "finalhandler": "^1.3.1", - "git-last-commit": "^1.0.1", - "humanize-duration": "^3.32.1", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "jszip": "^3.10.1", - "mkdir-recursive": "^0.4.0", - "moment": "^2.30.1", - "mqtt": "^5.10.2", - "object-assign-deep": "^0.4.0", - "rimraf": "^6.0.1", - "semver": "^7.6.3", - "source-map-support": "^0.5.21", - "throttleit": "^2.1.0", - "uri-js": "^4.4.1", - "winston": "^3.17.0", - "winston-syslog": "^2.7.1", - "winston-transport": "^4.9.0", - "ws": "^8.18.0", - "zigbee-herdsman": "2.1.9", - "zigbee-herdsman-converters": "20.58.0", - "zigbee2mqtt-frontend": "0.7.6" - }, - "bin": { - "zigbee2mqtt": "cli.js" - }, - "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-proposal-decorators": "^7.25.9", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@eslint/core": "^0.9.0", - "@eslint/js": "^9.15.0", - "@ianvs/prettier-plugin-sort-imports": "^4.4.0", - "@types/eslint__js": "^8.42.3", - "@types/finalhandler": "^1.2.3", - "@types/humanize-duration": "^3.27.4", - "@types/jest": "^29.5.14", - "@types/js-yaml": "^4.0.9", - "@types/node": "^22.9.3", - "@types/object-assign-deep": "^0.4.3", - "@types/readable-stream": "4.0.18", - "@types/sd-notify": "^2.8.2", - "@types/serve-static": "^1.15.7", - "@types/ws": "8.5.13", - "babel-jest": "^29.7.0", - "eslint": "^9.15.0", - "eslint-config-prettier": "^9.1.0", - "jest": "^29.7.0", - "prettier": "^3.3.3", - "tmp": "^0.2.3", - "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0" - }, - "engines": { - "node": "^18 || ^20 || ^22 || ^23" - }, - "optionalDependencies": { - "sd-notify": "^2.8.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz", - "integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-decorators": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz", - "integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-syntax-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", - "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@ianvs/prettier-plugin-sort-imports": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.4.0.tgz", - "integrity": "sha512-f4/e+/ANGk3tHuwRW0uh2YuBR50I4h1ZjGQ+5uD8sWfinHTivQsnieR5cz24t8M6Vx4rYvZ5v/IEKZhYpzQm9Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/generator": "^7.26.2", - "@babel/parser": "^7.26.2", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", - "semver": "^7.5.2" - }, - "peerDependencies": { - "@vue/compiler-sfc": "2.7.x || 3.x", - "prettier": "2 || 3" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@serialport/bindings-cpp": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz", - "integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@serialport/bindings-interface": "1.2.2", - "@serialport/parser-readline": "11.0.0", - "debug": "4.3.4", - "node-addon-api": "7.0.0", - "node-gyp-build": "4.6.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/bindings-cpp/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@serialport/bindings-cpp/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/@serialport/bindings-interface": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", - "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", - "license": "MIT", - "engines": { - "node": "^12.22 || ^14.13 || >=16" - } - }, - "node_modules/@serialport/parser-delimiter": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", - "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-readline": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz", - "integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==", - "license": "MIT", - "dependencies": { - "@serialport/parser-delimiter": "11.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/parser-readline/node_modules/@serialport/parser-delimiter": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz", - "integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/stream": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz", - "integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==", - "license": "MIT", - "dependencies": { - "@serialport/bindings-interface": "1.2.2", - "debug": "4.3.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/serialport/donate" - } - }, - "node_modules/@serialport/stream/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@serialport/stream/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/finalhandler": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/finalhandler/-/finalhandler-1.2.3.tgz", - "integrity": "sha512-I+Ba0JZEiuSr8LLjVmBhvLBEN8KG9GSITNXWwPCLeAvZj/k5pXEdOBEvnEEIgA038eeaauJ3BPxbuxeFBsqqUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/humanize-duration": { - "version": "3.27.4", - "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.4.tgz", - "integrity": "sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "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, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz", - "integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.8" - } - }, - "node_modules/@types/object-assign-deep": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@types/object-assign-deep/-/object-assign-deep-0.4.3.tgz", - "integrity": "sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/readable-stream": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", - "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "safe-buffer": "~5.1.1" - } - }, - "node_modules/@types/sd-notify": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@types/sd-notify/-/sd-notify-2.8.2.tgz", - "integrity": "sha512-LVWtuGvzso9z3N89NISzseq8RVHkEeg2h275370yQYx8/CoNaV2NnG17TTjDavy2FrmcUBFaR6OymlPQjqfb2g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", - "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/type-utils": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", - "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", - "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.15.0", - "@typescript-eslint/utils": "8.15.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.15.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bare-events": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", - "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bind-decorator": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/bind-decorator/-/bind-decorator-1.0.11.tgz", - "integrity": "sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==", - "license": "MIT" - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.16.tgz", - "integrity": "sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^4.2.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/bl/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001683", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz", - "integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commist": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", - "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debounce": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", - "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.64", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.64.tgz", - "integrity": "sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", - "@eslint/plugin-kit": "^0.2.3", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express-static-gzip": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.2.0.tgz", - "integrity": "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==", - "license": "MIT", - "dependencies": { - "parseurl": "^1.3.3", - "serve-static": "^1.16.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-unique-numbers": { - "version": "8.0.13", - "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", - "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.1.0" - } - }, - "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/git-last-commit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/git-last-commit/-/git-last-commit-1.0.1.tgz", - "integrity": "sha512-FDSgeMqa7GnJDxt/q0AbrxbfeTyxp4ImxEw1e4nw6NUHA5FMhFUq33dTXI4Xdgcj1VQ1q5QLWF6WxFrJ8KCBOg==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/glossy": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/glossy/-/glossy-0.1.7.tgz", - "integrity": "sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==", - "engines": { - "node": ">= 0.2.5" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/help-me": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/humanize-duration": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.32.1.tgz", - "integrity": "sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==", - "license": "Unlicense" - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "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, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mixin-deep": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", - "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mkdir-recursive": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/mkdir-recursive/-/mkdir-recursive-0.4.0.tgz", - "integrity": "sha512-gbTtiEu8P/GSMh1lAa0YYNr8XIfDzFgnWtetw3Hfz9nw6YXySHNYOZF/uUTgyp8GHvFnNw/EG7VhOkD6zfVb6A==", - "license": "GPL-3.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/mqtt": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.2.tgz", - "integrity": "sha512-Q8NrMXB6FwQ2DulGONeDb6BtFHxyQHmXWzDrSC724iyofxLleq/wuZmztV3kg1Kda9I7l0oHP+FKesowoFxyUg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.5", - "@types/ws": "^8.5.9", - "commist": "^3.2.0", - "concat-stream": "^2.0.0", - "debug": "^4.3.4", - "help-me": "^5.0.0", - "lru-cache": "^10.0.1", - "minimist": "^1.2.8", - "mqtt-packet": "^9.0.1", - "number-allocator": "^1.0.14", - "readable-stream": "^4.4.2", - "reinterval": "^1.1.0", - "rfdc": "^1.3.0", - "split2": "^4.2.0", - "worker-timers": "^7.1.4", - "ws": "^8.17.1" - }, - "bin": { - "mqtt": "build/bin/mqtt.js", - "mqtt_pub": "build/bin/pub.js", - "mqtt_sub": "build/bin/sub.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/mqtt-packet": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.1.tgz", - "integrity": "sha512-koZF1V/X2RZUI6uD9wN5OK1JxxcG1ofAR4H3LjCw1FkeKzruZQ26aAA6v2m1lZyWONZIR5wMMJFrZJDRNzbiQw==", - "license": "MIT", - "dependencies": { - "bl": "^6.0.8", - "debug": "^4.3.4", - "process-nextick-args": "^2.0.1" - } - }, - "node_modules/mqtt/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/mqtt/node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/mqtt/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/mqtt/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", - "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/number-allocator": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", - "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.1", - "js-sdsl": "4.3.0" - } - }, - "node_modules/object-assign-deep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-assign-deep/-/object-assign-deep-0.4.0.tgz", - "integrity": "sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", - "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/reinterval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", - "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sd-notify": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sd-notify/-/sd-notify-2.8.0.tgz", - "integrity": "sha512-e+D1v0Y6UzmqXcPlaTkHk1QMdqk36mF/jIYv5gwry/N2Tb8/UNnpfG6ktGLpeBOR6TCC5hPKgqA+0hTl9sm2tA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "linux", - "darwin", - "win32" - ], - "dependencies": { - "bindings": "1.5.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/slip/-/slip-1.0.2.tgz", - "integrity": "sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==", - "license": "(MIT OR GPL-2.0)" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamx": { - "version": "2.20.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", - "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-decoder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", - "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", - "license": "Apache-2.0" - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.15.0.tgz", - "integrity": "sha512-wY4FRGl0ZI+ZU4Jo/yjdBu0lVTSML58pu6PgGtJmCufvzfV565pUF6iACQt092uFOd49iLOTX/sEVmHtbSrS+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.15.0", - "@typescript-eslint/parser": "8.15.0", - "@typescript-eslint/utils": "8.15.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unix-dgram": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.6.tgz", - "integrity": "sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==", - "hasInstallScript": true, - "license": "ISC", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.16.0" - }, - "engines": { - "node": ">=0.10.48" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-syslog": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/winston-syslog/-/winston-syslog-2.7.1.tgz", - "integrity": "sha512-MrU5gSwXgEbHwz5wFhn1rZtSCbRcP1PEB8zU49tgsfjQ63EjIVAkQpXmMocdbTLnTJ2cW3gLW6gmoOOOhyJZgg==", - "license": "MIT", - "dependencies": { - "glossy": "^0.1.7", - "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" - }, - "engines": { - "node": ">= 8" - }, - "optionalDependencies": { - "unix-dgram": "2.0.6" - }, - "peerDependencies": { - "winston": "^3.8.2" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/worker-timers": { - "version": "7.1.8", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", - "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.5", - "tslib": "^2.6.2", - "worker-timers-broker": "^6.1.8", - "worker-timers-worker": "^7.0.71" - } - }, - "node_modules/worker-timers-broker": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", - "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.5", - "fast-unique-numbers": "^8.0.13", - "tslib": "^2.6.2", - "worker-timers-worker": "^7.0.71" - } - }, - "node_modules/worker-timers-worker": { - "version": "7.0.71", - "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", - "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.5", - "tslib": "^2.6.2" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zigbee-herdsman": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-2.1.9.tgz", - "integrity": "sha512-MXutgNp83F84sjhNfGPangv0uJD8zRtpRMjd7Xrr6hmKUgBwvdN/xQ/SfqpOarIqJaUNSO5T0Q2N13XPw2ukzQ==", - "license": "MIT", - "dependencies": { - "@serialport/bindings-cpp": "^12.0.1", - "@serialport/parser-delimiter": "^12.0.0", - "@serialport/stream": "^12.0.0", - "bonjour-service": "^1.2.1", - "debounce": "^2.2.0", - "fast-deep-equal": "^3.1.3", - "mixin-deep": "^2.0.1", - "slip": "^1.0.2" - } - }, - "node_modules/zigbee-herdsman-converters": { - "version": "20.58.0", - "resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-20.58.0.tgz", - "integrity": "sha512-TqBdR0W7PRO4M+s1IjcmakuIMdG3I+9daK/10kLw1oAwSW/JZEav094HgPn+a4AdMk2FW9m8LhCRtyKVJC4y8w==", - "license": "MIT", - "dependencies": { - "axios": "^1.7.7", - "buffer-crc32": "^1.0.0", - "https-proxy-agent": "^7.0.5", - "iconv-lite": "^0.6.3", - "semver": "^7.6.3", - "tar-stream": "^3.1.7", - "uri-js": "^4.4.1", - "zigbee-herdsman": "^2.1.9" - } - }, - "node_modules/zigbee2mqtt-frontend": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/zigbee2mqtt-frontend/-/zigbee2mqtt-frontend-0.7.6.tgz", - "integrity": "sha512-eBZk50vKKT0GCV6mDJlgVkPoNBEMUog3xpbG4L23NrfAAr3ysbVM217kOHOIrj8582WCrS2lapH8OukwPLaHIQ==", - "license": "GPL-3.0", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..f0d0bb94d2 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5968 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + ajv: + specifier: ^8.17.1 + version: 8.17.1 + bind-decorator: + specifier: ^1.0.11 + version: 1.0.11 + connect-gzip-static: + specifier: 3.0.1 + version: 3.0.1 + debounce: + specifier: ^2.1.1 + version: 2.1.1 + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 + finalhandler: + specifier: ^1.3.1 + version: 1.3.1 + git-last-commit: + specifier: ^1.0.1 + version: 1.0.1 + humanize-duration: + specifier: ^3.32.1 + version: 3.32.1 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + json-stable-stringify-without-jsonify: + specifier: ^1.0.1 + version: 1.0.1 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + mkdir-recursive: + specifier: ^0.4.0 + version: 0.4.0 + moment: + specifier: ^2.30.1 + version: 2.30.1 + mqtt: + specifier: ^5.10.1 + version: 5.10.1 + object-assign-deep: + specifier: ^0.4.0 + version: 0.4.0 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + semver: + specifier: ^7.6.3 + version: 7.6.3 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + throttleit: + specifier: ^2.1.0 + version: 2.1.0 + uri-js: + specifier: ^4.4.1 + version: 4.4.1 + winston: + specifier: ^3.15.0 + version: 3.15.0 + winston-syslog: + specifier: ^2.7.1 + version: 2.7.1(winston@3.15.0) + winston-transport: + specifier: ^4.8.0 + version: 4.8.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 + zigbee-herdsman: + specifier: 2.1.3 + version: 2.1.3 + zigbee-herdsman-converters: + specifier: 20.25.0 + version: 20.25.0 + zigbee2mqtt-frontend: + specifier: 0.7.4 + version: 0.7.4 + optionalDependencies: + sd-notify: + specifier: ^2.8.0 + version: 2.8.0 + devDependencies: + '@babel/core': + specifier: ^7.25.7 + version: 7.25.8 + '@babel/plugin-proposal-decorators': + specifier: ^7.25.7 + version: 7.25.7(@babel/core@7.25.8) + '@babel/preset-env': + specifier: ^7.25.7 + version: 7.25.8(@babel/core@7.25.8) + '@babel/preset-typescript': + specifier: ^7.25.7 + version: 7.25.7(@babel/core@7.25.8) + '@eslint/core': + specifier: ^0.6.0 + version: 0.6.0 + '@eslint/js': + specifier: ^9.12.0 + version: 9.12.0 + '@ianvs/prettier-plugin-sort-imports': + specifier: ^4.3.1 + version: 4.3.1(prettier@3.3.3) + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 + '@types/finalhandler': + specifier: ^1.2.3 + version: 1.2.3 + '@types/humanize-duration': + specifier: ^3.27.4 + version: 3.27.4 + '@types/jest': + specifier: ^29.5.13 + version: 29.5.13 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^22.7.4 + version: 22.7.5 + '@types/object-assign-deep': + specifier: ^0.4.3 + version: 0.4.3 + '@types/readable-stream': + specifier: 4.0.15 + version: 4.0.15 + '@types/sd-notify': + specifier: ^2.8.2 + version: 2.8.2 + '@types/ws': + specifier: 8.5.12 + version: 8.5.12 + babel-jest: + specifier: ^29.7.0 + version: 29.7.0(@babel/core@7.25.8) + eslint: + specifier: ^9.12.0 + version: 9.12.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.12.0) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.7.5) + prettier: + specifier: ^3.3.3 + version: 3.3.3 + tmp: + specifier: ^0.2.3 + version: 0.2.3 + typescript: + specifier: ^5.6.2 + version: 5.6.3 + typescript-eslint: + specifier: ^8.8.0 + version: 8.8.1(eslint@9.12.0)(typescript@5.6.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.25.7': + resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.25.8': + resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.25.8': + resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.7': + resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.7': + resolution: {integrity: sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': + resolution: {integrity: sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.7': + resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.25.7': + resolution: {integrity: sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.25.7': + resolution: {integrity: sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.2': + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-member-expression-to-functions@7.25.7': + resolution: {integrity: sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.7': + resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.25.7': + resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.25.7': + resolution: {integrity: sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.25.7': + resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.7': + resolution: {integrity: sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.25.7': + resolution: {integrity: sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-simple-access@7.25.7': + resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-skip-transparent-expression-wrappers@7.25.7': + resolution: {integrity: sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.7': + resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.7': + resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.7': + resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.25.7': + resolution: {integrity: sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.25.7': + resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.25.7': + resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.8': + resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7': + resolution: {integrity: sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7': + resolution: {integrity: sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7': + resolution: {integrity: sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7': + resolution: {integrity: sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7': + resolution: {integrity: sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-decorators@7.25.7': + resolution: {integrity: sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.25.7': + resolution: {integrity: sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.25.7': + resolution: {integrity: sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.25.7': + resolution: {integrity: sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.7': + resolution: {integrity: sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.7': + resolution: {integrity: sha512-rR+5FDjpCHqqZN2bzZm18bVYGaejGq5ZkpVCJLXor/+zlSrSoc4KWcHI0URVWjl/68Dyr1uwZUz/1njycEAv9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.25.7': + resolution: {integrity: sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.25.8': + resolution: {integrity: sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.25.7': + resolution: {integrity: sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.25.7': + resolution: {integrity: sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.25.7': + resolution: {integrity: sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.25.7': + resolution: {integrity: sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.25.8': + resolution: {integrity: sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.25.7': + resolution: {integrity: sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.25.7': + resolution: {integrity: sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.25.7': + resolution: {integrity: sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.25.7': + resolution: {integrity: sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.25.7': + resolution: {integrity: sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7': + resolution: {integrity: sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.25.8': + resolution: {integrity: sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.25.7': + resolution: {integrity: sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.25.8': + resolution: {integrity: sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.25.7': + resolution: {integrity: sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.25.7': + resolution: {integrity: sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.25.8': + resolution: {integrity: sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.25.7': + resolution: {integrity: sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.25.8': + resolution: {integrity: sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.25.7': + resolution: {integrity: sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.25.7': + resolution: {integrity: sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.25.7': + resolution: {integrity: sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.7': + resolution: {integrity: sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.25.7': + resolution: {integrity: sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.7': + resolution: {integrity: sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.7': + resolution: {integrity: sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.8': + resolution: {integrity: sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.25.8': + resolution: {integrity: sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.25.8': + resolution: {integrity: sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.25.7': + resolution: {integrity: sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.25.8': + resolution: {integrity: sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.25.8': + resolution: {integrity: sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.25.7': + resolution: {integrity: sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.7': + resolution: {integrity: sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.25.8': + resolution: {integrity: sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.25.7': + resolution: {integrity: sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.25.7': + resolution: {integrity: sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-reserved-words@7.25.7': + resolution: {integrity: sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.7': + resolution: {integrity: sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.7': + resolution: {integrity: sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.7': + resolution: {integrity: sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.7': + resolution: {integrity: sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.7': + resolution: {integrity: sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.25.7': + resolution: {integrity: sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.7': + resolution: {integrity: sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.7': + resolution: {integrity: sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.7': + resolution: {integrity: sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.7': + resolution: {integrity: sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.25.8': + resolution: {integrity: sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.25.7': + resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.25.7': + resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.7': + resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.7': + resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.8': + resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.18.0': + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.6.0': + resolution: {integrity: sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.12.0': + resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.0': + resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@folder/readdir@3.1.0': + resolution: {integrity: sha512-mqkVdQ77BcCOWur/dxoPxjJS2eJg8Eqqx1Dgdc/qbTeGI5UMPDLmT0peGEjxLdlnD9SvhMnpJuiyaYl2btdT6A==} + engines: {node: '>=10'} + + '@humanfs/core@0.19.0': + resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.5': + resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@ianvs/prettier-plugin-sort-imports@4.3.1': + resolution: {integrity: sha512-ZHwbyjkANZOjaBm3ZosADD2OUYGFzQGxfy67HmGZU94mHqe7g1LCMA7YYKB1Cq+UTPCBqlAYapY0KXAjKEw8Sg==} + peerDependencies: + '@vue/compiler-sfc': 2.7.x || 3.x + prettier: 2 || 3 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/eslint__js@8.42.3': + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/finalhandler@1.2.3': + resolution: {integrity: sha512-I+Ba0JZEiuSr8LLjVmBhvLBEN8KG9GSITNXWwPCLeAvZj/k5pXEdOBEvnEEIgA038eeaauJ3BPxbuxeFBsqqUw==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/humanize-duration@3.27.4': + resolution: {integrity: sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.13': + resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + + '@types/object-assign-deep@0.4.3': + resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==} + + '@types/readable-stream@4.0.15': + resolution: {integrity: sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==} + + '@types/sd-notify@2.8.2': + resolution: {integrity: sha512-LVWtuGvzso9z3N89NISzseq8RVHkEeg2h275370yQYx8/CoNaV2NnG17TTjDavy2FrmcUBFaR6OymlPQjqfb2g==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@8.8.1': + resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.8.1': + resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.8.1': + resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.8.1': + resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.8.1': + resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.8.1': + resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.8.1': + resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.8.1': + resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-polyfill-corejs2@0.4.11: + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.2: + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.5.0: + resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bind-decorator@1.0.11: + resolution: {integrity: sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@6.0.16: + resolution: {integrity: sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==} + + bonjour-service@1.2.1: + resolution: {integrity: sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.0: + resolution: {integrity: sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001668: + resolution: {integrity: sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commist@3.2.0: + resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + connect-gzip-static@3.0.1: + resolution: {integrity: sha512-VVqi1fJ4uZK8aIrb1BN9n7tsRAbc3BhkIo/mz1VuNhYC2KDuP1ZgCDapjYIfT3mcGgWzyUr04kv7nmnGOnCvWg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.38.1: + resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + debounce@2.1.1: + resolution: {integrity: sha512-+xRWxgel9LgTC4PwKlm7TJUK6B6qsEK77NaiNvXmeQ7Y3e6OVVsBC4a9BSptS/mAYceyAz37Oa8JTTuPRft7uQ==} + engines: {node: '>=18'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.36: + resolution: {integrity: sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@8.1.0: + resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.1.0: + resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.12.0: + resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.2.0: + resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-unique-numbers@8.0.13: + resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==} + engines: {node: '>=16.1.0'} + + fast-uri@3.0.2: + resolution: {integrity: sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + git-last-commit@1.0.1: + resolution: {integrity: sha512-FDSgeMqa7GnJDxt/q0AbrxbfeTyxp4ImxEw1e4nw6NUHA5FMhFUq33dTXI4Xdgcj1VQ1q5QLWF6WxFrJ8KCBOg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + glossy@0.1.7: + resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==} + engines: {node: '>= 0.2.5'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + humanize-duration@3.32.1: + resolution: {integrity: sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + js-sdsl@4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.0.1: + resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mixin-deep@2.0.1: + resolution: {integrity: sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==} + engines: {node: '>=6'} + + mkdir-recursive@0.4.0: + resolution: {integrity: sha512-gbTtiEu8P/GSMh1lAa0YYNr8XIfDzFgnWtetw3Hfz9nw6YXySHNYOZF/uUTgyp8GHvFnNw/EG7VhOkD6zfVb6A==} + engines: {node: '>=4'} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mqtt-packet@9.0.0: + resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==} + + mqtt@5.10.1: + resolution: {integrity: sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==} + engines: {node: '>=16.0.0'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + nan@2.22.0: + resolution: {integrity: sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + number-allocator@1.0.14: + resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==} + + object-assign-deep@0.4.0: + resolution: {integrity: sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==} + engines: {node: '>=6'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regexpu-core@6.1.1: + resolution: {integrity: sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.11.1: + resolution: {integrity: sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==} + hasBin: true + + reinterval@1.1.0: + resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@6.0.1: + resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + engines: {node: 20 || >=22} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sd-notify@2.8.0: + resolution: {integrity: sha512-e+D1v0Y6UzmqXcPlaTkHk1QMdqk36mF/jIYv5gwry/N2Tb8/UNnpfG6ktGLpeBOR6TCC5hPKgqA+0hTl9sm2tA==} + engines: {node: '>=8.0.0'} + os: [linux, darwin, win32] + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@0.19.1: + resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slip@1.0.2: + resolution: {integrity: sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + streamx@2.20.1: + resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-decoder@1.2.0: + resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.8.1: + resolution: {integrity: sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unix-dgram@2.0.6: + resolution: {integrity: sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==} + engines: {node: '>=0.10.48'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + winston-syslog@2.7.1: + resolution: {integrity: sha512-MrU5gSwXgEbHwz5wFhn1rZtSCbRcP1PEB8zU49tgsfjQ63EjIVAkQpXmMocdbTLnTJ2cW3gLW6gmoOOOhyJZgg==} + engines: {node: '>= 8'} + peerDependencies: + winston: ^3.8.2 + + winston-transport@4.8.0: + resolution: {integrity: sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==} + engines: {node: '>= 12.0.0'} + + winston@3.15.0: + resolution: {integrity: sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + worker-timers-broker@6.1.8: + resolution: {integrity: sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==} + + worker-timers-worker@7.0.71: + resolution: {integrity: sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==} + + worker-timers@7.1.8: + resolution: {integrity: sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zigbee-herdsman-converters@20.25.0: + resolution: {integrity: sha512-iqCQmcdwrUGz00IhvtB36l3YN9sCWPhbx8D+lf9owyjpE5To7u2pOn4GA4g6QWQCU7BndCif8fXed1FWt9/nrg==} + + zigbee-herdsman@2.1.3: + resolution: {integrity: sha512-1LiSb3L2ZFzNOuGJHMWBFPRgPs1WVMS4CagtvYxEVUdifhVivp1vMNrCxBsbwNhDiNBh/Blk0pTR0szJttLzrA==} + + zigbee2mqtt-frontend@0.7.4: + resolution: {integrity: sha512-skWNYxThSa6Ywn7aRB0ZvRKWifpqbku4+vUM5BbXiNaXYxCCbU0b3pN258Ahxt3NsLtYk2zBdYoQcXuBZxmJxw==} + engines: {node: '>=18'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.25.7': + dependencies: + '@babel/highlight': 7.25.7 + picocolors: 1.1.0 + + '@babel/compat-data@7.25.8': {} + + '@babel/core@7.25.8': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.25.7 + '@babel/generator': 7.25.7 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helpers': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/template': 7.25.7 + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.25.7': + dependencies: + '@babel/types': 7.25.8 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.25.7': + dependencies: + '@babel/types': 7.25.8 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-compilation-targets@7.25.7': + dependencies: + '@babel/compat-data': 7.25.8 + '@babel/helper-validator-option': 7.25.7 + browserslist: 4.24.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/helper-member-expression-to-functions': 7.25.7 + '@babel/helper-optimise-call-expression': 7.25.7 + '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/traverse': 7.25.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + regexpu-core: 6.1.1 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + debug: 4.3.7 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-imports': 7.25.7 + '@babel/helper-simple-access': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.7': + dependencies: + '@babel/types': 7.25.8 + + '@babel/helper-plugin-utils@7.25.7': {} + + '@babel/helper-remap-async-to-generator@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/helper-wrap-function': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-member-expression-to-functions': 7.25.7 + '@babel/helper-optimise-call-expression': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-simple-access@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.7': + dependencies: + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.7': {} + + '@babel/helper-validator-identifier@7.25.7': {} + + '@babel/helper-validator-option@7.25.7': {} + + '@babel/helper-wrap-function@7.25.7': + dependencies: + '@babel/template': 7.25.7 + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.25.7': + dependencies: + '@babel/template': 7.25.7 + '@babel/types': 7.25.8 + + '@babel/highlight@7.25.7': + dependencies: + '@babel/helper-validator-identifier': 7.25.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.0 + + '@babel/parser@7.25.8': + dependencies: + '@babel/types': 7.25.8 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + '@babel/plugin-syntax-decorators': 7.25.7(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-decorators@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-import-assertions@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-arrow-functions@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-async-generator-functions@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-imports': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-block-scoping@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-class-properties@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) + '@babel/traverse': 7.25.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/template': 7.25.7 + + '@babel/plugin-transform-destructuring@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-dotall-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-duplicate-keys@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-dynamic-import@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-exponentiation-operator@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-for-of@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-literals@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-logical-assignment-operators@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-member-expression-literals@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-modules-amd@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-simple-access': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 + '@babel/traverse': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-new-target@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-numeric-separator@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-object-rest-spread@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) + + '@babel/plugin-transform-object-super@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-optional-chaining@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-private-methods@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-regenerator@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-reserved-words@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-shorthand-properties@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-spread@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-template-literals@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-typeof-symbol@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-typescript@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-annotate-as-pure': 7.25.7 + '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-unicode-property-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-unicode-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/plugin-transform-unicode-sets-regex@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/helper-plugin-utils': 7.25.7 + + '@babel/preset-env@7.25.8(@babel/core@7.25.8)': + dependencies: + '@babel/compat-data': 7.25.8 + '@babel/core': 7.25.8 + '@babel/helper-compilation-targets': 7.25.7 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-validator-option': 7.25.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8) + '@babel/plugin-syntax-import-assertions': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.8) + '@babel/plugin-transform-arrow-functions': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-async-generator-functions': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-async-to-generator': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-block-scoped-functions': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-block-scoping': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-class-properties': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-class-static-block': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-classes': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-computed-properties': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-destructuring': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-dotall-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-duplicate-keys': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-dynamic-import': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-exponentiation-operator': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-export-namespace-from': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-for-of': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-function-name': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-json-strings': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-literals': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-logical-assignment-operators': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-member-expression-literals': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-modules-amd': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-modules-systemjs': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-modules-umd': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-new-target': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-numeric-separator': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-object-rest-spread': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-object-super': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-optional-catch-binding': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-private-methods': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-private-property-in-object': 7.25.8(@babel/core@7.25.8) + '@babel/plugin-transform-property-literals': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-regenerator': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-reserved-words': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-shorthand-properties': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-spread': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-sticky-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-template-literals': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-typeof-symbol': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-escapes': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-property-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-regex': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-sets-regex': 7.25.7(@babel/core@7.25.8) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.8) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.8) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.8) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.8) + core-js-compat: 3.38.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/types': 7.25.8 + esutils: 2.0.3 + + '@babel/preset-typescript@7.25.7(@babel/core@7.25.8)': + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-plugin-utils': 7.25.7 + '@babel/helper-validator-option': 7.25.7 + '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.25.7': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.7': + dependencies: + '@babel/code-frame': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/types': 7.25.8 + + '@babel/traverse@7.25.7': + dependencies: + '@babel/code-frame': 7.25.7 + '@babel/generator': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/template': 7.25.7 + '@babel/types': 7.25.8 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.25.8': + dependencies: + '@babel/helper-string-parser': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 + to-fast-properties: 2.0.0 + + '@bcoe/v8-coverage@0.2.3': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)': + dependencies: + eslint: 9.12.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.1': {} + + '@eslint/config-array@0.18.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.6.0': {} + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.2.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.12.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.0': + dependencies: + levn: 0.4.1 + + '@folder/readdir@3.1.0': {} + + '@humanfs/core@0.19.0': {} + + '@humanfs/node@0.16.5': + dependencies: + '@humanfs/core': 0.19.0 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.3.3)': + dependencies: + '@babel/core': 7.25.8 + '@babel/generator': 7.25.7 + '@babel/parser': 7.25.8 + '@babel/traverse': 7.25.7 + '@babel/types': 7.25.8 + prettier: 3.3.3 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.7.5) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.7.5 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 22.7.5 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.25.8 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.7.5 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.25.8 + '@babel/types': 7.25.8 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.25.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.25.8 + '@babel/types': 7.25.8 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.25.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/eslint__js@8.42.3': + dependencies: + '@types/eslint': 9.6.1 + + '@types/estree@1.0.6': {} + + '@types/finalhandler@1.2.3': + dependencies: + '@types/node': 22.7.5 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.7.5 + + '@types/humanize-duration@3.27.4': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.13': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@types/object-assign-deep@0.4.3': {} + + '@types/readable-stream@4.0.15': + dependencies: + '@types/node': 22.7.5 + safe-buffer: 5.1.2 + + '@types/sd-notify@2.8.2': {} + + '@types/stack-utils@2.0.3': {} + + '@types/triple-beam@1.3.5': {} + + '@types/ws@8.5.12': + dependencies: + '@types/node': 22.7.5 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 + eslint: 9.12.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 + debug: 4.3.7 + eslint: 9.12.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.8.1': + dependencies: + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 + + '@typescript-eslint/type-utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + debug: 4.3.7 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.8.1': {} + + '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + eslint: 9.12.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.8.1': + dependencies: + '@typescript-eslint/types': 8.8.1 + eslint-visitor-keys: 3.4.3 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + agent-base@7.1.1: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b4a@1.6.7: {} + + babel-jest@29.7.0(@babel/core@7.25.8): + dependencies: + '@babel/core': 7.25.8 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.25.8) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.25.7 + '@babel/types': 7.25.8 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.25.8): + dependencies: + '@babel/compat-data': 7.25.8 + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.8): + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + core-js-compat: 3.38.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.25.8): + dependencies: + '@babel/core': 7.25.8 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + transitivePeerDependencies: + - supports-color + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.8): + dependencies: + '@babel/core': 7.25.8 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.8) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.8) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.8) + '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.8) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.8) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.8) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.8) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.8) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.8) + + babel-preset-jest@29.6.3(@babel/core@7.25.8): + dependencies: + '@babel/core': 7.25.8 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + + balanced-match@1.0.2: {} + + bare-events@2.5.0: + optional: true + + base64-js@1.5.1: {} + + bind-decorator@1.0.11: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + bl@6.0.16: + dependencies: + '@types/readable-stream': 4.0.15 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.5.2 + + bonjour-service@1.2.1: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.0: + dependencies: + caniuse-lite: 1.0.30001668 + electron-to-chromium: 1.5.36 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.0) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001668: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commist@3.2.0: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + connect-gzip-static@3.0.1: + dependencies: + '@folder/readdir': 3.1.0 + debug: 4.3.7 + parseurl: 1.3.3 + send: 0.19.1 + serve-static: 1.16.2 + transitivePeerDependencies: + - supports-color + + convert-source-map@2.0.0: {} + + core-js-compat@3.38.1: + dependencies: + browserslist: 4.24.0 + + core-util-is@1.0.3: {} + + create-jest@29.7.0(@types/node@22.7.5): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.7.5) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debounce@2.1.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + dedent@1.5.3: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.36: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@9.1.0(eslint@9.12.0): + dependencies: + eslint: 9.12.0 + + eslint-scope@8.1.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.1.0: {} + + eslint@9.12.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) + '@eslint-community/regexpp': 4.11.1 + '@eslint/config-array': 0.18.0 + '@eslint/core': 0.6.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.12.0 + '@eslint/plugin-kit': 0.2.0 + '@humanfs/node': 0.16.5 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.1.0 + eslint-visitor-keys: 4.1.0 + espree: 10.2.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.2.0: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.1.0 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-unique-numbers@8.0.13: + dependencies: + '@babel/runtime': 7.25.7 + tslib: 2.7.0 + + fast-uri@3.0.2: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fecha@4.2.3: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-uri-to-path@1.0.0: + optional: true + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + fn.name@1.1.0: {} + + follow-redirects@1.15.9: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fresh@0.5.2: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-package-type@0.1.0: {} + + get-stream@6.0.1: {} + + git-last-commit@1.0.1: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.0.0: + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + glossy@0.1.7: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + html-escaper@2.0.2: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + humanize-duration@3.32.1: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + immediate@3.0.6: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@2.0.1: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.25.8 + '@babel/parser': 7.25.8 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.25.8 + '@babel/parser': 7.25.8 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@4.0.2: + dependencies: + '@isaacs/cliui': 8.0.2 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.7.5): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.7.5) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.7.5) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.7.5): + dependencies: + '@babel/core': 7.25.8 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.25.8) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.7.5 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.7.5 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.25.7 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.25.8 + '@babel/generator': 7.25.7 + '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.8) + '@babel/types': 7.25.8 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.7.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.7.5 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.7.5): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.7.5) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + js-sdsl@4.3.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kuler@2.0.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.debounce@4.0.8: {} + + lodash.merge@4.6.2: {} + + logform@2.6.1: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + lru-cache@10.4.3: {} + + lru-cache@11.0.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mixin-deep@2.0.1: {} + + mkdir-recursive@0.4.0: {} + + moment@2.30.1: {} + + mqtt-packet@9.0.0: + dependencies: + bl: 6.0.16 + debug: 4.3.7 + process-nextick-args: 2.0.1 + transitivePeerDependencies: + - supports-color + + mqtt@5.10.1: + dependencies: + '@types/readable-stream': 4.0.15 + '@types/ws': 8.5.12 + commist: 3.2.0 + concat-stream: 2.0.0 + debug: 4.3.7 + help-me: 5.0.0 + lru-cache: 10.4.3 + minimist: 1.2.8 + mqtt-packet: 9.0.0 + number-allocator: 1.0.14 + readable-stream: 4.5.2 + reinterval: 1.1.0 + rfdc: 1.4.1 + split2: 4.2.0 + worker-timers: 7.1.8 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + nan@2.22.0: + optional: true + + natural-compare@1.4.0: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + node-int64@0.4.0: {} + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + number-allocator@1.0.14: + dependencies: + debug: 4.3.7 + js-sdsl: 4.3.0 + transitivePeerDependencies: + - supports-color + + object-assign-deep@0.4.0: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.25.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.1 + minipass: 7.1.2 + + picocolors@1.1.0: {} + + picomatch@2.3.1: {} + + pirates@4.0.6: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + prelude-ls@1.2.1: {} + + prettier@3.3.3: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + queue-microtask@1.2.3: {} + + queue-tick@1.0.1: {} + + range-parser@1.2.1: {} + + react-is@18.3.1: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.14.1: {} + + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.25.7 + + regexpu-core@6.1.1: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.11.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.11.1: + dependencies: + jsesc: 3.0.2 + + reinterval@1.1.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + rimraf@6.0.1: + dependencies: + glob: 11.0.0 + package-json-from-dist: 1.0.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sd-notify@2.8.0: + dependencies: + bindings: 1.5.0 + optional: true + + semver@6.3.1: {} + + semver@7.6.3: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@0.19.1: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slip@1.0.2: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@2.0.1: {} + + streamx@2.20.1: + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + text-decoder: 1.2.0 + optionalDependencies: + bare-events: 2.5.0 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.20.1 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-decoder@1.2.0: + dependencies: + b4a: 1.6.7 + + text-hex@1.0.0: {} + + text-table@0.2.0: {} + + throttleit@2.1.0: {} + + thunky@1.1.0: {} + + tmp@0.2.3: {} + + tmpl@1.0.5: {} + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + triple-beam@1.4.1: {} + + ts-api-utils@1.3.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + tslib@2.7.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + typedarray@0.0.6: {} + + typescript-eslint@8.8.1(eslint@9.12.0)(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.6.3: {} + + undici-types@6.19.8: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unix-dgram@2.0.6: + dependencies: + bindings: 1.5.0 + nan: 2.22.0 + optional: true + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.1(browserslist@4.24.0): + dependencies: + browserslist: 4.24.0 + escalade: 3.2.0 + picocolors: 1.1.0 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + winston-syslog@2.7.1(winston@3.15.0): + dependencies: + glossy: 0.1.7 + triple-beam: 1.4.1 + winston: 3.15.0 + winston-transport: 4.8.0 + optionalDependencies: + unix-dgram: 2.0.6 + + winston-transport@4.8.0: + dependencies: + logform: 2.6.1 + readable-stream: 4.5.2 + triple-beam: 1.4.1 + + winston@3.15.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.6.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.8.0 + + word-wrap@1.2.5: {} + + worker-timers-broker@6.1.8: + dependencies: + '@babel/runtime': 7.25.7 + fast-unique-numbers: 8.0.13 + tslib: 2.7.0 + worker-timers-worker: 7.0.71 + + worker-timers-worker@7.0.71: + dependencies: + '@babel/runtime': 7.25.7 + tslib: 2.7.0 + + worker-timers@7.1.8: + dependencies: + '@babel/runtime': 7.25.7 + tslib: 2.7.0 + worker-timers-broker: 6.1.8 + worker-timers-worker: 7.0.71 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@8.18.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zigbee-herdsman-converters@20.25.0: + dependencies: + axios: 1.7.7 + buffer-crc32: 1.0.0 + https-proxy-agent: 7.0.5 + iconv-lite: 0.6.3 + semver: 7.6.3 + tar-stream: 3.1.7 + zigbee-herdsman: 2.1.3 + transitivePeerDependencies: + - debug + - supports-color + + zigbee-herdsman@2.1.3: + dependencies: + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/stream': 12.0.0 + bonjour-service: 1.2.1 + debounce: 2.1.1 + fast-deep-equal: 3.1.3 + mixin-deep: 2.0.1 + slip: 1.0.2 + transitivePeerDependencies: + - supports-color + + zigbee2mqtt-frontend@0.7.4: {} diff --git a/scripts/install.sh b/scripts/install.sh deleted file mode 100644 index 38a4c1eac5..0000000000 --- a/scripts/install.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -function zigbee2mqtt-show-short-info { - echo "Setup for Zigbee2MQTT bridge." -} - -function zigbee2mqtt-show-long-info { - echo "This script installs the Zigbee2MQTT bridge" -} - -function zigbee2mqtt-show-copyright-info { - echo "Original concept by Landrash ." -} - -function zigbee2mqtt-install-package { -echo -n "Installing dependencies : " -node=$(which npm) -if [ -z "${node}" ]; then #Installing NodeJS if not already installed. - printf "Downloading and installing NodeJS...\\n" - curl -sL https://deb.nodesource.com/setup_10.x | bash - - apt install -y nodejs -fi - -echo "Cloning Zigbee2MQTT git repository" -git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt -chown -R pi:pi /opt/zigbee2mqtt - -echo "Running install. This might take a while and can produce some expected errors" -cd /opt/zigbee2mqtt || exit -su pi -c "npm ci" - -echo "Creating service file zigbee2mqtt.service" -service_path="/etc/systemd/system/zigbee2mqtt.service" - -echo "[Unit] -Description=zigbee2mqtt -After=network.target - -[Service] -Type=notify -ExecStart=/usr/bin/node index.js -WorkingDirectory=/opt/zigbee2mqtt -StandardOutput=inherit -StandardError=inherit -WatchdogSec=10s -Restart=always -User=pi - -[Install] -WantedBy=multi-user.target" > $service_path - -echo "Checking the installation..." -if [ ! -f /opt/zigbee2mqtt/data/configuration.yaml ]; then - validation="" -else - validation="ok" -fi - -if [ ! -z "${validation}" ]; then - echo - echo -e "\\e[32mInstallation done..\\e[0m" - echo -e "Update of configuration.yaml is required found at /opt/zigbee2mqtt/data/" - echo -e "Some further configuration is required and details can be found here https://www.zigbee2mqtt.io" - echo - echo -e "Service can be started after configuration but running sudo systemctl start zigbee2mqtt" - echo -else - echo - echo -e "\\e[31mInstallation failed..." - echo - return 1 -fi -return 0 -} - -[[ "$_" == "$0" ]] && zigbee2mqtt-install-package diff --git a/test/availability.test.js b/test/availability.test.js index 118f1badcc..a48c6e3009 100644 --- a/test/availability.test.js +++ b/test/availability.test.js @@ -68,11 +68,21 @@ describe('Availability', () => { }); it('Should publish availability on startup for device where it is enabled for', async () => { - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + stringify({state: 'online'}), + {retain: true, qos: 1}, + expect.any(Function), + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote/availability', + stringify({state: 'online'}), + {retain: true, qos: 1}, + expect.any(Function), + ); expect(MQTT.publish).not.toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color_2/availability', - 'online', + stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); @@ -109,7 +119,12 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(7)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Shouldnt do anything for a device when availability: false is set for device', async () => { @@ -123,7 +138,12 @@ describe('Availability', () => { MQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Should reset ping timer when device last seen changes for active device', async () => { @@ -133,7 +153,12 @@ describe('Availability', () => { expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); await setTimeAndAdvanceTimers(utils.minutes(7)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); @@ -165,7 +190,12 @@ describe('Availability', () => { expect(devices.remote.ping).toHaveBeenCalledTimes(0); await zigbeeHerdsman.events.lastSeenChanged({device: devices.remote}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); await setTimeAndAdvanceTimers(utils.hours(25)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); @@ -178,12 +208,22 @@ describe('Availability', () => { MQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(15)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); devices.bulb_color.lastSeen = Date.now(); await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + stringify({state: 'online'}), + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Should allow to change availability timeout via device options', async () => { @@ -299,13 +339,22 @@ describe('Availability', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_new_name/availability', + stringify({state: 'online'}), + {retain: true, qos: 1}, + expect.any(Function), + ); await setTimeAndAdvanceTimers(utils.hours(12)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_new_name/availability', + stringify({state: 'offline'}), + {retain: true, qos: 1}, + expect.any(Function), + ); }); it('Should publish availability payload in JSON format', async () => { - settings.set(['advanced', 'legacy_availability_payload'], false); await resetExtension(); devices.remote.ping.mockClear(); MQTT.publish.mockClear(); @@ -319,37 +368,6 @@ describe('Availability', () => { ); }); - it('Deprecated - should allow to block via advanced.availability_blocklist', async () => { - settings.set(['advanced', 'availability_blocklist'], [devices.bulb_color.ieeeAddr]); - await resetExtension(); - devices.bulb_color.ping.mockClear(); - - await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - }); - - it('Deprecated - should allow to pass certain devices via availability_passlist', async () => { - settings.set(['advanced', 'availability_passlist'], [devices.bulb_color_2.ieeeAddr]); - settings.changeEntityOptions(devices.bulb_color_2.ieeeAddr, {availability: null}); - await resetExtension(); - devices.bulb_color.ping.mockClear(); - devices.bulb_color_2.ping.mockClear(); - - await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(1); - }); - - it('Deprecated - should allow to enable via availability_timeout', async () => { - settings.set(['availability'], false); - settings.set(['advanced', 'availability_timeout'], 60); - await resetExtension(); - devices.bulb_color.ping.mockClear(); - - await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); - }); - it('Should publish availability for groups', async () => { settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], true); await resetExtension(); @@ -357,7 +375,7 @@ describe('Availability', () => { expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', - 'online', + stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); @@ -365,7 +383,7 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(12)); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', - 'offline', + stringify({state: 'offline'}), {retain: true, qos: 1}, expect.any(Function), ); @@ -375,7 +393,7 @@ describe('Availability', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', - 'online', + stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); diff --git a/test/bind.test.js b/test/bind.test.js index 35924b6826..9a65d41514 100644 --- a/test/bind.test.js +++ b/test/bind.test.js @@ -366,9 +366,9 @@ describe('Bind', () => { expect.any(Function), ); - // Should configure reproting for device added to group + // Should configure reporting for device added to group target1Member.configureReporting.mockClear(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb'); + await MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'})); await flushPromises(); expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ @@ -648,222 +648,6 @@ describe('Bind', () => { ); }); - it('Legacy api: Should bind', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}, - }); - }); - - it('Legacy api: Should log error when there is nothing to bind', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - mockClear(device); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'button'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(logger.error).toHaveBeenCalledWith(`Nothing to bind from 'remote' to 'button'`); - }); - - it('Legacy api: Should unbind', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'bulb_color'); - await flushPromises(); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}, - }); - }); - - it('Legacy api: Should unbind coordinator', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const endpoint = device.getEndpoint(1); - mockClear(device); - endpoint.unbind.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'Coordinator'); - await flushPromises(); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'Coordinator', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'Coordinator', cluster: 'genLevelCtrl'}, - }); - }); - - it('Legacy api: Should bind to groups', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const endpoint = device.getEndpoint(1); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'group_1'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}, - }); - }); - - it('Legacy api: Should bind to group by number', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const endpoint = device.getEndpoint(1); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote', '1'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_bind', - message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}, - }); - }); - - it('Legacy api: Should log when bind fails', async () => { - logger.error.mockClear(); - const device = zigbeeHerdsman.devices.remote; - const endpoint = device.getEndpoint(1); - mockClear(device); - endpoint.bind.mockImplementationOnce(() => { - throw new Error('failed'); - }); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Failed to bind cluster 'genScenes' from 'remote' to 'bulb_color' (Error: failed)"); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - }); - - it('Legacy api: Should bind from non default endpoints', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG03LM.getEndpoint(3); - const endpoint = device.getEndpoint(2); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch_double/right'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - }); - - it('Legacy api: Should bind to default endpoint returned by endpoints()', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG04LM.getEndpoint(2); - const endpoint = device.getEndpoint(2); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch'); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - }); - - it('Legacy api: Should unbind from default_bind_group', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = 'default_bind_group'; - const endpoint = device.getEndpoint(1); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', target); - await flushPromises(); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901); - expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901); - expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}, - }); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'}, - }); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({ - type: 'device_unbind', - message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'}, - }); - }); - it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { const remote = zigbeeHerdsman.devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; diff --git a/test/bridge.test.js b/test/bridge.test.js index b26d79b56c..9f9fc8c431 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -49,7 +49,6 @@ describe('Bridge', () => { beforeAll(async () => { jest.useFakeTimers(); mockRestart = jest.fn(); - settings.set(['advanced', 'legacy_api'], false); controller = new Controller(mockRestart, jest.fn()); await controller.start(); await flushPromises(); @@ -60,7 +59,6 @@ describe('Bridge', () => { MQTT.mock.reconnecting = false; data.writeDefaultConfiguration(); settings.reRead(); - settings.set(['advanced', 'legacy_api'], false); data.writeDefaultState(); logger.info.mockClear(); logger.warning.mockClear(); @@ -94,10 +92,6 @@ describe('Bridge', () => { advanced: { adapter_concurrent: undefined, adapter_delay: undefined, - availability_blacklist: [], - availability_blocklist: [], - availability_passlist: [], - availability_whitelist: [], cache_state: true, cache_state_persistent: true, cache_state_send_on_startup: true, @@ -105,8 +99,6 @@ describe('Bridge', () => { elapsed: false, ext_pan_id: [221, 221, 221, 221, 221, 221, 221, 221], last_seen: 'disable', - legacy_api: false, - legacy_availability_payload: true, log_debug_namespace_ignore: '', log_debug_to_mqtt_frontend: false, log_directory: directory, @@ -119,8 +111,6 @@ describe('Bridge', () => { log_syslog: {}, output: 'json', pan_id: 6754, - report: false, - soft_reset_timeout: 0, timestamp_format: 'YYYY-MM-DD HH:mm:ss', }, blocklist: [], @@ -2731,11 +2721,11 @@ describe('Bridge', () => { it('Should put error in response when format is incorrect', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', stringify({value_not_good: false})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: false})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({data: {}, status: 'error', error: 'No value given'}), + 'zigbee2mqtt/bridge/response/options', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -2914,16 +2904,6 @@ describe('Bridge', () => { expect(settings.get().blocklist).toStrictEqual(['0x123']); }); - it('Should allow to add and remove from availabliltiy blocklist', async () => { - expect(settings.get().blocklist).toStrictEqual([]); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123', '0x1234']}}})); - await flushPromises(); - expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123', '0x1234']); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {availability_blocklist: ['0x123']}}})); - await flushPromises(); - expect(settings.get().advanced.availability_blocklist).toStrictEqual(['0x123']); - }); - it('Should throw error on removing non-existing device', async () => { const device = zigbeeHerdsman.devices.bulb; MQTT.publish.mockClear(); @@ -3426,148 +3406,6 @@ describe('Bridge', () => { ); }); - it('Should allow to enable/disable Home Assistant extension', async () => { - // Test if disabled initially - const device = zigbeeHerdsman.devices.WXKG11LM; - settings.set(['devices', device.ieeeAddr, 'legacy'], false); - const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - expect(settings.get().homeassistant).toBeFalsy(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - - // Disable when already disabled should go OK - MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: false})); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({data: {value: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - expect(settings.get().homeassistant).toBeFalsy(); - - // Enable - MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: true})); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({data: {value: true}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - expect(settings.get().homeassistant).toBeTruthy(); - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - - // Disable - MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', stringify({value: false})); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({data: {value: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - expect(settings.get().homeassistant).toBeFalsy(); - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - }); - - it('Should fail to set Home Assistant when invalid type', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', 'invalid_one'); - await flushPromises(); - expect(settings.get().homeassistant).toBeFalsy(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/homeassistant', - stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: true,false"}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should allow to set last_seen', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', 'ISO_8601'); - await flushPromises(); - expect(settings.get().advanced.last_seen).toBe('ISO_8601'); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({data: {value: 'ISO_8601'}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should fail to set last_seen when invalid type', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', 'invalid_one'); - await flushPromises(); - expect(settings.get().advanced.last_seen).toBe('disable'); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/last_seen', - stringify({data: {}, status: 'error', error: "'invalid_one' is not an allowed value, allowed: disable,ISO_8601,epoch,ISO_8601_local"}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should allow to set elapsed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/elapsed', 'true'); - await flushPromises(); - expect(settings.get().advanced.elapsed).toBe(true); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/elapsed', - stringify({data: {value: true}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should fail to set last_seen when invalid type', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/elapsed', 'not_valid'); - await flushPromises(); - expect(settings.get().advanced.elapsed).toBe(false); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/elapsed', - stringify({data: {}, status: 'error', error: "'not_valid' is not an allowed value, allowed: true,false"}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should allow to set log level', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/log_level', 'debug'); - await flushPromises(); - expect(logger.getLevel()).toBe('debug'); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/log_level', - stringify({data: {value: 'debug'}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should fail to set log level when invalid type', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/config/log_level', 'not_valid'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/config/log_level', - stringify({data: {}, status: 'error', error: `'not_valid' is not an allowed value, allowed: ${settings.LOG_LEVELS.join(',')}`}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - it('Should allow to touchlink factory reset (succeeds)', async () => { MQTT.publish.mockClear(); zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); diff --git a/test/configure.test.js b/test/configure.test.js index da7a0e2539..d5a700deb3 100644 --- a/test/configure.test.js +++ b/test/configure.test.js @@ -191,26 +191,6 @@ describe('Configure', () => { ); }); - it('Legacy api: Should allow to reconfigure manually', async () => { - mockClear(zigbeeHerdsman.devices.remote); - expectRemoteNotConfigured(); - await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote'); - await flushPromises(); - expectRemoteConfigured(); - }); - - it('Legacy api: Shouldnt manually reconfigure when device does not exist', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote_random_non_existing'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Device 'remote_random_non_existing' does not exist`); - }); - - it('Legacy api: Should skip reconfigure when device does not require this', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/configure', '0x0017882104a44559'); - await flushPromises(); - expect(logger.warning).toHaveBeenCalledWith(`Skipping configure of 'TS0601_thermostat', device does not require this.`); - }); - it('Should not configure when interview not completed', async () => { const device = zigbeeHerdsman.devices.remote; delete device.meta.configured; diff --git a/test/controller.test.js b/test/controller.test.js index b71723c37d..939daf82d3 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -94,7 +94,7 @@ describe('Controller', () => { expect(logger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); expect(MQTT.connect).toHaveBeenCalledTimes(1); expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', { - will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', @@ -140,7 +140,7 @@ describe('Controller', () => { await flushPromises(); expect(MQTT.connect).toHaveBeenCalledTimes(1); const expected = { - will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, keepalive: 30, ca: Buffer.from([99, 97]), key: Buffer.from([107, 101, 121]), @@ -403,8 +403,8 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceJoined(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -466,8 +466,8 @@ describe('Controller', () => { zigbeeHerdsman.events.deviceJoined(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -480,8 +480,8 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'pairing', message: 'interview_started', meta: {friendly_name: 'bulb'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -494,8 +494,8 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'pairing', message: 'interview_failed', meta: {friendly_name: 'bulb'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -503,40 +503,46 @@ describe('Controller', () => { it('On zigbee deviceInterview successful supported', async () => { await controller.start(); + MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.bulb; const payload = {device, status: 'successful'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({ - type: 'pairing', - message: 'interview_successful', - meta: { - friendly_name: 'bulb', - model: 'LED1545G12', - vendor: 'IKEA', - description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', - supported: true, - }, - }), - {retain: false, qos: 0}, - expect.any(Function), - ); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(parsedMessage.type).toStrictEqual('device_interview'); + expect(parsedMessage.data.friendly_name).toStrictEqual('bulb'); + expect(parsedMessage.data.status).toStrictEqual('successful'); + expect(parsedMessage.data.ieee_address).toStrictEqual(device.ieeeAddr); + expect(parsedMessage.data.supported).toStrictEqual(true); + expect(parsedMessage.data.definition.model).toStrictEqual('LED1545G12'); + expect(parsedMessage.data.definition.vendor).toStrictEqual('IKEA'); + expect(parsedMessage.data.definition.description).toStrictEqual('TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm'); + expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); + expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee deviceInterview successful not supported', async () => { await controller.start(); + MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.unsupported; const payload = {device, status: 'successful'}; await zigbeeHerdsman.events.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'pairing', message: 'interview_successful', meta: {friendly_name: '0x0017880104e45518', supported: false}}), - {retain: false, qos: 0}, - expect.any(Function), - ); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(parsedMessage.type).toStrictEqual('device_interview'); + expect(parsedMessage.data.friendly_name).toStrictEqual(device.ieeeAddr); + expect(parsedMessage.data.status).toStrictEqual('successful'); + expect(parsedMessage.data.ieee_address).toStrictEqual(device.ieeeAddr); + expect(parsedMessage.data.supported).toStrictEqual(false); + expect(parsedMessage.data.definition.model).toStrictEqual('notSupportedModelID'); + expect(parsedMessage.data.definition.vendor).toStrictEqual('notSupportedMfg'); + expect(parsedMessage.data.definition.description).toStrictEqual('Automatically generated definition'); + expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); + expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee event device announce', async () => { @@ -547,8 +553,8 @@ describe('Controller', () => { await flushPromises(); expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_announced', message: 'announce', meta: {friendly_name: 'bulb'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -564,8 +570,8 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceLeave(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -580,8 +586,8 @@ describe('Controller', () => { await zigbeeHerdsman.events.deviceLeave(payload); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_removed', message: 'left_network', meta: {friendly_name: '0x000b57fffec6a5b2'}}), + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}), {retain: false, qos: 0}, expect.any(Function), ); @@ -889,7 +895,7 @@ describe('Controller', () => { await flushPromises(); expect(MQTT.connect).toHaveBeenCalledTimes(1); const expected = { - will: {payload: Buffer.from('offline'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }; expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); }); @@ -899,7 +905,7 @@ describe('Controller', () => { MQTT.publish.mockClear(); MQTT.events['connect'](); await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers - expect(MQTT.publish).toHaveBeenCalledTimes(14); + expect(MQTT.publish).toHaveBeenCalledTimes(13); expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); @@ -924,17 +930,6 @@ describe('Controller', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function)); }); - it('Should disable legacy options on new network start', async () => { - settings.set(['homeassistant'], true); - settings.reRead(); - expect(settings.get().homeassistant.legacy_entity_attributes).toBeTruthy(); - expect(settings.get().advanced.legacy_api).toBeTruthy(); - zigbeeHerdsman.start.mockReturnValueOnce('reset'); - await controller.start(); - expect(settings.get().homeassistant.legacy_entity_attributes).toBeFalsy(); - expect(settings.get().advanced.legacy_api).toBeFalsy(); - }); - it('Should publish last seen changes', async () => { settings.set(['advanced', 'last_seen'], 'epoch'); await controller.start(); diff --git a/test/frontend.test.js b/test/frontend.test.js index e7321b3e81..f75743fecc 100644 --- a/test/frontend.test.js +++ b/test/frontend.test.js @@ -244,8 +244,7 @@ describe('Frontend', () => { const allTopics = mockWSClient.implementation.send.mock.calls.map((m) => JSON.parse(m).topic); expect(allTopics).toContain('bridge/devices'); expect(allTopics).toContain('bridge/info'); - expect(allTopics).toContain('bridge/config'); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: 'online'})); + expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}})); expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}})); // Message diff --git a/test/group.test.js b/test/group.test.js index 7168308364..4c160bb416 100644 --- a/test/group.test.js +++ b/test/group.test.js @@ -120,197 +120,6 @@ describe('Groups', () => { ]); }); - it('Legacy api: Add to group via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); - expect(group.members.length).toBe(0); - await resetExtension(); - MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color'); - await flushPromises(); - expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group_1'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Legacy api: Add to group with slashes via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups['group/with/slashes']; - settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}}); - expect(group.members.length).toBe(0); - await resetExtension(); - MQTT.events.message('zigbee2mqtt/bridge/group/group/with/slashes/add', 'bulb_color'); - await flushPromises(); - expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_group_add', message: {friendly_name: 'bulb_color', group: 'group/with/slashes'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Legacy api: Add to group via MQTT with postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; - expect(group.members.length).toBe(0); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right'); - await flushPromises(); - expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - }); - - it('Legacy api: Add to group via MQTT with postfix shouldnt add it twice', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; - expect(group.members.length).toBe(0); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right'); - await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', '0x0017880104e45542/3'); - await flushPromises(); - expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - }); - - it('Legacy api: Remove from group via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_group_remove', message: {friendly_name: 'bulb_color', group: 'group_1'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Legacy api: Remove from group via MQTT when in zigbee but not in settings', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']); - }); - - it('Legacy api: Remove from group via MQTT with postfix variant 1', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/3'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - }); - - it('Legacy api: Remove from group via MQTT with postfix variant 2', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'wall_switch_double/3'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - }); - - it('Legacy api: Remove from group via MQTT with postfix variant 3', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/right'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - }); - - it('Legacy api: Remove from group all', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/remove_all', '0x0017880104e45542/right'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_group_remove_all', message: {friendly_name: 'wall_switch_double'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Remove from group all deprecated', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); - await resetExtension(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove_all', '0x0017880104e45542/right'); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - }); - - it('Legacy api: Log when adding to non-existing group', async () => { - await resetExtension(); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/group/group_1_not_existing/add', 'bulb_color'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Group 'group_1_not_existing' does not exist"); - }); - - it('Legacy api: Log when adding a non-existing device', async () => { - await resetExtension(); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color_not_existing'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Device 'bulb_color_not_existing' does not exist"); - }); - - it('Legacy api: Log when adding a non-existing endpoint', async () => { - await resetExtension(); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color/not_existing_endpoint'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"); - }); - it('Should publish group state change when a device in it changes state', async () => { const device = zigbeeHerdsman.devices.bulb_color; const endpoint = device.getEndpoint(1); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index caed4fe3bc..45c7fb1a84 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -95,7 +95,7 @@ describe('HomeAssistant extension', () => { let payload; payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], brightness: true, brightness_scale: 254, command_topic: 'zigbee2mqtt/ha_discovery_group/set', @@ -140,7 +140,7 @@ describe('HomeAssistant extension', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/ha_discovery_group/set', device: { identifiers: ['zigbee2mqtt_1221051039810110150109113116116_9'], @@ -186,7 +186,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], enabled_by_default: true, }; @@ -216,7 +216,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -245,7 +245,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -275,7 +275,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -306,7 +306,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -317,7 +317,7 @@ describe('HomeAssistant extension', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/wall_switch_double/left/set', device: { identifiers: ['zigbee2mqtt_0x0017880104e45542'], @@ -346,7 +346,7 @@ describe('HomeAssistant extension', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/wall_switch_double/right/set', device: { identifiers: ['zigbee2mqtt_0x0017880104e45542'], @@ -375,7 +375,7 @@ describe('HomeAssistant extension', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], brightness: true, brightness_scale: 254, supported_color_modes: ['color_temp'], @@ -482,7 +482,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }); const topic2 = 'homeassistant/device_automation/0x0017880104e45522/action_double/config'; const payload2 = stringify({ @@ -553,7 +553,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -582,7 +582,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -611,7 +611,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -670,7 +670,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'From Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], expire_after: 90, icon: 'mdi:test', }; @@ -699,7 +699,7 @@ describe('HomeAssistant extension', () => { via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, origin: origin, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], expire_after: 30, icon: 'mdi:test', object_id: 'weather_sensor_humidity', @@ -745,7 +745,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], enabled_by_default: true, }; @@ -775,7 +775,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -807,7 +807,7 @@ describe('HomeAssistant extension', () => { await flushPromises(); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/my_switch/set', device: { identifiers: ['zigbee2mqtt_0x0017880104e45541'], @@ -896,7 +896,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Hampton Bay', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -915,6 +915,7 @@ describe('HomeAssistant extension', () => { availability: [ { topic: 'zigbee2mqtt/bridge/state', + value_template: '{{ value_json.state }}', }, ], current_temperature_template: '{{ value_json.local_temperature }}', @@ -962,7 +963,7 @@ describe('HomeAssistant extension', () => { action_template: "{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}", action_topic: 'zigbee2mqtt/bosch_radiator', - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], current_temperature_template: '{{ value_json.local_temperature }}', current_temperature_topic: 'zigbee2mqtt/bosch_radiator', device: { @@ -1028,7 +1029,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Keen Home', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1039,7 +1040,7 @@ describe('HomeAssistant extension', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/zigfred_plus/l6/set', device: { identifiers: ['zigbee2mqtt_0xf4ce368a38be56a1'], @@ -1101,7 +1102,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1280,7 +1281,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1449,7 +1450,10 @@ describe('HomeAssistant extension', () => { via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, availability_mode: 'all', - availability: [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}], + availability: [ + {topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}, + {topic: 'zigbee2mqtt/weather_sensor/availability', value_template: '{{ value_json.state }}'}, + ], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1462,7 +1466,7 @@ describe('HomeAssistant extension', () => { it('Should clear discovery when device is removed', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'weather_sensor'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor'); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( @@ -1544,7 +1548,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1595,7 +1599,7 @@ describe('HomeAssistant extension', () => { await flushPromises(); const payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], brightness: true, brightness_scale: 254, command_topic: 'zigbee2mqtt/ha_discovery_group_new/set', @@ -1681,7 +1685,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1712,7 +1716,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'IKEA', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], device_class: 'update', entity_category: 'diagnostic', }; @@ -2037,10 +2041,13 @@ describe('HomeAssistant extension', () => { it('Shouldnt crash in onPublishEntityState on group publish', async () => { logger.error.mockClear(); MQTT.publish.mockClear(); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(logger.error).toHaveBeenCalledTimes(0); + group.members.pop(); }); it('Should counter an action payload with an empty payload', async () => { @@ -2083,7 +2090,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message( 'homeassistant/light/1221051039810110150109113116116_91231/light/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); @@ -2098,14 +2105,17 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message( 'homeassistant/light/1221051039810110150109113116116_9/light/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); // Existing group with old topic structure (1.20.0) -> clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/light/9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/light/9/light/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function)); @@ -2114,7 +2124,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message( 'homeassistant/light/1221051039810110150109113116116_9/switch/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); @@ -2127,7 +2137,10 @@ describe('HomeAssistant extension', () => { // Non-existing device -> clear MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]})); + await MQTT.events.message( + 'homeassistant/sensor/0x123/temperature/config', + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), + ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', '', {qos: 1, retain: true}, expect.any(Function)); @@ -2136,7 +2149,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(0); @@ -2154,7 +2167,7 @@ describe('HomeAssistant extension', () => { MQTT.publish.mockClear(); await MQTT.events.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); @@ -2196,7 +2209,7 @@ describe('HomeAssistant extension', () => { await MQTT.events.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', - stringify({availability: [{topic: 'zigbee2mqtt/bridge/state'}]}), + stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( @@ -2244,7 +2257,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2265,7 +2278,7 @@ describe('HomeAssistant extension', () => { await flushPromises(); const payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], brightness: true, brightness_scale: 254, command_topic: 'zigbee2mqtt/ha_discovery_group/set', @@ -2311,9 +2324,6 @@ describe('HomeAssistant extension', () => { }); it('Should discover with json availability payload value_template', async () => { - settings.set(['advanced', 'legacy_availability_payload'], false); - await resetExtension(); - const payload = { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], brightness: true, @@ -2413,6 +2423,7 @@ describe('HomeAssistant extension', () => { availability: [ { topic: 'zigbee2mqtt/bridge/state', + value_template: '{{ value_json.state }}', }, ], device: { @@ -2472,7 +2483,7 @@ describe('HomeAssistant extension', () => { configuration_url: 'http://zigbee.mqtt/#/device/0x0017880104e45522/info', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2518,7 +2529,7 @@ describe('HomeAssistant extension', () => { via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, origin: origin, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, @@ -2562,7 +2573,7 @@ describe('HomeAssistant extension', () => { via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, origin: origin, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; expect(MQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, @@ -2593,7 +2604,7 @@ describe('HomeAssistant extension', () => { hw_version: 'z-Stack 20190425', sw_version: z2m_version, }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; @@ -2628,7 +2639,7 @@ describe('HomeAssistant extension', () => { device_class: 'connectivity', unique_id: 'bridge_0x00124b00120144ae_connection_state_zigbee2mqtt', state_topic: 'zigbee2mqtt/bridge/state', - value_template: '{{ value }}', + value_template: '{{ value_json.state }}', payload_on: 'online', payload_off: 'offline', origin: origin, @@ -2654,7 +2665,7 @@ describe('HomeAssistant extension', () => { payload_off: false, origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2674,7 +2685,7 @@ describe('HomeAssistant extension', () => { payload_press: '', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2697,7 +2708,7 @@ describe('HomeAssistant extension', () => { options: settings.LOG_LEVELS, origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2718,7 +2729,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.version }}', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2739,7 +2750,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.coordinator.meta.revision }}', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2761,7 +2772,7 @@ describe('HomeAssistant extension', () => { json_attributes_template: '{{ value_json.data.value | tojson }}', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2782,7 +2793,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2806,7 +2817,7 @@ describe('HomeAssistant extension', () => { payload_off: 'false', origin: origin, device: devicePayload, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -2842,7 +2853,7 @@ describe('HomeAssistant extension', () => { await zigbeeHerdsman.events.message(msg); await flushPromises(); const payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/0x18fc26000000cafe/set/device_mode', device: { identifiers: ['zigbee2mqtt_0x18fc26000000cafe'], diff --git a/test/legacy/bridgeLegacy.test.js b/test/legacy/bridgeLegacy.test.js deleted file mode 100644 index 5ee9c5112d..0000000000 --- a/test/legacy/bridgeLegacy.test.js +++ /dev/null @@ -1,538 +0,0 @@ -const data = require('../stub/data'); -const logger = require('../stub/logger'); -const zigbeeHerdsman = require('../stub/zigbeeHerdsman'); -const MQTT = require('../stub/mqtt'); -const stringify = require('json-stable-stringify-without-jsonify'); -const path = require('path'); -const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); -const settings = require('../../lib/util/settings'); -const Controller = require('../../lib/controller'); -const flushPromises = require('../lib/flushPromises'); - -describe('Bridge legacy', () => { - let controller; - let version; - - beforeAll(async () => { - jest.useFakeTimers(); - version = await require('../../lib/util/utils').default.getZigbee2MQTTVersion(); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - }); - - beforeEach(() => { - data.writeDefaultConfiguration(); - settings.reRead(); - logger.info.mockClear(); - logger.warning.mockClear(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - it('Should publish bridge configuration on startup', async () => { - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/config', - stringify({ - version: version.version, - commit: version.commitHash, - coordinator: {type: 'z-Stack', meta: {version: 1, revision: 20190425}}, - network: {panID: 5674, extendedPanID: [0, 11, 22], channel: 15}, - log_level: 'info', - permit_join: false, - }), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should allow to set elapsed', async () => { - MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'true'); - await flushPromises(); - expect(settings.get().advanced.elapsed).toBe(true); - MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'false'); - await flushPromises(); - expect(settings.get().advanced.elapsed).toBe(false); - MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'wrong'); - await flushPromises(); - expect(settings.get().advanced.elapsed).toBe(false); - }); - - it('Should allow whitelist', async () => { - const bulb_color = zigbeeHerdsman.devices.bulb_color; - const bulb = zigbeeHerdsman.devices.bulb; - expect(settings.get().passlist).toStrictEqual([]); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb_color'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb_color'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - - MQTT.publish.mockClear(); - expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr]); - MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_whitelisted', message: {friendly_name: 'bulb'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - - MQTT.publish.mockClear(); - expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); - MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); - await flushPromises(); - expect(settings.get().passlist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - }); - - it('Should allow changing device options', async () => { - const bulb_color = zigbeeHerdsman.devices.bulb_color; - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {retain: true}})); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', optionswrong: {retain: true}})); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', "{friendly_name: 'bulb_color'malformed: {retain: true}}"); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: true}); - MQTT.events.message('zigbee2mqtt/bridge/config/device_options', stringify({friendly_name: 'bulb_color', options: {random_setting: true}})); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ - ID: '0x000b57fffec6a5b3', - friendly_name: 'bulb_color', - random_setting: true, - retain: true, - }); - MQTT.events.message( - 'zigbee2mqtt/bridge/config/device_options', - stringify({friendly_name: 'bulb_color', options: {options: {random_1: true}}}), - ); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ - ID: '0x000b57fffec6a5b3', - friendly_name: 'bulb_color', - random_setting: true, - retain: true, - options: {random_1: true}, - }); - MQTT.events.message( - 'zigbee2mqtt/bridge/config/device_options', - stringify({friendly_name: 'bulb_color', options: {options: {random_2: false}}}), - ); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ - ID: '0x000b57fffec6a5b3', - friendly_name: 'bulb_color', - random_setting: true, - retain: true, - options: {random_1: true, random_2: false}, - }); - }); - - it('Should allow permit join', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'true'); - await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'false'); - await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); - }); - - it('Should allow to reset', async () => { - zigbeeHerdsman.reset.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/reset', ''); - await flushPromises(); - expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.reset).toHaveBeenCalledWith('soft'); - zigbeeHerdsman.reset.mockImplementationOnce(() => { - throw new Error(''); - }); - MQTT.events.message('zigbee2mqtt/bridge/config/reset', ''); - await flushPromises(); - expect(zigbeeHerdsman.reset).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.reset.mock.calls[1][0]).toBe('soft'); - }); - - it('Should allow to set last_seen', async () => { - MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'ISO_8601'); - await flushPromises(); - expect(settings.get().advanced.last_seen).toBe('ISO_8601'); - MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'disable'); - await flushPromises(); - expect(settings.get().advanced.last_seen).toBe('disable'); - MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'notvalid'); - await flushPromises(); - expect(settings.get().advanced.last_seen).toBe('disable'); - }); - - it('Should allow to set log_level', async () => { - MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'debug'); - await flushPromises(); - expect(logger.getLevel()).toBe('debug'); - MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'error'); - await flushPromises(); - expect(logger.getLevel()).toBe('error'); - MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'notvalid'); - await flushPromises(); - expect(logger.getLevel()).toBe('error'); - }); - - it('Should allow to get devices', async () => { - const now = Date.now; - Date.now = () => 100; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', ''); - await flushPromises(); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/config/devices'); - const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); - expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length); - expect(payload[1]).toStrictEqual({ - ieeeAddr: '0x00124b00120144ae', - type: 'Coordinator', - dateCode: '20190425', - friendly_name: 'Coordinator', - networkAddress: 0, - softwareBuildID: 'z-Stack', - lastSeen: 100, - }); - expect(payload[2]).toStrictEqual({ - dateCode: null, - friendly_name: 'bulb', - ieeeAddr: '0x000b57fffec6a5b2', - lastSeen: 1000, - manufacturerID: 4476, - model: 'LED1545G12', - modelID: 'TRADFRI bulb E27 WS opal 980lm', - networkAddress: 40369, - powerSource: 'Mains (single phase)', - softwareBuildID: null, - type: 'Router', - description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', - vendor: 'IKEA', - }); - Date.now = now; - }); - - it('Should allow to get groups', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/groups', ''); - await flushPromises(); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); - const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); - expect(payload).toStrictEqual({ - message: [ - {ID: 1, devices: [], friendly_name: 'group_1', retain: false}, - {ID: 2, devices: [], friendly_name: 'group_2', retain: false}, - {ID: 9, devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'}, - {ID: 11, devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false}, - {ID: 12, devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false}, - {ID: 14, devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false}, - {ID: 21, devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'}, - {ID: 15071, devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false}, - ], - type: 'groups', - }); - }); - - it('Should allow rename devices', async () => { - const bulb_color2 = {ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color2', retain: false}; - MQTT.publish.mockClear(); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); - MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color', new: 'bulb_color2'})); - await flushPromises(); - expect(settings.getDevice('bulb_color')).toBeUndefined(); - expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_renamed', message: {from: 'bulb_color', to: 'bulb_color2'}}), - {qos: 0, retain: false}, - expect.any(Function), - ); - - MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color2', newmalformed: 'bulb_color3'})); - await flushPromises(); - expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); - - MQTT.events.message('zigbee2mqtt/bridge/config/rename', "{old: 'bulb_color2'newmalformed: 'bulb_color3'}"); - await flushPromises(); - expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); - - MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'bulb_color', new: 'bulb_color3'})); - await flushPromises(); - expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); - }); - - it('Should allow rename groups', async () => { - MQTT.publish.mockClear(); - expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); - MQTT.events.message('zigbee2mqtt/bridge/config/rename', stringify({old: 'group_1', new: 'group_1_renamed'})); - await flushPromises(); - expect(settings.getGroup(1)).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1_renamed', retain: false}); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_renamed', message: {from: 'group_1', to: 'group_1_renamed'}}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Should allow to rename last joined device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const payload = {device}; - await zigbeeHerdsman.events.deviceJoined(payload); - await flushPromises(); - expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb'); - MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name'); - await flushPromises(); - expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb_new_name'); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Shouldnt rename when no device has been joined', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - await flushPromises(); - expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb'); - MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name'); - await flushPromises(); - expect(settings.getDevice('0x000b57fffec6a5b2').friendly_name).toStrictEqual('bulb'); - }); - - it('Should allow to add groups', async () => { - zigbeeHerdsman.createGroup.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_added', message: 'new_group'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []}); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3); - }); - - it('Should allow to add groups with json', async () => { - zigbeeHerdsman.createGroup.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group"}'); - await flushPromises(); - expect(settings.getGroup('new_group')).toStrictEqual({ID: 3, friendly_name: 'new_group', devices: []}); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_added', message: 'new_group'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Should allow to add groups with json specifying id', async () => { - zigbeeHerdsman.createGroup.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"friendly_name": "new_group", "id": 42}'); - await flushPromises(); - expect(settings.getGroup('new_group')).toStrictEqual({ID: 42, friendly_name: 'new_group', devices: []}); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_added', message: 'new_group'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Should allow to add groups with json specifying only id', async () => { - zigbeeHerdsman.createGroup.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{"id": 42}'); - await flushPromises(); - expect(settings.getGroup('group_42')).toStrictEqual({ID: 42, friendly_name: 'group_42', devices: []}); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(42); - }); - - it('Should allow to remove groups', async () => { - const group = zigbeeHerdsman.groups.group_1; - MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'group_1'); - await flushPromises(); - expect(settings.getGroup('to_be_removed')).toBeUndefined(); - expect(group.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_removed', message: 'group_1'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Should allow to force remove groups', async () => { - const group = zigbeeHerdsman.groups.group_1; - MQTT.events.message('zigbee2mqtt/bridge/config/force_remove_group', 'group_1'); - await flushPromises(); - expect(settings.getGroup('to_be_removed')).toBeUndefined(); - expect(group.removeFromDatabase).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'group_removed', message: 'group_1'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - it('Shouldnt allow add groups without id or friendly_name in json', async () => { - zigbeeHerdsman.createGroup.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/add_group', '{}'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith('Failed to add group, missing friendly_name!'); - }); - - it('Shouldnt do anything on unsupported topic', async () => { - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/not_supported', 'to_be_removed'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - }); - - it('Should allow to remove device', async () => { - controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}}; - const device = zigbeeHerdsman.devices.bulb_color; - device.removeFromNetwork.mockClear(); - expect(settings.get().blocklist.length).toBe(0); - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); - await flushPromises(); - expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(controller.state[device.ieeeAddr]).toBeUndefined(); - expect(settings.getDevice('bulb_color')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_removed', message: 'bulb_color'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - expect(controller.state.state).toStrictEqual({}); - expect(settings.get().blocklist.length).toBe(0); - }); - - it('Should allow to force remove device', async () => { - controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}}; - const device = zigbeeHerdsman.devices.bulb_color; - device.removeFromDatabase.mockClear(); - expect(settings.get().blocklist.length).toBe(0); - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/force_remove', 'bulb_color'); - await flushPromises(); - expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); - expect(controller.state[device.ieeeAddr]).toBeUndefined(); - expect(settings.getDevice('bulb_color')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_force_removed', message: 'bulb_color'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - expect(controller.state.state).toStrictEqual({}); - expect(settings.get().blocklist.length).toBe(0); - }); - - it('Should allow to block device', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - device.removeFromNetwork.mockClear(); - expect(settings.get().blocklist.length).toBe(0); - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); - await flushPromises(); - expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(controller.state[device.ieeeAddr]).toBeUndefined(); - expect(settings.getDevice('bulb_color')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_banned', message: 'bulb_color'}), - {qos: 0, retain: false}, - expect.any(Function), - ); - expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b3']); - }); - - it('Shouldnt crash when removing non-existing device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'not_existing_123'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Cannot remove, device 'not_existing_123' does not exist`); - }); - - it('Should handle when remove fails', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - device.removeFromNetwork.mockClear(); - device.removeFromNetwork.mockImplementationOnce(() => { - throw new Error(''); - }); - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); - await flushPromises(); - expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - }); - - it('Should handle when ban fails', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - device.removeFromNetwork.mockClear(); - device.removeFromNetwork.mockImplementationOnce(() => { - throw new Error(''); - }); - await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); - await flushPromises(); - expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(settings.getDevice('bulb_color')).toStrictEqual({ID: '0x000b57fffec6a5b3', friendly_name: 'bulb_color', retain: false}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - }); - - it('Should allow to touchlink factory reset (OK)', async () => { - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', ''); - await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith('Successfully factory reset device through Touchlink'); - }); - - it('Should allow to touchlink factory reset (FAILS)', async () => { - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(false); - MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', ''); - await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(logger.warning).toHaveBeenCalledWith('Failed to factory reset device through Touchlink'); - }); -}); diff --git a/test/legacy/report.test.js b/test/legacy/report.test.js deleted file mode 100644 index 425a40d913..0000000000 --- a/test/legacy/report.test.js +++ /dev/null @@ -1,315 +0,0 @@ -const data = require('../stub/data'); -const logger = require('../stub/logger'); -const zigbeeHerdsman = require('../stub/zigbeeHerdsman'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); -zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45553'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45559'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45524'); -zigbeeHerdsman.returnDevices.push('0x90fd9ffffe4b64ax'); -const MQTT = require('../stub/mqtt'); -const settings = require('../../lib/util/settings'); -const Controller = require('../../lib/controller'); -const flushPromises = require('../lib/flushPromises'); -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -describe('Report', () => { - let controller; - let extension; - - function expectOnOffBrightnessColorReport(endpoint, colorXY) { - const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const device = endpoint.getDevice(); - expect(device.meta.reporting).toBe(1); - expect(endpoint.unbind).toHaveBeenCalledTimes(0); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 300, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - ]); - if (colorXY) { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - {attribute: 'currentX', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - {attribute: 'currentY', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - ]); - } else { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - ]); - } - } - - function expectOnOffBrightnessColorReportDisabled(endpoint, colorXY) { - const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const device = endpoint.getDevice(); - expect(device.meta.reporting).toBe(undefined); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); - expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - expect(endpoint.unbind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, - ]); - if (colorXY) { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, - {attribute: 'currentX', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, - {attribute: 'currentY', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, - ]); - } else { - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', maximumReportInterval: 0xffff, minimumReportInterval: 3, reportableChange: 1}, - ]); - } - } - - const mockClear = (device) => { - for (const endpoint of device.endpoints) { - endpoint.read.mockClear(); - endpoint.write.mockClear(); - endpoint.configureReporting.mockClear(); - endpoint.bind.mockClear(); - endpoint.unbind.mockClear(); - } - }; - - beforeAll(async () => { - jest.useFakeTimers(); - settings.set(['advanced', 'report'], true); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - extension = controller.extensions.find((e) => e.constructor.name === 'Report'); - }); - - beforeEach(async () => { - extension.enabled = true; - data.writeDefaultConfiguration(); - settings.reRead(); - for (const device of Object.values(zigbeeHerdsman.devices)) { - mockClear(device); - delete device.meta.reporting; - } - mocksClear.forEach((m) => m.mockClear()); - extension.queue = new Set(); - extension.failed = new Set(); - await extension.start(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - it('Should configure reporting on startup', async () => { - await extension.start(); - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - expectOnOffBrightnessColorReport(endpoint, true); - }); - - it('Should not configure reporting on startup when disabled', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - mockClear(device); - delete device.meta.report; - extension.enabled = false; - await extension.start(); - expect(device.meta.reporting).toBe(undefined); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - }); - - it('Should disable reporting on startup when enabled earlier', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - device.meta.reporting = 1; - const endpoint = device.getEndpoint(1); - extension.enabled = false; - mockClear(device); - await extension.start(); - expectOnOffBrightnessColorReportDisabled(endpoint, true); - }); - - it('Should configure reporting when receicing message from device which has not been setup yet', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - device.save.mockClear(); - mockClear(device); - delete device.meta.reporting; - const data = {onOff: 1}; - const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expectOnOffBrightnessColorReport(endpoint, false); - expect(device.save).toHaveBeenCalledTimes(1); - }); - - it('Should not configure reporting when still configuring', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - endpoint.bind.mockImplementationOnce(async () => await wait(1000)); - delete device.meta.reporting; - mockClear(device); - const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(1); - }); - - it('Should not mark as configured when reporting setup fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - endpoint.bind.mockImplementationOnce(async () => { - throw new Error('failed'); - }); - delete device.meta.reporting; - mockClear(device); - const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(device.meta.reporting).toBeUndefined(); - }); - - it('Should not configure reporting when interviewing', async () => { - const device = zigbeeHerdsman.devices.bulb_2; - const endpoint = device.getEndpoint(1); - device.interviewing = true; - delete device.meta.reporting; - mockClear(device); - const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(device.meta.reporting).toBeUndefined(); - }); - - it('Should not configure reporting when receicing message from device which has already been setup yet', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - mockClear(device); - const data = {onOff: 1}; - const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - }); - - it('Should not configure reporting for end devices', async () => { - const device = zigbeeHerdsman.devices.E11_G13; - const endpoint = device.getEndpoint(1); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - }); - - it('Should configure reporting when deviceAnnounce message from IKEA device', async () => { - const device = zigbeeHerdsman.devices.bulb_2; - const endpoint = device.getEndpoint(1); - mockClear(device); - const payload = {device}; - await zigbeeHerdsman.events.deviceAnnounce(payload); - await flushPromises(); - expectOnOffBrightnessColorReport(endpoint, false); - }); - - it('Should not configure reporting on device leave', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - delete device.meta.reporting; - mockClear(device); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr}); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - }); - - it('Should not configure reporting for the CC2530 router', async () => { - const device = zigbeeHerdsman.devices.CC2530_ROUTER; - const endpoint = device.getEndpoint(1); - delete device.meta.reporting; - mockClear(device); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr}); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - }); - - it('Should not configure reporting again when it already failed once', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); - endpoint.bind.mockImplementationOnce(async () => { - throw new Error('failed'); - }); - delete device.meta.reporting; - mockClear(device); - const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(1); - }); - - it('Should not configure reporting for the ZNLDP12LM closuresWindowCovering as it is ignored', async () => { - const device = zigbeeHerdsman.devices.ZNLDP12LM; - const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const endpoint = device.getEndpoint(1); - delete device.meta.reporting; - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - }); - - it('Should not setup colorTemperature reporting when bulb does not support it and should read colorCapabilities when its not there yet ', async () => { - const device = zigbeeHerdsman.devices.bulb; - const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const endpoint = device.getEndpoint(1); - const configuredReportings = endpoint.configuredReportings; - endpoint.configuredReportings = []; - delete device.meta.reporting; - mockClear(device); - endpoint.getClusterAttributeValue = jest.fn(); - - let count = 0; - endpoint.getClusterAttributeValue.mockImplementation((d) => { - count++; - if (count === 1) return undefined; - return 17; - }); - - const payload = {device}; - await zigbeeHerdsman.events.deviceAnnounce(payload); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); - expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); - expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', maximumReportInterval: 300, minimumReportInterval: 3, reportableChange: 1}, - ]); - expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); - endpoint.configuredReportings = configuredReportings; - }); -}); diff --git a/test/networkMap.test.js b/test/networkMap.test.js index c61cce9444..0f553d7589 100644 --- a/test/networkMap.test.js +++ b/test/networkMap.test.js @@ -102,350 +102,8 @@ describe('Networkmap', () => { unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed')); } - it('Output raw networkmap legacy api', async () => { + it('Output raw networkmap', async () => { mock(); - MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'raw'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); - - const expected = { - links: [ - { - depth: 1, - linkquality: 120, - lqi: 120, - relationship: 2, - routes: [], - source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, - sourceIeeeAddr: '0x000b57fffec6a5b3', - sourceNwkAddr: 40399, - target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, - targetIeeeAddr: '0x00124b00120144ae', - }, - { - depth: 1, - linkquality: 92, - lqi: 92, - relationship: 2, - routes: [{destinationAddress: 6540, nextHop: 40369, status: 'ACTIVE'}], - source: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, - sourceIeeeAddr: '0x000b57fffec6a5b2', - sourceNwkAddr: 40369, - target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, - targetIeeeAddr: '0x00124b00120144ae', - }, - { - depth: 1, - linkquality: 92, - lqi: 92, - relationship: 2, - routes: [], - source: {ieeeAddr: '0x0017880104e45511', networkAddress: 1114}, - sourceIeeeAddr: '0x0017880104e45511', - sourceNwkAddr: 1114, - target: {ieeeAddr: '0x00124b00120144ae', networkAddress: 0}, - targetIeeeAddr: '0x00124b00120144ae', - }, - { - depth: 2, - linkquality: 110, - lqi: 110, - relationship: 1, - routes: [], - source: {ieeeAddr: '0x000b57fffec6a5b3', networkAddress: 40399}, - sourceIeeeAddr: '0x000b57fffec6a5b3', - sourceNwkAddr: 40399, - target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, - targetIeeeAddr: '0x000b57fffec6a5b2', - }, - { - depth: 2, - linkquality: 100, - lqi: 100, - relationship: 1, - routes: [], - source: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, - sourceIeeeAddr: '0x0017880104e45559', - sourceNwkAddr: 6540, - target: {ieeeAddr: '0x000b57fffec6a5b2', networkAddress: 40369}, - targetIeeeAddr: '0x000b57fffec6a5b2', - }, - { - depth: 2, - linkquality: 130, - lqi: 130, - relationship: 1, - routes: [], - source: {ieeeAddr: '0x0017880104e45521', networkAddress: 6538}, - sourceIeeeAddr: '0x0017880104e45521', - sourceNwkAddr: 6538, - target: {ieeeAddr: '0x0017880104e45559', networkAddress: 6540}, - targetIeeeAddr: '0x0017880104e45559', - }, - ], - nodes: [ - { - // definition: null, - failed: [], - friendlyName: 'Coordinator', - ieeeAddr: '0x00124b00120144ae', - lastSeen: 1000, - modelID: null, - networkAddress: 0, - type: 'Coordinator', - }, - { - definition: { - description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', - model: 'LED1545G12', - supports: - 'light (state, brightness, color_temp, color_temp_startup, level_config), effect, power_on_behavior, color_options, identify, linkquality', - vendor: 'IKEA', - }, - failed: [], - friendlyName: 'bulb', - ieeeAddr: '0x000b57fffec6a5b2', - lastSeen: 1000, - modelID: 'TRADFRI bulb E27 WS opal 980lm', - networkAddress: 40369, - type: 'Router', - }, - { - definition: { - description: 'Hue Go', - model: '7146060PH', - supports: - 'light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality', - vendor: 'Philips', - }, - failed: [], - friendlyName: 'bulb_color', - ieeeAddr: '0x000b57fffec6a5b3', - lastSeen: 1000, - modelID: 'LLC020', - networkAddress: 40399, - type: 'Router', - }, - { - definition: { - description: 'Wireless remote switch (double rocker), 2016 model', - model: 'WXKG02LM_rev1', - supports: 'battery, voltage, power_outage_count, action, linkquality', - vendor: 'Aqara', - }, - friendlyName: 'button_double_key', - ieeeAddr: '0x0017880104e45521', - lastSeen: 1000, - modelID: 'lumi.sensor_86sw2.es1', - networkAddress: 6538, - type: 'EndDevice', - }, - { - definition: { - description: 'Automatically generated definition', - model: 'notSupportedModelID', - supports: 'action, linkquality', - vendor: 'Boef', - }, - failed: ['lqi', 'routingTable'], - friendlyName: '0x0017880104e45525', - ieeeAddr: '0x0017880104e45525', - lastSeen: 1000, - manufacturerName: 'Boef', - modelID: 'notSupportedModelID', - networkAddress: 6536, - type: 'Router', - }, - { - definition: {description: 'CC2530 router', model: 'CC2530.ROUTER', supports: 'led, linkquality', vendor: 'Custom devices (DiY)'}, - failed: [], - friendlyName: 'cc2530_router', - ieeeAddr: '0x0017880104e45559', - lastSeen: 1000, - modelID: 'lumi.router', - networkAddress: 6540, - type: 'Router', - }, - { - definition: {description: 'external', model: 'external_converter_device', supports: 'linkquality', vendor: 'external'}, - friendlyName: '0x0017880104e45511', - ieeeAddr: '0x0017880104e45511', - lastSeen: 1000, - modelID: 'external_converter_device', - networkAddress: 1114, - type: 'EndDevice', - }, - ], - }; - expect(JSON.parse(call[1])).toStrictEqual(expected); - - /** - * Check again without routes - */ - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/networkmap', 'raw'); - await flushPromises(); - call = MQTT.publish.mock.calls[0]; - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); - - // Remove routing information - expected.nodes.forEach((n) => { - if (n.failed && n.failed.includes('routingTable')) { - n.failed.splice(n.failed.indexOf('routingTable'), 1); - } - }); - - expected.links.forEach((l) => (l.routes = [])); - expect(JSON.parse(call[1])).toStrictEqual(expected); - }); - - it('Output graphviz networkmap legacy api', async () => { - mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; - const endpoint = device.getEndpoint(1); - const data = {modelID: 'test'}; - const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'graphviz'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/graphviz'); - - const expected = `digraph G { - node[shape=record]; - "0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"]; - "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"]; - "0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"] - "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"]; - "0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"] - "0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"] - "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"]; - "0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"] - "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef Automatically generated definition (notSupportedModelID)|9 seconds ago}"]; - "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (0x198c)|Custom devices (DiY) CC2530 router (CC2530.ROUTER)|9 seconds ago}"]; - "0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"] - "0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (0x045a)|external external (external_converter_device)|9 seconds ago}"]; - "0x0017880104e45511" -> "0x00124b00120144ae" [penwidth=1, weight=0, color="#994444", label="92"] - }`; - - const expectedLines = expected.split('\n'); - const actualLines = call[1].split('\n'); - - for (let i = 0; i < expectedLines.length; i++) { - expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); - } - }); - - it('Output plantuml networkmap legacy api', async () => { - mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; - const endpoint = device.getEndpoint(1); - const data = {modelID: 'test'}; - const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'plantuml'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/plantuml'); - - const expected = `' paste into: https://www.planttext.com/ - - @startuml - card 0x0017880104e45511 [ - 0x0017880104e45511 - --- - 0x0017880104e45511 (0x045a) - --- - external external (external_converter_device) - --- - 9 seconds ago - ] - - card 0x0017880104e45525 [ - 0x0017880104e45525 - --- - 0x0017880104e45525 (0x1988) failed: lqi,routingTable - --- - Boef Automatically generated definition (notSupportedModelID) - --- - 9 seconds ago - ] - - card 0x000b57fffec6a5b2 [ - bulb - --- - 0x000b57fffec6a5b2 (0x9db1) - --- - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12) - --- - 9 seconds ago - ] - - card 0x000b57fffec6a5b3 [ - bulb_color - --- - 0x000b57fffec6a5b3 (0x9dcf) - --- - Philips Hue Go (7146060PH) - --- - unknown - ] - - card 0x0017880104e45521 [ - button_double_key - --- - 0x0017880104e45521 (0x198a) - --- - Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1) - --- - 9 seconds ago - ] - - card 0x0017880104e45559 [ - cc2530_router - --- - 0x0017880104e45559 (0x198c) - --- - Custom devices (DiY) CC2530 router (CC2530.ROUTER) - --- - 9 seconds ago - ] - - card 0x00124b00120144ae [ - Coordinator - --- - 0x00124b00120144ae (0x0000) - --- - 0 seconds ago - ] - - 0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120 - 0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92 - 0x0017880104e45511 --> 0x00124b00120144ae: 92 - 0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110 - 0x0017880104e45559 --> 0x000b57fffec6a5b2: 100 - 0x0017880104e45521 --> 0x0017880104e45559: 130 - - @enduml`; - - const expectedLines = expected.split('\n'); - const actualLines = call[1].split('\n'); - - for (let i = 0; i < expectedLines.length; i++) { - expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); - } - }); - - it('Should output raw networkmap', async () => { - mock(); - MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledTimes(1); @@ -637,6 +295,147 @@ describe('Networkmap', () => { expect(actual).toStrictEqual(expected); }); + it('Output graphviz networkmap', async () => { + mock(); + const device = zigbeeHerdsman.devices.bulb_color; + device.lastSeen = null; + const endpoint = device.getEndpoint(1); + const data = {modelID: 'test'}; + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + + const expected = `digraph G { + node[shape=record]; + "0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"]; + "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"]; + "0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"] + "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"]; + "0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"] + "0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"] + "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"]; + "0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"] + "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (0x1988)failed: lqi,routingTable|Boef Automatically generated definition (notSupportedModelID)|9 seconds ago}"]; + "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (0x198c)|Custom devices (DiY) CC2530 router (CC2530.ROUTER)|9 seconds ago}"]; + "0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"] + "0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (0x045a)|external external (external_converter_device)|9 seconds ago}"]; + "0x0017880104e45511" -> "0x00124b00120144ae" [penwidth=1, weight=0, color="#994444", label="92"] + }`; + + const expectedLines = expected.split('\n'); + const actualLines = JSON.parse(call[1]).data.value.split('\n'); + + for (let i = 0; i < expectedLines.length; i++) { + expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); + } + }); + + it('Output plantuml networkmap', async () => { + mock(); + const device = zigbeeHerdsman.devices.bulb_color; + device.lastSeen = null; + const endpoint = device.getEndpoint(1); + const data = {modelID: 'test'}; + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + + const expected = `' paste into: https://www.planttext.com/ + + @startuml + card 0x0017880104e45511 [ + 0x0017880104e45511 + --- + 0x0017880104e45511 (0x045a) + --- + external external (external_converter_device) + --- + 9 seconds ago + ] + + card 0x0017880104e45525 [ + 0x0017880104e45525 + --- + 0x0017880104e45525 (0x1988) failed: lqi,routingTable + --- + Boef Automatically generated definition (notSupportedModelID) + --- + 9 seconds ago + ] + + card 0x000b57fffec6a5b2 [ + bulb + --- + 0x000b57fffec6a5b2 (0x9db1) + --- + IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12) + --- + 9 seconds ago + ] + + card 0x000b57fffec6a5b3 [ + bulb_color + --- + 0x000b57fffec6a5b3 (0x9dcf) + --- + Philips Hue Go (7146060PH) + --- + unknown + ] + + card 0x0017880104e45521 [ + button_double_key + --- + 0x0017880104e45521 (0x198a) + --- + Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1) + --- + 9 seconds ago + ] + + card 0x0017880104e45559 [ + cc2530_router + --- + 0x0017880104e45559 (0x198c) + --- + Custom devices (DiY) CC2530 router (CC2530.ROUTER) + --- + 9 seconds ago + ] + + card 0x00124b00120144ae [ + Coordinator + --- + 0x00124b00120144ae (0x0000) + --- + 0 seconds ago + ] + + 0x000b57fffec6a5b3 --> 0x00124b00120144ae: 120 + 0x000b57fffec6a5b2 --> 0x00124b00120144ae: 92 + 0x0017880104e45511 --> 0x00124b00120144ae: 92 + 0x000b57fffec6a5b3 --> 0x000b57fffec6a5b2: 110 + 0x0017880104e45559 --> 0x000b57fffec6a5b2: 100 + 0x0017880104e45521 --> 0x0017880104e45559: 130 + + @enduml`; + + const expectedLines = expected.split('\n'); + const actualLines = JSON.parse(call[1]).data.value.split('\n'); + + for (let i = 0; i < expectedLines.length; i++) { + expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); + } + }); + it('Should throw error when requesting invalid type', async () => { mock(); MQTT.publish.mockClear(); diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js index 58a3098d7b..812d555f24 100644 --- a/test/otaUpdate.test.js +++ b/test/otaUpdate.test.js @@ -94,19 +94,19 @@ describe('OTA update', () => { expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: false, update: {state: 'updating', progress: 0}}), + stringify({update: {state: 'updating', progress: 0}}), {retain: true, qos: 0}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: false, update: {state: 'updating', progress: 10, remaining: 3600}}), + stringify({update: {state: 'updating', progress: 10, remaining: 3600}}), {retain: true, qos: 0}, expect.any(Function), ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: false, update: {state: 'idle', installed_version: 90, latest_version: 90}}), + stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}), {retain: true, qos: 0}, expect.any(Function), ); @@ -139,7 +139,7 @@ describe('OTA update', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: true, update: {state: 'available'}}), + stringify({update: {state: 'available'}}), {retain: true, qos: 0}, expect.any(Function), ); @@ -163,7 +163,7 @@ describe('OTA update', () => { expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'bulb', updateAvailable: false}, status: 'ok'}), + stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -176,7 +176,7 @@ describe('OTA update', () => { expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'bulb', updateAvailable: true}, status: 'ok'}), + stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -309,7 +309,7 @@ describe('OTA update', () => { expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: true, update: {state: 'available', installed_version: 10, latest_version: 12}}), + stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}), {retain: true, qos: 0}, expect.any(Function), ); @@ -342,7 +342,7 @@ describe('OTA update', () => { expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: false, update: {state: 'idle'}}), + stringify({update: {state: 'idle'}}), {retain: true, qos: 0}, expect.any(Function), ); @@ -373,7 +373,7 @@ describe('OTA update', () => { expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({update_available: false, update: {state: 'idle', installed_version: 13, latest_version: 13}}), + stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}), {retain: true, qos: 0}, expect.any(Function), ); @@ -438,126 +438,6 @@ describe('OTA update', () => { expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); }); - it('Legacy api: Should OTA update a device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - let count = 0; - endpoint.read.mockImplementation(() => { - count++; - return {swBuildId: count, dateCode: '2019010' + count}; - }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.info.mockClear(); - logger.error.mockClear(); - device.save.mockClear(); - mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { - onUpdate(0, null); - onUpdate(10, 3600); - return 91; - }); - - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); - await flushPromises(); - expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, expect.any(Function)); - expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); - expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); - expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith( - `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, - ); - expect(logger.error).toHaveBeenCalledTimes(0); - expect(device.save).toHaveBeenCalledTimes(2); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); - }); - - it('Legacy api: Should handle when OTA update fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => { - return {swBuildId: 1, dateCode: '2019010'}; - }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.info.mockClear(); - logger.error.mockClear(); - device.save.mockClear(); - mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { - throw new Error('Update failed'); - }); - - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`); - }); - - it('Legacy api: Should be able to check if OTA update is available', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - - logger.info.mockClear(); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13}); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`); - - logger.info.mockClear(); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 13, otaFileVersion: 15}); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); - }); - - it('Legacy api: Should handle if OTA update check fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.error.mockClear(); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - throw new Error('RF signals disturbed because of dogs barking'); - }); - - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`); - }); - - it('Legacy api: Should not check for OTA when device does not support it', async () => { - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'dimmer_wall_switch'); - await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Device 'dimmer_wall_switch' does not support OTA updates`); - }); - - it('Legacy api: Shouldnt crash when read modelID after OTA update fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - let count = 0; - endpoint.read.mockImplementation(() => { - if (count === 1) throw new Error('Failed!'); - count++; - return {swBuildId: 1, dateCode: '2019010'}; - }); - - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.info.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); - await flushPromises(); - expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - }); - it('Set zigbee_ota_override_index_location', async () => { settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json'); await resetExtension(); diff --git a/test/publish.test.js b/test/publish.test.js index fe7d370eef..36f5e060e8 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -402,24 +402,28 @@ describe('Publish', () => { it('Should publish messages groups', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); + group.members.pop(); }); it('Should publish messages to groups with brightness_percent', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 128, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 128}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128}); + group.members.pop(); }); it('Should publish messages to groups when converter is not in the default list but device in it supports it', async () => { @@ -437,46 +441,54 @@ describe('Publish', () => { it('Should publish messages to groups with on and brightness', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50}); + group.members.pop(); }); it('Should publish messages to groups with off and brightness', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'}); + group.members.pop(); }); it('Should publish messages to groups color', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 24248, colory: 18350, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); + group.members.pop(); }); it('Should publish messages to groups color temperature', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); + group.members.pop(); }); it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => { @@ -485,8 +497,8 @@ describe('Publish', () => { await MQTT.events.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'})); await flushPromises(); expect(Object.values(zigbeeHerdsman.groups).length).toBe(11); - expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); + // group contains no device + expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(0); delete zigbeeHerdsman.groups.group_12312; }); @@ -599,7 +611,6 @@ describe('Publish', () => { logger.error.mockClear(); await MQTT.events.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'})); await flushPromises(); - console.log(logger.error.mock.calls); expect(logger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m'); }); @@ -616,24 +627,6 @@ describe('Publish', () => { expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']); }); - it('Should not respond to bridge/config/devices/get', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', stringify({state: 'ON'})); - await flushPromises(); - expectNothingPublished(); - }); - - it('Should not respond to bridge/config/devices/set', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/config/devices/set', stringify({state: 'ON'})); - await flushPromises(); - expectNothingPublished(); - }); - - it('Should not respond to bridge/config/devices', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/config/devices', stringify({state: 'ON'})); - await flushPromises(); - expectNothingPublished(); - }); - it('Should parse topic with when base topic has multiple slashes', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); @@ -806,22 +799,6 @@ describe('Publish', () => { expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]); }); - it('Should read after write when enabled', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - settings.set(['devices', device.ieeeAddr, 'retrieve_state'], true); - const endpoint = device.getEndpoint(1); - const payload = {state: 'ON', color: {x: 0.701, y: 0.299}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); - await flushPromises(); - jest.runOnlyPendingTimers(); - expect(endpoint.command).toHaveBeenCalledTimes(2); - expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); - expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 45940, colory: 19595, transtime: 0}, {}]); - expect(endpoint.read).toHaveBeenCalledTimes(2); - expect(endpoint.read.mock.calls[0]).toEqual(['genOnOff', ['onOff']]); - expect(endpoint.read.mock.calls[1]).toEqual(['lightingColorCtrl', ['colorMode', 'currentX', 'currentY', 'colorTemperature']]); - }); - it('Should also use on/off cluster when controlling group with switch', async () => { const group = zigbeeHerdsman.groups.group_with_switch; @@ -855,14 +832,16 @@ describe('Publish', () => { it('Should use transition when brightness with group', async () => { const group = zigbeeHerdsman.groups.group_1; + group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); settings.set(['groups', '1', 'transition'], 20); await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 200}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 100}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100}); + group.members.pop(); }); it('Should use transition on brightness command', async () => { @@ -1545,18 +1524,16 @@ describe('Publish', () => { const endpoint = device.getEndpoint(1); await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); - jest.runOnlyPendingTimers(); - await flushPromises(); + await jest.runOnlyPendingTimersAsync(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GL-S-007ZS', stringify({state: 'ON', brightness: 20}), {qos: 0, retain: false}, expect.any(Function), - ]); + ); }); it('Should log as error when setting property with no defined converter', async () => { diff --git a/test/settings.test.js b/test/settings.test.js index 98772d3088..d29407c4d4 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -66,7 +66,7 @@ describe('Settings', () => { it('Should apply environment variables', () => { process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true'; - process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = 15; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'attribute_and_json'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT'] = '["console"]'; process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = @@ -97,7 +97,7 @@ describe('Settings', () => { }; expected.groups = {}; expected.serial.disable_led = true; - expected.advanced.soft_reset_timeout = 1; + expected.advanced.channel = 15; expected.advanced.log_output = ['console']; expected.advanced.output = 'attribute_and_json'; expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'}; @@ -548,7 +548,15 @@ describe('Settings', () => { expect(() => { settings.changeEntityOptions('not_existing_123', {}); - }).toThrow(new Error("Device or group 'not_existing_123' does not exist")); + }).toThrow(`Device or group 'not_existing_123' does not exist`); + }); + + it('Should throw error when changing entity friendlyName of non-existing device', () => { + write(configurationFile, {}); + + expect(() => { + settings.changeFriendlyName('not_existing_123', 'non_existing_456'); + }).toThrow(`Device or group 'not_existing_123' does not exist`); }); it('Should not add duplicate groups', () => { @@ -557,7 +565,7 @@ describe('Settings', () => { settings.addGroup('test123'); expect(() => { settings.addGroup('test123'); - }).toThrow(new Error("friendly_name 'test123' is already in use")); + }).toThrow(`friendly_name 'test123' is already in use`); const expected = { 1: { friendly_name: 'test123', @@ -746,19 +754,6 @@ describe('Settings', () => { expect(settings.validate()).toEqual(expect.arrayContaining([error])); }); - it('Should not allow non-existing entities in availability_blocklist', () => { - write(configurationFile, { - ...minimalConfig, - devices: {'0x0017880104e45519': {friendly_name: 'tain'}}, - advanced: {availability_blocklist: ['0x0017880104e45519', 'non_existing']}, - }); - - settings.reRead(); - - const error = `Non-existing entity 'non_existing' specified in 'availability_blocklist'`; - expect(settings.validate()).toEqual(expect.arrayContaining([error])); - }); - it('Should validate if settings do not conform to scheme', () => { write(configurationFile, { ...minimalConfig, diff --git a/update.sh b/update.sh index 4879594e95..7ca9e44abe 100755 --- a/update.sh +++ b/update.sh @@ -3,13 +3,6 @@ cd "$(dirname "$0")" NEED_RESTART=0 -if [ -d data-backup ]; then - echo "ERROR: Backup directory exists. May be previous restoring was failed?" - echo "1. Save 'data-backup' and 'data' dirs to safe location to make possibility to restore config later." - echo "2. Manually delete 'data-backup' dir and try again." - exit 1 -fi - if which systemctl 2> /dev/null > /dev/null; then echo "Checking Zigbee2MQTT status..." if systemctl is-active --quiet zigbee2mqtt; then @@ -21,24 +14,14 @@ else echo "Skipped stopping Zigbee2MQTT, no systemctl found" fi -echo "Creating backup of configuration..." -cp -R data data-backup - -echo "Checking out changes to package-lock.json..." -git checkout package-lock.json - echo "Updating..." git pull --no-rebase echo "Installing dependencies..." -npm ci +pnpm i --frozen-lockfile echo "Building..." -npm run build - -echo "Restore configuration..." -cp -R data-backup/* data -rm -rf data-backup +pnpm run build if [ $NEED_RESTART -eq 1 ]; then echo "Starting Zigbee2MQTT..." From 2124d3450cbc5ad5901a7fa2d657b881152a6db6 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:18:23 +0200 Subject: [PATCH 06/70] fix!: Improve permit join (#24257) * fix: Improve permit join * Update Home Assistant permit join switch * Remove `permit_join` from `settings.schema.json` * Update zigbee-herdsman version to pre-release. * Fix pnpm overrides * Update test/homeassistant.test.js --------- Co-authored-by: Koen Kanters --- lib/controller.ts | 13 ---- lib/extension/bridge.ts | 37 +++--------- lib/extension/homeassistant.ts | 5 +- lib/types/types.d.ts | 1 - lib/util/settings.schema.json | 6 -- lib/util/settings.ts | 1 - lib/zigbee.ts | 16 ++--- package.json | 10 +-- pnpm-lock.yaml | 36 ++++++----- test/bridge.test.js | 107 +++++++++------------------------ test/controller.test.js | 19 ------ test/homeassistant.test.js | 5 +- test/settings.test.js | 8 +-- test/stub/data.js | 1 - test/stub/zigbeeHerdsman.js | 3 +- 15 files changed, 80 insertions(+), 188 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index 60dcde1733..e36ed832a0 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -167,19 +167,6 @@ export class Controller { logger.info(`Currently ${deviceCount} devices are joined.`); - // Enable zigbee join - try { - if (settings.get().permit_join) { - logger.warning('`permit_join` set to `true` in configuration.yaml.'); - logger.warning('Allowing new devices to join.'); - logger.warning('Set `permit_join` to `false` once you joined all devices.'); - } - - await this.zigbee.permitJoin(settings.get().permit_join); - } catch (error) { - logger.error(`Failed to set permit join to ${settings.get().permit_join} (${(error as Error).message})`); - } - // MQTT try { await this.mqtt.connect(); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index d5d1549d30..69ff55963f 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -225,10 +225,6 @@ export default class Bridge extends Extension { if (restartRequired) this.restartRequired = true; // Apply some settings on-the-fly. - if (newSettings.permit_join != undefined) { - await this.zigbee.permitJoin(settings.get().permit_join); - } - if (newSettings.homeassistant != undefined) { await this.enableDisableExtension(!!settings.get().homeassistant, 'HomeAssistant'); } @@ -323,17 +319,15 @@ export default class Bridge extends Extension { } @bind async permitJoin(message: KeyValue | string): Promise { - if (typeof message === 'object' && message.value === undefined) { - throw new Error('Invalid payload'); - } - - let value: boolean | string; let time: number | undefined; let device: Device | undefined; if (typeof message === 'object') { - value = message.value; - time = message.time; + if (message.time === undefined) { + throw new Error('Invalid payload'); + } + + time = Number.parseInt(message.time, 10); if (message.device) { const resolved = this.zigbee.resolveEntity(message.device); @@ -345,25 +339,15 @@ export default class Bridge extends Extension { } } } else { - value = message; - } - - if (typeof value === 'string') { - value = value.toLowerCase() === 'true'; + time = Number.parseInt(message, 10); } - await this.zigbee.permitJoin(value, device, time); + await this.zigbee.permitJoin(time, device); - const response: {value: boolean; device?: string; time?: number} = {value}; + const response: {time: number; device?: string} = {time}; - if (typeof message === 'object') { - if (device) { - response.device = message.device; - } - - if (time != undefined) { - response.time = message.time; - } + if (device) { + response.device = device.name; } return utils.getResponse(message, response); @@ -679,7 +663,6 @@ export default class Bridge extends Extension { }, network: utils.toSnakeCaseObject(await this.zigbee.getNetworkParameters()), log_level: logger.getLevel(), - permit_join: this.zigbee.getPermitJoin(), permit_join_timeout: this.zigbee.getPermitJoinTimeout(), restart_required: this.restartRequired, config, diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 4e964d2b7e..f641a129d5 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -2203,8 +2203,9 @@ export default class HomeAssistant extends Extension { value_template: '{{ value_json.permit_join | lower }}', command_topic: `${baseTopic}/request/permit_join`, state_on: 'true', - payload_on: '{"value": true, "time": 254}', - payload_off: 'false', + state_off: 'false', + payload_on: '{"time": 254}', + payload_off: '{"time": 0}', }, }, ); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index aee04cd7b7..312a442892 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -120,7 +120,6 @@ declare global { legacy_triggers: boolean; experimental_event_entities: boolean; }; - permit_join: boolean; availability?: { active: {timeout: number}; passive: {timeout: number}; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index defa569e0e..8043099cd6 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -51,12 +51,6 @@ } ] }, - "permit_join": { - "type": "boolean", - "default": false, - "title": "Permit join", - "description": "Allow new devices to join (re-applied at restart)" - }, "availability": { "oneOf": [ { diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 7ecdd7a469..eabd8be7fe 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -43,7 +43,6 @@ const ajvRestartRequiredGroupOptions = new Ajv({allErrors: true}) .addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}) .compile(schemaJson.definitions.group); const defaults: RecursivePartial = { - permit_join: false, external_converters: [], mqtt: { base_topic: 'zigbee2mqtt', diff --git a/lib/zigbee.ts b/lib/zigbee.ts index 5786cdc713..8a7e5e2b52 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -225,26 +225,18 @@ export default class Zigbee { logger.info('Stopped zigbee-herdsman'); } - getPermitJoin(): boolean { - return this.herdsman.getPermitJoin(); - } - - getPermitJoinTimeout(): number | undefined { + getPermitJoinTimeout(): number { return this.herdsman.getPermitJoinTimeout(); } - async permitJoin(permit: boolean, device?: Device, time?: number): Promise { - if (permit) { + async permitJoin(time: number, device?: Device): Promise { + if (time > 0) { logger.info(`Zigbee: allowing new devices to join${device ? ` via ${device.name}` : ''}.`); } else { logger.info('Zigbee: disabling joining new devices.'); } - if (device && permit) { - await this.herdsman.permitJoin(permit, device.zh, time); - } else { - await this.herdsman.permitJoin(permit, undefined, time); - } + await this.herdsman.permitJoin(time, device?.zh); } @bind private resolveDevice(ieeeAddr: string): Device | undefined { diff --git a/package.json b/package.json index cb3b655406..c0ba7d1d12 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,9 @@ "winston-syslog": "^2.7.1", "winston-transport": "^4.9.0", "ws": "^8.18.0", - "zigbee-herdsman": "2.1.9", - "zigbee-herdsman-converters": "20.58.0", - "zigbee2mqtt-frontend": "0.7.6" + "zigbee-herdsman": "3.0.0-pre.0", + "zigbee-herdsman-converters": "20.28.0", + "zigbee2mqtt-frontend": "0.7.4" }, "devDependencies": { "@babel/core": "^7.26.0", @@ -93,8 +93,8 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.15.0" }, - "overrides": { - "zigbee-herdsman-converters": { + "pnpm": { + "overrides": { "zigbee-herdsman": "$zigbee-herdsman" } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d0bb94d2..a7378bcb44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + zigbee-herdsman: 3.0.0-pre.0 + importers: .: @@ -81,11 +84,11 @@ importers: specifier: ^8.18.0 version: 8.18.0 zigbee-herdsman: - specifier: 2.1.3 - version: 2.1.3 + specifier: 3.0.0-pre.0 + version: 3.0.0-pre.0 zigbee-herdsman-converters: - specifier: 20.25.0 - version: 20.25.0 + specifier: 20.28.0 + version: 20.28.0 zigbee2mqtt-frontend: specifier: 0.7.4 version: 0.7.4 @@ -95,13 +98,13 @@ importers: version: 2.8.0 devDependencies: '@babel/core': - specifier: ^7.25.7 + specifier: ^7.25.8 version: 7.25.8 '@babel/plugin-proposal-decorators': specifier: ^7.25.7 version: 7.25.7(@babel/core@7.25.8) '@babel/preset-env': - specifier: ^7.25.7 + specifier: ^7.25.8 version: 7.25.8(@babel/core@7.25.8) '@babel/preset-typescript': specifier: ^7.25.7 @@ -131,7 +134,7 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.7.4 + specifier: ^22.7.5 version: 22.7.5 '@types/object-assign-deep': specifier: ^0.4.3 @@ -164,10 +167,10 @@ importers: specifier: ^0.2.3 version: 0.2.3 typescript: - specifier: ^5.6.2 + specifier: ^5.6.3 version: 5.6.3 typescript-eslint: - specifier: ^8.8.0 + specifier: ^8.8.1 version: 8.8.1(eslint@9.12.0)(typescript@5.6.3) packages: @@ -2800,11 +2803,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zigbee-herdsman-converters@20.25.0: - resolution: {integrity: sha512-iqCQmcdwrUGz00IhvtB36l3YN9sCWPhbx8D+lf9owyjpE5To7u2pOn4GA4g6QWQCU7BndCif8fXed1FWt9/nrg==} + zigbee-herdsman-converters@20.28.0: + resolution: {integrity: sha512-AMCuG4mlIR21JgShIyFnZsCimfITH2HpjQLS0vaAqIKmbYrG7sBF0MU/iznYc/8JqNq+Jav3BdV+BrE1M3GpEg==} - zigbee-herdsman@2.1.3: - resolution: {integrity: sha512-1LiSb3L2ZFzNOuGJHMWBFPRgPs1WVMS4CagtvYxEVUdifhVivp1vMNrCxBsbwNhDiNBh/Blk0pTR0szJttLzrA==} + zigbee-herdsman@3.0.0-pre.0: + resolution: {integrity: sha512-3mCSmdwu5eJb0DEJrTDSQPYEQAz1mOGAbqvXyEOjo6UOllo++GyzpmawTxWBH4FLWrbuKc9SefiVqQdC/4uZbQ==} zigbee2mqtt-frontend@0.7.4: resolution: {integrity: sha512-skWNYxThSa6Ywn7aRB0ZvRKWifpqbku4+vUM5BbXiNaXYxCCbU0b3pN258Ahxt3NsLtYk2zBdYoQcXuBZxmJxw==} @@ -5939,7 +5942,7 @@ snapshots: yocto-queue@0.1.0: {} - zigbee-herdsman-converters@20.25.0: + zigbee-herdsman-converters@20.28.0: dependencies: axios: 1.7.7 buffer-crc32: 1.0.0 @@ -5947,12 +5950,13 @@ snapshots: iconv-lite: 0.6.3 semver: 7.6.3 tar-stream: 3.1.7 - zigbee-herdsman: 2.1.3 + uri-js: 4.4.1 + zigbee-herdsman: 3.0.0-pre.0 transitivePeerDependencies: - debug - supports-color - zigbee-herdsman@2.1.3: + zigbee-herdsman@3.0.0-pre.0: dependencies: '@serialport/bindings-cpp': 12.0.1 '@serialport/parser-delimiter': 12.0.0 diff --git a/test/bridge.test.js b/test/bridge.test.js index 9f9fc8c431..7bd2ddfb5b 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -64,6 +64,7 @@ describe('Bridge', () => { logger.warning.mockClear(); logger.setTransportsEnabled(false); MQTT.publish.mockClear(); + zigbeeHerdsman.permitJoin.mockClear(); const device = zigbeeHerdsman.devices.bulb; device.interview.mockClear(); device.removeFromDatabase.mockClear(); @@ -203,14 +204,13 @@ describe('Bridge', () => { mqtt: {base_topic: 'zigbee2mqtt', force_disable_retain: false, include_device_information: false, server: 'mqtt://localhost'}, ota: {disable_automatic_update_check: false, update_check_interval: 1440}, passlist: [], - permit_join: true, serial: {disable_led: false, port: '/dev/dummy'}, }, config_schema: settings.schema, coordinator: {ieee_address: '0x00124b00120144ae', meta: {revision: 20190425, version: 1}, type: 'z-Stack'}, log_level: 'info', network: {channel: 15, extended_pan_id: [0, 11, 22], pan_id: 5674}, - permit_join: false, + permit_join_timeout: 0, restart_required: false, version: version.version, zigbee_herdsman: zhVersion, @@ -2579,50 +2579,47 @@ describe('Bridge', () => { ); }); - it('Should allow permit join', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', 'true'); + it('Should allow permit join on all', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: true}, status: 'ok'}), + stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); + }); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false})); + it('Should disallow permit join on all', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(0, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), + stringify({data: {time: 0}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); + }); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: 'False'})); + it('Should allow permit join with number string (automatically on all)', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', '1'); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), + stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); + }); - // Invalid payload - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value_bla: false})); + it('Should not allow permit join with invalid payload', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( @@ -2633,21 +2630,6 @@ describe('Bridge', () => { ); }); - it('Should allow permit join for certain time', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false, time: 10})); - await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false, time: 10}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - it('Should republish bridge info when permit join changes', async () => { MQTT.publish.mockClear(); await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); @@ -2665,23 +2647,22 @@ describe('Bridge', () => { it('Should allow permit join via device', async () => { const device = zigbeeHerdsman.devices.bulb; - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: true, device: 'bulb'})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, device, undefined); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(123, device); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: true, device: 'bulb'}, status: 'ok'}), + stringify({data: {time: 123, device: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); + }); - // Device does not exist - zigbeeHerdsman.permitJoin.mockClear(); + it('Should not allow permit join via non-existing device', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: true, device: 'bulb_not_existing_woeeee'})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); await flushPromises(); expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); expect(MQTT.publish).toHaveBeenCalledWith( @@ -2694,11 +2675,11 @@ describe('Bridge', () => { it('Should put transaction in response when request is done with transaction', async () => { MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false, transaction: 22})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0, transaction: 22})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok', transaction: 22}), + stringify({data: {time: 0}, status: 'ok', transaction: 22}), {retain: false, qos: 0}, expect.any(Function), ); @@ -2709,7 +2690,7 @@ describe('Bridge', () => { throw new Error('Failed to connect to adapter'); }); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: false})); + MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', @@ -3555,7 +3536,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3600,7 +3580,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3627,7 +3606,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3654,7 +3632,6 @@ describe('Bridge', () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1); endpoint.configureReporting.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', @@ -3704,7 +3681,6 @@ describe('Bridge', () => { }); it('Should allow to restart', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/restart', ''); await flushPromises(); @@ -3718,24 +3694,6 @@ describe('Bridge', () => { ); }); - it('Change options', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - settings.apply({permit_join: false}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {permit_join: true}})); - await flushPromises(); - expect(settings.get().permit_join).toBe(true); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/options', - stringify({data: {restart_required: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - it('Change options and apply - homeassistant', async () => { expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).toBeUndefined(); MQTT.publish.mockClear(); @@ -3807,7 +3765,6 @@ describe('Bridge', () => { }); it('Change options restart required', async () => { - zigbeeHerdsman.permitJoin.mockClear(); settings.apply({serial: {port: '123'}}); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); @@ -3822,7 +3779,6 @@ describe('Bridge', () => { }); it('Change options array', async () => { - zigbeeHerdsman.permitJoin.mockClear(); expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]); MQTT.publish.mockClear(); MQTT.events.message( @@ -3840,7 +3796,6 @@ describe('Bridge', () => { }); it('Change options with null', async () => { - zigbeeHerdsman.permitJoin.mockClear(); expect(settings.get().serial).toStrictEqual({disable_led: false, port: '/dev/dummy'}); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {disable_led: false, port: null}}})); @@ -3855,7 +3810,6 @@ describe('Bridge', () => { }); it('Change options invalid payload', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); await flushPromises(); @@ -3868,13 +3822,12 @@ describe('Bridge', () => { }); it('Change options not valid against schema', async () => { - zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {permit_join: 'true'}})); + MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({data: {}, error: 'permit_join must be boolean', status: 'error'}), + stringify({data: {}, error: 'external_converters must be array', status: 'error'}), {retain: false, qos: 0}, expect.any(Function), ); diff --git a/test/controller.test.js b/test/controller.test.js index 939daf82d3..82c243f044 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -10,7 +10,6 @@ const stringify = require('json-stable-stringify-without-jsonify'); const flushPromises = require('./lib/flushPromises'); const tmp = require('tmp'); const mocksClear = [ - zigbeeHerdsman.permitJoin, MQTT.end, zigbeeHerdsman.stop, logger.debug, @@ -84,8 +83,6 @@ describe('Controller', () => { }); expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`); expect(logger.info).toHaveBeenCalledWith( 'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)', @@ -105,15 +102,6 @@ describe('Controller', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0}, expect.any(Function)); }); - it('Start controller when permit join fails', async () => { - zigbeeHerdsman.permitJoin.mockImplementationOnce(() => { - throw new Error('failed!'); - }); - await controller.start(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(MQTT.connect).toHaveBeenCalledTimes(1); - }); - it('Start controller with specific MQTT settings', async () => { const ca = tmp.fileSync().name; fs.writeFileSync(ca, 'ca'); @@ -283,13 +271,6 @@ describe('Controller', () => { expect(mockExit).toHaveBeenCalledWith(1, false); }); - it('Start controller with permit join true', async () => { - settings.set(['permit_join'], false); - await controller.start(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); - }); - it('Start controller and stop with restart', async () => { await controller.start(); await controller.stop(true); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 45c7fb1a84..05687cc5e0 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -2813,8 +2813,9 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.permit_join | lower }}', command_topic: 'zigbee2mqtt/bridge/request/permit_join', state_on: 'true', - payload_on: '{"value": true, "time": 254}', - payload_off: 'false', + state_off: 'false', + payload_on: '{"time": 254}', + payload_off: '{"time": 0}', origin: origin, device: devicePayload, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], diff --git a/test/settings.test.js b/test/settings.test.js index d29407c4d4..1902648da5 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -13,7 +13,7 @@ const yaml = require('js-yaml'); const objectAssignDeep = require(`object-assign-deep`); const minimalConfig = { - permit_join: true, + external_converters: [], homeassistant: true, mqtt: {base_topic: 'zigbee2mqtt', server: 'localhost'}, }; @@ -55,12 +55,12 @@ describe('Settings', () => { }); it('Should return settings', () => { - write(configurationFile, {permit_join: true}); + write(configurationFile, {external_converters: ['abcd.js']}); const s = settings.get(); const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; - expected.permit_join = true; + expected.external_converters = ['abcd.js']; expect(s).toStrictEqual(expected); }); @@ -964,7 +964,7 @@ describe('Settings', () => { }, }); settings.reRead(); - settings.apply({permit_join: false}); + settings.apply({external_converters: []}); expect(settings.get().device_options.homeassistant).toStrictEqual({temperature: null}); expect(settings.get().devices['0x1234567812345678'].homeassistant).toStrictEqual({humidity: null}); }); diff --git a/test/stub/data.js b/test/stub/data.js index 66a9f80e49..8e00f0d2eb 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -10,7 +10,6 @@ const stateFile = path.join(mockDir, 'state.json'); function writeDefaultConfiguration() { const config = { homeassistant: false, - permit_join: true, mqtt: { base_topic: 'zigbee2mqtt', server: 'mqtt://localhost', diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 5cdfe3c563..75a12437ca 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -895,8 +895,7 @@ const mock = { getGroupByID: jest.fn().mockImplementation((groupID) => { return Object.values(groups).find((d) => d.groupID === groupID); }), - getPermitJoin: jest.fn().mockReturnValue(false), - getPermitJoinTimeout: jest.fn().mockReturnValue(undefined), + getPermitJoinTimeout: jest.fn().mockReturnValue(0), reset: jest.fn(), createGroup: jest.fn().mockImplementation((groupID) => { const group = new Group(groupID, []); From 6f6e7c3ee040504231e1aecda004edbd16301578 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:33:54 +0200 Subject: [PATCH 07/70] fix: Refactor tests to TS (#24357) --- .prettierrc | 1 + index.js | 2 +- lib/controller.ts | 2 - lib/util/utils.ts | 9 +- lib/util/yaml.ts | 4 +- ...{controller.test.js => controller.test.ts} | 584 +++++---- test/{data.test.js => data.test.ts} | 10 +- .../availability.test.ts} | 157 +-- .../{bind.test.js => extensions/bind.test.ts} | 387 +++--- .../bridge.test.ts} | 1023 ++++++++------- .../configure.test.ts} | 163 +-- .../externalConverters.test.ts} | 92 +- .../externalExtension.test.ts} | 112 +- test/extensions/frontend.test.ts | 393 ++++++ .../groups.test.ts} | 738 ++++++----- .../homeassistant.test.ts} | 705 +++++----- .../networkMap.test.ts} | 243 ++-- test/extensions/onEvent.test.ts | 88 ++ test/extensions/otaUpdate.test.ts | 426 ++++++ .../publish.test.ts} | 1138 +++++++++-------- .../receive.test.ts} | 459 +++---- test/frontend.test.js | 426 ------ test/lib/flushPromises.js | 2 - test/{logger.test.js => logger.test.ts} | 119 +- test/{stub/data.js => mocks/data.ts} | 45 +- test/mocks/debounce.ts | 3 + test/mocks/jszip.ts | 11 + test/mocks/logger.ts | 53 + test/{stub/mqtt.js => mocks/mqtt.ts} | 35 +- test/mocks/sleep.ts | 11 + test/mocks/types.d.ts | 15 + test/mocks/utils.ts | 15 + .../zigbeeHerdsman.ts} | 572 ++++++--- test/onEvent.test.js | 96 -- test/otaUpdate.test.js | 459 ------- test/{settings.test.js => settings.test.ts} | 67 +- test/stub/logger.js | 48 - test/stub/sleep.js | 10 - test/tsconfig.json | 10 + test/{utils.test.js => utils.test.ts} | 34 +- tsconfig.json | 7 +- 41 files changed, 4577 insertions(+), 4197 deletions(-) rename test/{controller.test.js => controller.test.ts} (59%) rename test/{data.test.js => data.test.ts} (83%) rename test/{availability.test.js => extensions/availability.test.ts} (76%) rename test/{bind.test.js => extensions/bind.test.ts} (64%) rename test/{bridge.test.js => extensions/bridge.test.ts} (83%) rename test/{configure.test.js => extensions/configure.test.ts} (63%) rename test/{externalConverters.test.js => extensions/externalConverters.test.ts} (57%) rename test/{externalExtension.test.js => extensions/externalExtension.test.ts} (56%) create mode 100644 test/extensions/frontend.test.ts rename test/{group.test.js => extensions/groups.test.ts} (52%) rename test/{homeassistant.test.js => extensions/homeassistant.test.ts} (85%) rename test/{networkMap.test.js => extensions/networkMap.test.ts} (83%) create mode 100644 test/extensions/onEvent.test.ts create mode 100644 test/extensions/otaUpdate.test.ts rename test/{publish.test.js => extensions/publish.test.ts} (56%) rename test/{receive.test.js => extensions/receive.test.ts} (55%) mode change 100755 => 100644 delete mode 100644 test/frontend.test.js delete mode 100644 test/lib/flushPromises.js rename test/{logger.test.js => logger.test.ts} (79%) rename test/{stub/data.js => mocks/data.ts} (91%) create mode 100644 test/mocks/debounce.ts create mode 100644 test/mocks/jszip.ts create mode 100644 test/mocks/logger.ts rename test/{stub/mqtt.js => mocks/mqtt.ts} (50%) create mode 100644 test/mocks/sleep.ts create mode 100644 test/mocks/types.d.ts create mode 100644 test/mocks/utils.ts rename test/{stub/zigbeeHerdsman.js => mocks/zigbeeHerdsman.ts} (62%) delete mode 100644 test/onEvent.test.js delete mode 100644 test/otaUpdate.test.js rename test/{settings.test.js => settings.test.ts} (95%) delete mode 100644 test/stub/logger.js delete mode 100644 test/stub/sleep.js create mode 100644 test/tsconfig.json rename test/{utils.test.js => utils.test.ts} (68%) diff --git a/.prettierrc b/.prettierrc index 0c99705e7f..aa720a4e85 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,6 +7,7 @@ "endOfLine": "lf", "tabWidth": 4, "importOrder": [ + "^[./]*/mocks", "", "^(node:)", "", diff --git a/index.js b/index.js index bfc1eecedd..61789ea726 100644 --- a/index.js +++ b/index.js @@ -148,7 +148,7 @@ async function start() { return exit(1); } - const Controller = require('./dist/controller'); + const {Controller} = require('./dist/controller'); controller = new Controller(restart, exit); await controller.start(); diff --git a/lib/controller.ts b/lib/controller.ts index e36ed832a0..8d5ecde8ca 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -366,5 +366,3 @@ export class Controller { } } } - -module.exports = Controller; diff --git a/lib/util/utils.ts b/lib/util/utils.ts index bde76c8a69..7985049725 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -10,6 +10,11 @@ import humanizeDuration from 'humanize-duration'; import data from './data'; +function pad(num: number): string { + const norm = Math.floor(Math.abs(num)); + return (norm < 10 ? '0' : '') + norm; +} + // construct a local ISO8601 string (instead of UTC-based) // Example: // - ISO8601 (UTC) = 2019-03-01T15:32:45.941+0000 @@ -17,10 +22,6 @@ import data from './data'; function toLocalISOString(date: Date): string { const tzOffset = -date.getTimezoneOffset(); const plusOrMinus = tzOffset >= 0 ? '+' : '-'; - const pad = (num: number): string => { - const norm = Math.floor(Math.abs(num)); - return (norm < 10 ? '0' : '') + norm; - }; return ( date.getFullYear() + diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index 2c77db2c45..b3aed60168 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import fs from 'fs'; import equals from 'fast-deep-equal/es6'; @@ -20,7 +21,8 @@ export class YAMLFileException extends YAMLException { function read(file: string): KeyValue { try { const result = yaml.load(fs.readFileSync(file, 'utf8')); - return (result as KeyValue) ?? {}; + assert(result instanceof Object); + return result as KeyValue; } catch (error) { if (error instanceof YAMLException) { throw new YAMLFileException(error, file); diff --git a/test/controller.test.js b/test/controller.test.ts similarity index 59% rename from test/controller.test.js rename to test/controller.test.ts index 82c243f044..671a8dd21c 100644 --- a/test/controller.test.js +++ b/test/controller.test.ts @@ -1,56 +1,60 @@ -process.env.NOTIFY_SOCKET = 'mocked'; -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const path = require('path'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const stringify = require('json-stable-stringify-without-jsonify'); -const flushPromises = require('./lib/flushPromises'); -const tmp = require('tmp'); -const mocksClear = [ - MQTT.end, - zigbeeHerdsman.stop, - logger.debug, - MQTT.publish, - MQTT.connect, - zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, - logger.error, -]; +import * as data from './mocks/data'; +import {mockLogger} from './mocks/logger'; +import {mockMQTT, mockMQTTConnect, events as mockMQTTEvents} from './mocks/mqtt'; +import {EventHandler, flushPromises, JestMockAny} from './mocks/utils'; +import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from './mocks/zigbeeHerdsman'; + +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import tmp from 'tmp'; -const fs = require('fs'); +import {Controller as ZHController} from 'zigbee-herdsman'; +import {Controller} from '../lib/controller'; +import * as settings from '../lib/util/settings'; + +process.env.NOTIFY_SOCKET = 'mocked'; const LOG_MQTT_NS = 'z2m:mqtt'; jest.mock( 'sd-notify', () => { return { - watchdogInterval: () => { - return 3000; - }, - startWatchdogMode: (interval) => {}, - stopWatchdogMode: () => {}, - ready: () => {}, - stopping: () => {}, + watchdogInterval: jest.fn(() => 3000), + startWatchdogMode: jest.fn(), + stopWatchdogMode: jest.fn(), + ready: jest.fn(), + stopping: jest.fn(), }; }, {virtual: true}, ); +const mocksClear = [ + mockZHController.stop, + mockMQTT.end, + mockMQTT.publish, + mockMQTTConnect, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockLogger.log, + mockLogger.debug, + mockLogger.info, + mockLogger.error, +]; + describe('Controller', () => { - let controller; - let mockExit; + let controller: Controller; + let mockExit: JestMockAny; beforeAll(async () => { jest.useFakeTimers(); }); beforeEach(() => { - MQTT.restoreOnMock(); - zigbeeHerdsman.returnDevices.splice(0); + returnDevices.splice(0); mockExit = jest.fn(); data.writeDefaultConfiguration(); settings.reRead(); @@ -67,7 +71,7 @@ describe('Controller', () => { it('Start controller', async () => { settings.set(['advanced', 'transmit_power'], 14); await controller.start(); - expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({ + expect(ZHController).toHaveBeenCalledWith({ network: { panID: 6754, extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], @@ -81,25 +85,29 @@ describe('Controller', () => { adapter: {concurrent: undefined, delay: undefined, disableLED: false, transmitPower: 14}, serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'}, }); - expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined.`); - expect(logger.info).toHaveBeenCalledWith( + expect(mockZHController.start).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith(`Currently ${Object.values(devices).length - 1} devices are joined.`); + expect(mockLogger.info).toHaveBeenCalledWith( 'bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (Router)', ); - expect(logger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); - expect(logger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); - expect(MQTT.connect).toHaveBeenCalledTimes(1); - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', { + expect(mockLogger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); + expect(mockLogger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', { will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote', + stringify({brightness: 255}), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Start controller with specific MQTT settings', async () => { @@ -126,7 +134,7 @@ describe('Controller', () => { settings.set(['mqtt'], configuration); await controller.start(); await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); const expected = { will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, keepalive: 30, @@ -139,7 +147,7 @@ describe('Controller', () => { rejectUnauthorized: false, protocolVersion: 5, }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => { @@ -148,9 +156,9 @@ describe('Controller', () => { settings.set(['advanced', 'ext_pan_id'], 'GENERATE'); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.networkKey.length).toStrictEqual(16); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8); - expect(zigbeeHerdsman.constructor.mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number)); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.networkKey.length).toStrictEqual(16); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8); + expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number)); expect(data.read().advanced.network_key.length).toStrictEqual(16); expect(data.read().advanced.ext_pan_id.length).toStrictEqual(8); expect(data.read().advanced.pan_id).toStrictEqual(expect.any(Number)); @@ -160,14 +168,19 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: true}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote', + stringify({brightness: 255}), + {qos: 0, retain: true}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false}, expect.any(Function)); }); it('Start controller should not publish cached states when disabled', async () => { @@ -175,7 +188,7 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - const publishedTopics = MQTT.publish.mock.calls.map((m) => m[0]); + const publishedTopics = mockMQTT.publish.mock.calls.map((m) => m[0]); expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote'])); }); @@ -184,30 +197,34 @@ describe('Controller', () => { data.writeDefaultState(); await controller.start(); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'zigbee2mqtt/bulb', `{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`, {qos: 0, retain: true}, expect.any(Function), ); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}, expect.any(Function)); }); it('Log when MQTT client is unavailable', async () => { await controller.start(); await flushPromises(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.mqtt.client.reconnecting = true; jest.advanceTimersByTime(11 * 1000); - expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + // @ts-expect-error private controller.mqtt.client.reconnecting = false; }); it('Dont publish to mqtt when client is unavailable', async () => { await controller.start(); await flushPromises(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.mqtt.client.reconnecting = true; + // @ts-expect-error private const device = controller.zigbee.resolveEntity('bulb'); await controller.publishEntityState(device, { state: 'ON', @@ -217,11 +234,12 @@ describe('Controller', () => { dummy: {1: 'yes', 2: 'no'}, }); await flushPromises(); - expect(logger.error).toHaveBeenCalledTimes(2); - expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); - expect(logger.error).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledTimes(2); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + expect(mockLogger.error).toHaveBeenCalledWith( 'Cannot send message: topic: \'zigbee2mqtt/bulb\', payload: \'{"brightness":50,"color":{"b":10,"g":50,"r":100},"color_temp":370,"dummy":{"1":"yes","2":"no"},"linkquality":99,"state":"ON"}', ); + // @ts-expect-error private controller.mqtt.client.reconnecting = false; }); @@ -229,30 +247,31 @@ describe('Controller', () => { data.removeState(); await controller.start(); await flushPromises(); + // @ts-expect-error private expect(controller.state.state).toStrictEqual({}); }); it('Should remove device not on passlist on startup', async () => { - settings.set(['passlist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - zigbeeHerdsman.devices.bulb.removeFromNetwork.mockImplementationOnce(() => { + settings.set(['passlist'], [devices.bulb_color.ieeeAddr]); + devices.bulb.removeFromNetwork.mockImplementationOnce(() => { throw new Error('dummy'); }); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); + expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); }); it('Should remove device on blocklist on startup', async () => { - settings.set(['blocklist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); + settings.set(['blocklist'], [devices.bulb_color.ieeeAddr]); await controller.start(); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); + expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); }); it('Start controller fails', async () => { - zigbeeHerdsman.start.mockImplementationOnce(() => { + mockZHController.start.mockImplementationOnce(() => { throw new Error('failed'); }); await controller.start(); @@ -260,13 +279,16 @@ describe('Controller', () => { }); it('Start controller fails due to MQTT', async () => { - MQTT.on.mockImplementation((type, handler) => { - if (type === 'error') handler({message: 'addr not found'}); - }); + const cb = (type: string, handler: EventHandler): void => { + if (type === 'error') { + handler({message: 'addr not found'}); + } + }; + mockMQTT.on.mockImplementationOnce(cb).mockImplementationOnce(cb); await controller.start(); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith('MQTT error: addr not found'); - expect(logger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)'); + expect(mockLogger.error).toHaveBeenCalledWith('MQTT error: addr not found'); + expect(mockLogger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)'); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); @@ -274,65 +296,62 @@ describe('Controller', () => { it('Start controller and stop with restart', async () => { await controller.start(); await controller.stop(true); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(0, true); }); it('Start controller and stop', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => { - throw new Error('failed'); - }); + mockZHController.stop.mockRejectedValueOnce('failed'); await controller.start(); await controller.stop(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); it('Start controller adapter disconnects', async () => { - zigbeeHerdsman.stop.mockImplementationOnce(() => { - throw new Error('failed'); - }); + mockZHController.stop.mockRejectedValueOnce('failed'); await controller.start(); - await zigbeeHerdsman.events.adapterDisconnected(); + await mockZHEvents.adapterDisconnected(); await flushPromises(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockMQTT.end).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledTimes(1); expect(mockExit).toHaveBeenCalledWith(1, false); }); it('Handle mqtt message', async () => { - const eventbus = controller.eventBus; - let spyEventbusEmitMQTTMessage = jest.spyOn(eventbus, 'emitMQTTMessage').mockImplementation(); + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); await controller.start(); - logger.debug.mockClear(); - await MQTT.events.message('dummytopic', 'dummymessage'); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('dummytopic', 'dummymessage'); expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'dummytopic', message: 'dummymessage'}); - expect(logger.log).toHaveBeenCalledWith('debug', "Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS); + expect(mockLogger.log).toHaveBeenCalledWith('debug', "Received MQTT message on 'dummytopic' with data 'dummymessage'", LOG_MQTT_NS); }); it('Skip MQTT messages on topic we published to', async () => { - const eventbus = controller.eventBus; - let spyEventbusEmitMQTTMessage = jest.spyOn(eventbus, 'emitMQTTMessage').mockImplementation(); + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); await controller.start(); - logger.debug.mockClear(); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'zigbee2mqtt/skip-this-topic', message: 'skipped'}); - logger.debug.mockClear(); + mockLogger.debug.mockClear(); + // @ts-expect-error private await controller.mqtt.publish('skip-this-topic', '', {}); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledTimes(0); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); + expect(mockLogger.debug).toHaveBeenCalledTimes(0); }); it('On zigbee event message', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = { device, endpoint: device.getEndpoint(1), @@ -341,9 +360,9 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`, 'z2m', @@ -352,7 +371,7 @@ describe('Controller', () => { it('On zigbee event message with group ID', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = { device, endpoint: device.getEndpoint(1), @@ -362,9 +381,9 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`, 'z2m', @@ -373,17 +392,17 @@ describe('Controller', () => { it('Should add entities which are missing from configuration but are in database to configuration', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.notInSettings; + const device = devices.notInSettings; expect(settings.getDevice(device.ieeeAddr)).not.toBeUndefined(); }); it('On zigbee deviceJoined', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - await zigbeeHerdsman.events.deviceJoined(payload); + await mockZHEvents.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -393,60 +412,60 @@ describe('Controller', () => { it('acceptJoiningDeviceHandler reject device on blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(false); }); it('acceptJoiningDeviceHandler accept device not on blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['blocklist'], ['123']); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler accept device on passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler reject device not in passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], ['123']); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(false); }); it('acceptJoiningDeviceHandler should prefer passlist above blocklist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['passlist'], [device.ieeeAddr]); settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('acceptJoiningDeviceHandler accept when not on blocklist and passlist', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler; + const device = devices.bulb; + const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler; expect(await handler(device.ieeeAddr)).toBe(true); }); it('Shouldnt crash when two device join events are received', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - zigbeeHerdsman.events.deviceJoined(payload); - zigbeeHerdsman.events.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -456,11 +475,11 @@ describe('Controller', () => { it('On zigbee deviceInterview started', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device, status: 'started'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -470,11 +489,11 @@ describe('Controller', () => { it('On zigbee deviceInterview failed', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device, status: 'failed'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -484,13 +503,13 @@ describe('Controller', () => { it('On zigbee deviceInterview successful supported', async () => { await controller.start(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.bulb; + mockMQTT.publish.mockClear(); + const device = devices.bulb; const payload = {device, status: 'successful'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); - const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publish.mock.calls[1][1]); expect(parsedMessage.type).toStrictEqual('device_interview'); expect(parsedMessage.data.friendly_name).toStrictEqual('bulb'); expect(parsedMessage.data.status).toStrictEqual('successful'); @@ -501,18 +520,18 @@ describe('Controller', () => { expect(parsedMessage.data.definition.description).toStrictEqual('TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm'); expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee deviceInterview successful not supported', async () => { await controller.start(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.unsupported; + mockMQTT.publish.mockClear(); + const device = devices.unsupported; const payload = {device, status: 'successful'}; - await zigbeeHerdsman.events.deviceInterview(payload); + await mockZHEvents.deviceInterview(payload); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); - const parsedMessage = JSON.parse(MQTT.publish.mock.calls[1][1]); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publish.mock.calls[1][1]); expect(parsedMessage.type).toStrictEqual('device_interview'); expect(parsedMessage.data.friendly_name).toStrictEqual(device.ieeeAddr); expect(parsedMessage.data.status).toStrictEqual('successful'); @@ -523,17 +542,17 @@ describe('Controller', () => { expect(parsedMessage.data.definition.description).toStrictEqual('Automatically generated definition'); expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array)); expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array)); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); }); it('On zigbee event device announce', async () => { await controller.start(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {device}; - await zigbeeHerdsman.events.deviceAnnounce(payload); + await mockZHEvents.deviceAnnounce(payload); await flushPromises(); - expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -543,14 +562,14 @@ describe('Controller', () => { it('On zigbee event device leave (removed from database and settings)', async () => { await controller.start(); - zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); + returnDevices.push('0x00124b00120144ae'); settings.set(['devices'], {}); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.bulb; + mockMQTT.publish.mockClear(); + const device = devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; - await zigbeeHerdsman.events.deviceLeave(payload); + await mockZHEvents.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}), {retain: false, qos: 0}, @@ -560,13 +579,13 @@ describe('Controller', () => { it('On zigbee event device leave (removed from database and NOT settings)', async () => { await controller.start(); - zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); + returnDevices.push('0x00124b00120144ae'); + const device = devices.bulb; + mockMQTT.publish.mockClear(); const payload = {ieeeAddr: device.ieeeAddr}; - await zigbeeHerdsman.events.deviceLeave(payload); + await mockZHEvents.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}), {retain: false, qos: 0}, @@ -577,8 +596,9 @@ describe('Controller', () => { it('Publish entity state attribute output', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute'); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, { dummy: {1: 'yes', 2: 'no'}, color: {r: 100, g: 50, b: 10}, @@ -589,29 +609,30 @@ describe('Controller', () => { brightness: 50, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}, expect.any(Function)); }); it('Publish entity state attribute_json output', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}), {qos: 0, retain: true}, @@ -622,15 +643,16 @@ describe('Controller', () => { it('Publish entity state attribute_json output filtered', async () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -642,14 +664,15 @@ describe('Controller', () => { await controller.start(); settings.set(['experimental', 'output'], 'attribute_and_json'); settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -660,21 +683,24 @@ describe('Controller', () => { it('Publish entity state attribute_json output filtered cache', async () => { await controller.start(); settings.set(['advanced', 'output'], 'attribute_and_json'); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); - MQTT.publish.mockClear(); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); + mockMQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); await flushPromises(); + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'}); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), {qos: 0, retain: true}, @@ -686,20 +712,23 @@ describe('Controller', () => { await controller.start(); settings.set(['advanced', 'output'], 'attribute_and_json'); settings.set(['device_options', 'filtered_cache'], ['linkquality']); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}); await flushPromises(); + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'}); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), {qos: 0, retain: true}, @@ -710,11 +739,12 @@ describe('Controller', () => { it('Publish entity state with device information', async () => { await controller.start(); settings.set(['mqtt', 'include_device_information'], true); - MQTT.publish.mockClear(); - let device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + let device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -729,8 +759,6 @@ describe('Controller', () => { type: 'Router', manufacturerID: 4476, powerSource: 'Mains (single phase)', - dateCode: null, - softwareBuildID: null, }, }), {qos: 0, retain: true}, @@ -738,10 +766,11 @@ describe('Controller', () => { ); // Unsupported device should have model "unknown" - device = controller.zigbee.resolveEntity('unsupported2'); + // @ts-expect-error private + device = controller.zigbee.resolveEntity('unsupported2')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/unsupported2', stringify({ state: 'ON', @@ -753,8 +782,6 @@ describe('Controller', () => { type: 'EndDevice', manufacturerID: 0, powerSource: 'Battery', - dateCode: null, - softwareBuildID: null, }, }), {qos: 0, retain: false}, @@ -764,12 +791,13 @@ describe('Controller', () => { it('Should publish entity state without retain', async () => { await controller.start(); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], false); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], false); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: false}, @@ -779,12 +807,13 @@ describe('Controller', () => { it('Should publish entity state with retain', async () => { await controller.start(); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], true); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: true}, @@ -795,13 +824,14 @@ describe('Controller', () => { it('Should publish entity state with expiring retention', async () => { await controller.start(); settings.set(['mqtt', 'version'], 5); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retain'], true); - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'retention'], 37); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + settings.set(['devices', devices.bulb.ieeeAddr, 'retention'], 37); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), {qos: 0, retain: true, properties: {messageExpiryInterval: 37}}, @@ -812,25 +842,27 @@ describe('Controller', () => { it('Publish entity state no empty messages', async () => { data.writeEmptyState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should allow to disable state persistency', async () => { settings.set(['advanced', 'cache_state_persistent'], false); data.removeState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await controller.publishEntityState(device, {brightness: 200}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}, @@ -843,30 +875,34 @@ describe('Controller', () => { it('Shouldnt crash when it cannot save state', async () => { data.removeState(); await controller.start(); - logger.error.mockClear(); + mockLogger.error.mockClear(); + // @ts-expect-error private controller.state.file = '/'; - await controller.state.save(); - expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to \'\/\'/)); + // @ts-expect-error private + controller.state.save(); + expect(mockLogger.error).toHaveBeenCalledWith(expect.stringMatching(/Failed to write state to '\/'/)); }); it('Publish should not cache when set', async () => { settings.set(['advanced', 'cache_state'], false); data.writeEmptyState(); await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); + mockMQTT.publish.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; await controller.publishEntityState(device, {state: 'ON'}); await controller.publishEntityState(device, {brightness: 200}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}, expect.any(Function)); }); it('Should start when state is corrupted', async () => { fs.writeFileSync(path.join(data.mockDir, 'state.json'), 'corrupted'); await controller.start(); await flushPromises(); + // @ts-expect-error private expect(controller.state.state).toStrictEqual({}); }); @@ -874,52 +910,54 @@ describe('Controller', () => { settings.set(['mqtt', 'force_disable_retain'], true); await controller.start(); await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); + expect(mockMQTTConnect).toHaveBeenCalledTimes(1); const expected = { will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); + expect(mockMQTTConnect).toHaveBeenCalledWith('mqtt://localhost', expected); }); it('Should republish retained messages on MQTT reconnect', async () => { await controller.start(); - MQTT.publish.mockClear(); - MQTT.events['connect'](); + mockMQTT.publish.mockClear(); + mockMQTTEvents['connect'](); await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers - expect(MQTT.publish).toHaveBeenCalledTimes(13); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(13); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should not republish retained messages on MQTT reconnect when retained message are sent', async () => { await controller.start(); - MQTT.publish.mockClear(); - MQTT.events['connect'](); + mockMQTT.publish.mockClear(); + mockMQTTEvents['connect'](); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bridge/info', 'dummy'); + await mockMQTTEvents.message('zigbee2mqtt/bridge/info', 'dummy'); await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => { settings.set(['mqtt', 'force_disable_retain'], true); + // @ts-expect-error private await controller.mqtt.connect(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private await controller.mqtt.publish('fo', 'bar', {retain: true}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish last seen changes', async () => { settings.set(['advanced', 'last_seen'], 'epoch'); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.remote; - await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'deviceAnnounce'}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + mockMQTT.publish.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'deviceAnnounce'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({brightness: 255, last_seen: 1000}), {qos: 0, retain: true}, @@ -931,16 +969,16 @@ describe('Controller', () => { settings.set(['advanced', 'last_seen'], 'epoch'); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.remote; - await zigbeeHerdsman.events.lastSeenChanged({device, reason: 'messageEmitted'}); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + mockMQTT.publish.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'messageEmitted'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Ignore messages from coordinator', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/9218 await controller.start(); - const device = zigbeeHerdsman.devices.coordinator; + const device = devices.coordinator; const payload = { device, endpoint: device.getEndpoint(1), @@ -949,33 +987,37 @@ describe('Controller', () => { cluster: 'genBasic', data: {modelId: device.modelID}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( + expect(mockLogger.log).toHaveBeenCalledWith( 'debug', - `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' from endpoint 1, ignoring since it is from coordinator`, + `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{}' from endpoint 1, ignoring since it is from coordinator`, 'z2m', ); }); it('Should remove state of removed device when stopped', async () => { await controller.start(); - const device = controller.zigbee.resolveEntity('bulb'); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); device.zh.isDeleted = true; await controller.stop(); + // @ts-expect-error private expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined); }); it('EventBus should handle errors', async () => { + // @ts-expect-error private const eventbus = controller.eventBus; const callback = jest.fn().mockImplementation(async () => { throw new Error('Whoops!'); }); - eventbus.onStateChange('test', callback); + eventbus.onStateChange({constructor: {name: 'Test'}}, callback); eventbus.emitStateChange({}); await flushPromises(); expect(callback).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`EventBus error 'String/stateChange': Whoops!`); + expect(mockLogger.error).toHaveBeenCalledWith(`EventBus error 'Test/stateChange': Whoops!`); }); }); diff --git a/test/data.test.js b/test/data.test.ts similarity index 83% rename from test/data.test.js rename to test/data.test.ts index 34b80302f1..6b9b41b437 100644 --- a/test/data.test.js +++ b/test/data.test.ts @@ -1,8 +1,8 @@ -const logger = require('./stub/logger'); -const data = require('../lib/util/data').default; -const path = require('path'); -const tmp = require('tmp'); -const fs = require('fs'); +import path from 'path'; + +import tmp from 'tmp'; + +import data from '../lib/util/data'; describe('Data', () => { describe('Get path', () => { diff --git a/test/availability.test.js b/test/extensions/availability.test.ts similarity index 76% rename from test/availability.test.js rename to test/extensions/availability.test.ts index a48c6e3009..43a2395655 100644 --- a/test/availability.test.js +++ b/test/extensions/availability.test.ts @@ -1,45 +1,47 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const Availability = require('../lib/extension/availability').default; -const stringify = require('json-stable-stringify-without-jsonify'); -const utils = require('../lib/util/utils').default; - -const mocks = [MQTT.publish, logger.warning, logger.info]; -const devices = zigbeeHerdsman.devices; -zigbeeHerdsman.returnDevices.push( - ...[ - devices.bulb_color.ieeeAddr, - devices.bulb_color_2.ieeeAddr, - devices.coordinator.ieeeAddr, - devices.remote.ieeeAddr, - devices.TS0601_thermostat.ieeeAddr, - devices.bulb_2.ieeeAddr, - devices.ZNCZ02LM.ieeeAddr, - devices.GLEDOPTO_2ID.ieeeAddr, - devices.QBKG03LM.ieeeAddr, - ], +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import assert from 'assert'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import Availability from '../../lib/extension/availability'; +import * as settings from '../../lib/util/settings'; +import utils from '../../lib/util/utils'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.info]; + +returnDevices.push( + devices.bulb_color.ieeeAddr, + devices.bulb_color_2.ieeeAddr, + devices.coordinator.ieeeAddr, + devices.remote.ieeeAddr, + devices.TS0601_thermostat.ieeeAddr, + devices.bulb_2.ieeeAddr, + devices.ZNCZ02LM.ieeeAddr, + devices.GLEDOPTO_2ID.ieeeAddr, + devices.QBKG03LM.ieeeAddr, ); -describe('Availability', () => { - let controller; +describe('Extension: Availability', () => { + let controller: Controller; - let resetExtension = async () => { + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Availability'); await controller.enableDisableExtension(true, 'Availability'); }; - const setTimeAndAdvanceTimers = async (value) => { + const setTimeAndAdvanceTimers = async (value: number): Promise => { jest.setSystemTime(Date.now() + value); await jest.advanceTimersByTimeAsync(value); }; beforeAll(async () => { - jest.spyOn(utils, 'sleep').mockImplementation(async (seconds) => {}); + jest.spyOn(utils, 'sleep').mockImplementation(); jest.useFakeTimers(); settings.reRead(); settings.set(['availability'], true); @@ -55,7 +57,7 @@ describe('Availability', () => { settings.set(['availability'], true); settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], false); Object.values(devices).forEach((d) => (d.lastSeen = utils.minutes(1))); - mocks.forEach((m) => m.mockClear()); + mocksClear.forEach((m) => m.mockClear()); await resetExtension(); Object.values(devices).forEach((d) => d.ping.mockClear()); }); @@ -68,19 +70,19 @@ describe('Availability', () => { }); it('Should publish availability on startup for device where it is enabled for', async () => { - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color_2/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -112,14 +114,14 @@ describe('Availability', () => { }); it('Should publish offline for active device when not seen for 10 minutes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(5)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); await setTimeAndAdvanceTimers(utils.minutes(7)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1); expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -128,17 +130,17 @@ describe('Availability', () => { }); it('Shouldnt do anything for a device when availability: false is set for device', async () => { - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); // Coverage satisfaction + await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2}); // Coverage satisfaction await setTimeAndAdvanceTimers(utils.minutes(12)); expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0); }); it('Should publish offline for passive device when not seen for 25 hours', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -147,13 +149,13 @@ describe('Availability', () => { }); it('Should reset ping timer when device last seen changes for active device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(5)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); - expect(MQTT.publish).toHaveBeenCalledWith( + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -169,9 +171,9 @@ describe('Availability', () => { }); it('Should ping again when first ping fails', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); devices.bulb_color.ping.mockImplementationOnce(() => { throw new Error('failed'); @@ -184,13 +186,13 @@ describe('Availability', () => { }); it('Should reset ping timer when device last seen changes for passive device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(24)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.remote}); - expect(MQTT.publish).toHaveBeenCalledWith( + await mockZHEvents.lastSeenChanged({device: devices.remote}); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -205,10 +207,10 @@ describe('Availability', () => { }); it('Should immediately mark device as online when it lastSeen changes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(15)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -216,9 +218,9 @@ describe('Availability', () => { ); devices.bulb_color.lastSeen = Date.now(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -263,7 +265,7 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(9)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr}); + await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr}); await flushPromises(); await setTimeAndAdvanceTimers(utils.minutes(3)); @@ -274,7 +276,7 @@ describe('Availability', () => { await setTimeAndAdvanceTimers(utils.minutes(9)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb_color'})); await flushPromises(); await setTimeAndAdvanceTimers(utils.minutes(3)); @@ -302,18 +304,19 @@ describe('Availability', () => { }); it('Should retrieve device state when it reconnects', async () => { - //@ts-expect-error private + // @ts-expect-error private const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); - //@ts-expect-error private + // @ts-expect-error private controller.state.set(device, {state: 'OFF'}); const endpoint = devices.bulb_color.getEndpoint(1); + assert(endpoint); endpoint.read.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); await setTimeAndAdvanceTimers(utils.seconds(1)); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(0); @@ -323,7 +326,7 @@ describe('Availability', () => { expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); endpoint.read.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color}); + await mockZHEvents.deviceAnnounce({device: devices.bulb_color}); await flushPromises(); endpoint.read.mockImplementationOnce(() => { throw new Error(''); @@ -333,20 +336,20 @@ describe('Availability', () => { }); it('Should republish availability when device is renamed', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_color', to: 'bulb_new_name'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_color', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_new_name/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); await setTimeAndAdvanceTimers(utils.hours(12)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_new_name/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -357,10 +360,10 @@ describe('Availability', () => { it('Should publish availability payload in JSON format', async () => { await resetExtension(); devices.remote.ping.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, @@ -373,25 +376,25 @@ describe('Availability', () => { await resetExtension(); devices.bulb_color_2.ping.mockClear(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); devices.bulb_color_2.lastSeen = Date.now(); - await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}, @@ -400,17 +403,21 @@ describe('Availability', () => { }); it('Should clear the ping queue on stop', async () => { - //@ts-expect-error private - const availability = controller.extensions.find((extension) => extension instanceof Availability); + // @ts-expect-error private + const availability = controller.extensions.find((extension) => extension instanceof Availability)!; + // @ts-expect-error private const publishAvailabilitySpy = jest.spyOn(availability, 'publishAvailability'); devices.bulb_color.ping.mockImplementationOnce(() => new Promise((resolve) => setTimeout(resolve, 1000))); + // @ts-expect-error private availability.addToPingQueue(devices.bulb_color); + // @ts-expect-error private availability.addToPingQueue(devices.bulb_color_2); await availability.stop(); await setTimeAndAdvanceTimers(utils.minutes(1)); + // @ts-expect-error private expect(availability.pingQueue).toEqual([]); // Validate the stop-interrupt implicitly by checking that it prevents further function invocations expect(publishAvailabilitySpy).not.toHaveBeenCalled(); @@ -418,8 +425,8 @@ describe('Availability', () => { }); it('Should prevent instance restart', async () => { - //@ts-expect-error private - const availability = controller.extensions.find((extension) => extension instanceof Availability); + // @ts-expect-error private + const availability = controller.extensions.find((extension) => extension instanceof Availability)!; await availability.stop(); diff --git a/test/bind.test.js b/test/extensions/bind.test.ts similarity index 64% rename from test/bind.test.js rename to test/extensions/bind.test.ts index 9a65d41514..b29bb33096 100644 --- a/test/bind.test.js +++ b/test/extensions/bind.test.ts @@ -1,18 +1,32 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const stringify = require('json-stable-stringify-without-jsonify'); -jest.mock('debounce', () => jest.fn((fn) => fn)); -const debounce = require('debounce'); - -describe('Bind', () => { - let controller; - - const mockClear = (device) => { +import * as data from '../mocks/data'; +import {mockDebounce} from '../mocks/debounce'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {Device, devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [ + mockDebounce, + mockMQTT.publish, + devices.bulb_color.getEndpoint(1)!.configureReporting, + devices.bulb_color.getEndpoint(1)!.bind, + devices.bulb_color_2.getEndpoint(1)!.read, +]; + +describe('Extension: Bind', () => { + let controller: Controller; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'Bind'); + await controller.enableDisableExtension(true, 'Bind'); + }; + + const mockClear = (device: Device): void => { for (const endpoint of device.endpoints) { endpoint.read.mockClear(); endpoint.write.mockClear(); @@ -23,11 +37,6 @@ describe('Bind', () => { } }; - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'Bind'); - await controller.enableDisableExtension(true, 'Bind'); - }; - beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); @@ -38,13 +47,9 @@ describe('Bind', () => { beforeEach(async () => { data.writeDefaultConfiguration(); settings.reRead(); - zigbeeHerdsman.groups.group_1.members = []; - zigbeeHerdsman.devices.bulb_color.getEndpoint(1).configureReporting.mockClear(); - zigbeeHerdsman.devices.bulb_color.getEndpoint(1).bind.mockClear(); - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockClear(); - debounce.mockClear(); + groups.group_1.members = []; await resetExtension(); - MQTT.publish.mockClear(); + mocksClear.forEach((m) => m.mockClear()); }); afterAll(async () => { @@ -52,22 +57,22 @@ describe('Bind', () => { }); it('Should bind to device and configure reporting', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + device.getEndpoint(1)!.outputClusters = [...device.getEndpoint(1)!.outputClusters, 768]; const originalTargetBinds = target.binds; - target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); @@ -87,7 +92,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -98,37 +103,37 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Filters out unsupported clusters for reporting setup', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceInputClusters = device.getEndpoint(1).inputClusters; - device.getEndpoint(1).inputClusters = [...device.getEndpoint(1).inputClusters, 8]; - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceInputClusters = device.getEndpoint(1)!.inputClusters; + device.getEndpoint(1)!.inputClusters = [...device.getEndpoint(1)!.inputClusters, 8]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + device.getEndpoint(1)!.outputClusters = [...device.getEndpoint(1)!.outputClusters, 768]; const originalTargetInputClusters = target.inputClusters; target.inputClusters = [...originalTargetInputClusters]; target.inputClusters.splice(originalTargetInputClusters.indexOf(8), 1); // remove genLevelCtrl const originalTargetOutputClusters = target.outputClusters; target.outputClusters = [...target.outputClusters, 8]; const originalTargetBinds = target.binds; - target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); @@ -147,7 +152,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -158,27 +163,27 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.binds = originalTargetBinds; target.inputClusters = originalTargetInputClusters; target.outputClusters = originalTargetOutputClusters; - device.getEndpoint(1).inputClusters = originalDeviceInputClusters; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.inputClusters = originalDeviceInputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Filters out reporting setup based on bind status', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; // Setup - const originalDeviceOutputClusters = device.getEndpoint(1).outputClusters; - device.getEndpoint(1).outputClusters = [...device.getEndpoint(1).outputClusters, 768]; + const originalDeviceOutputClusters = device.getEndpoint(1)!.outputClusters; + device.getEndpoint(1)!.outputClusters = [...device.getEndpoint(1)!.outputClusters, 768]; const originalTargetBinds = target.binds; - target.binds = [{cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}]; - target.getClusterAttributeValue.mockImplementationOnce((cluster, value) => undefined); + target.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); mockClear(device); target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); @@ -194,7 +199,7 @@ describe('Bind', () => { }, ]; - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({transaction: '1234', from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint.bind).toHaveBeenCalledTimes(4); @@ -212,7 +217,7 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', @@ -223,24 +228,24 @@ describe('Bind', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); // Teardown target.configuredReportings = originalTargetCR; target.binds = originalTargetBinds; - device.getEndpoint(1).outputClusters = originalDeviceOutputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); it('Should bind only specified clusters', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ['genOnOff']})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color', clusters: ['genOnOff']})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -249,14 +254,14 @@ describe('Bind', () => { }); it('Should log error when there is nothing to bind', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; mockClear(device); - logger.error.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'})); + mockLogger.error.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: 'button', clusters: [], failed: []}, status: 'error', error: 'Nothing to bind'}), {retain: false, qos: 0}, @@ -265,27 +270,27 @@ describe('Bind', () => { }); it('Should unbind', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; // setup target.configureReporting.mockImplementationOnce(() => { throw new Error('timeout'); }); - const originalRemoteBinds = device.getEndpoint(1).binds; - device.getEndpoint(1).binds = []; + const originalRemoteBinds = device.getEndpoint(1)!.binds; + device.getEndpoint(1)!.binds = []; const originalTargetBinds = target.binds; target.binds = [ - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'lightingColorCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'lightingColorCtrl'}, target: devices.coordinator.getEndpoint(1)!}, ]; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); - delete zigbeeHerdsman.devices.bulb_color.meta.configured; - expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(undefined); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'bulb_color'})); + delete devices.bulb_color.meta.configured; + expect(devices.bulb_color.meta.configured).toBe(undefined); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); @@ -305,8 +310,8 @@ describe('Bind', () => { {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, ]); - expect(zigbeeHerdsman.devices.bulb_color.meta.configured).toBe(332242049); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(devices.bulb_color.meta.configured).toBe(332242049); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -315,22 +320,22 @@ describe('Bind', () => { // Teardown target.binds = originalTargetBinds; - device.getEndpoint(1).binds = originalRemoteBinds; + device.getEndpoint(1)!.binds = originalRemoteBinds; }); it('Should unbind coordinator', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.coordinator.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = devices.coordinator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.unbind.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'Coordinator'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'Coordinator'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -339,14 +344,14 @@ describe('Bind', () => { }); it('Should bind to groups', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'group_1'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'group_1'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); @@ -359,7 +364,7 @@ describe('Bind', () => { expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, ]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -368,7 +373,7 @@ describe('Bind', () => { // Should configure reporting for device added to group target1Member.configureReporting.mockClear(); - await MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'})); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb'})); await flushPromises(); expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ @@ -380,20 +385,20 @@ describe('Bind', () => { }); it('Should unbind from group', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1'})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target); expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -402,10 +407,10 @@ describe('Bind', () => { }); it('Should unbind from group with skip_disable_reporting=true', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb_2.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. @@ -413,12 +418,12 @@ describe('Bind', () => { endpoint.binds = []; target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, ]; target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: true})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: true})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); // with skip_disable_reporting set to false, we don't expect it to reconfigure reporting @@ -427,10 +432,10 @@ describe('Bind', () => { }); it('Should unbind from group with skip_disable_reporting=false', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const target1Member = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const target1Member = devices.bulb_2.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; target.members.push(target1Member); // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. @@ -438,12 +443,12 @@ describe('Bind', () => { endpoint.binds = []; target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, ]; target1Member.configureReporting.mockClear(); mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: false})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: 'group_1', skip_disable_reporting: false})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); // with skip_disable_reporting set, we expect it to reconfigure reporting @@ -458,17 +463,17 @@ describe('Bind', () => { }); it('Should bind to group by number', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.groups.group_1; - const endpoint = device.getEndpoint(1); + const device = devices.remote; + const target = groups.group_1; + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '1'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: '1'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -477,17 +482,17 @@ describe('Bind', () => { }); it('Should log when bind fails', async () => { - logger.error.mockClear(); - const device = zigbeeHerdsman.devices.remote; - const endpoint = device.getEndpoint(1); + mockLogger.error.mockClear(); + const device = devices.remote; + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.bind.mockImplementation(() => { throw new Error('failed'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color', clusters: [], failed: ['genScenes', 'genOnOff', 'genLevelCtrl']}, @@ -500,15 +505,15 @@ describe('Bind', () => { }); it('Should bind from non default endpoints', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG03LM.getEndpoint(3); - const endpoint = device.getEndpoint(2); + const device = devices.remote; + const target = devices.QBKG03LM.getEndpoint(3)!; + const endpoint = device.getEndpoint(2)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote/ep2', to: 'wall_switch_double/right', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -517,15 +522,15 @@ describe('Bind', () => { }); it('Should bind server clusters to client clusters', async () => { - const device = zigbeeHerdsman.devices.temperature_sensor; - const target = zigbeeHerdsman.devices.heating_actuator.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.temperature_sensor; + const target = devices.heating_actuator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'temperature_sensor', to: 'heating_actuator'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'temperature_sensor', to: 'heating_actuator'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('msTemperatureMeasurement', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -534,15 +539,15 @@ describe('Bind', () => { }); it('Should bind to default endpoint returned by endpoints()', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.QBKG04LM.getEndpoint(2); - const endpoint = device.getEndpoint(2); + const device = devices.remote; + const target = devices.QBKG04LM.getEndpoint(2)!; + const endpoint = device.getEndpoint(2)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, @@ -551,17 +556,17 @@ describe('Bind', () => { }); it('Should unbind from default_bind_group', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; const target = 'default_bind_group'; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: target})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/unbind', stringify({from: 'remote', to: target})); await flushPromises(); expect(endpoint.unbind).toHaveBeenCalledTimes(3); expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901); expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901); expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({ data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, @@ -573,13 +578,11 @@ describe('Bind', () => { }); it('Error bind fails when source device does not exist', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote_not_existing', to: 'bulb_color'}, @@ -592,13 +595,11 @@ describe('Bind', () => { }); it("Error bind fails when source device's endpoint does not exist", async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote/not_existing_endpoint', to: 'bulb_color'}, @@ -611,13 +612,11 @@ describe('Bind', () => { }); it('Error bind fails when target device or group does not exist', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color_not_existing'}, @@ -630,13 +629,11 @@ describe('Bind', () => { }); it("Error bind fails when target device's endpoint does not exist", async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const endpoint = device.getEndpoint(1); + const device = devices.remote; mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ data: {from: 'remote', to: 'bulb_color/not_existing_endpoint'}, @@ -649,79 +646,75 @@ describe('Bind', () => { }); it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { - const remote = zigbeeHerdsman.devices.remote; + const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; const payload = { data, cluster: 'manuSpecificPhilips', device: remote, - endpoint: remote.getEndpoint(2), + endpoint: remote.getEndpoint(2)!, type: 'commandHueNotification', linkquality: 10, groupID: 0, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(mockDebounce).toHaveBeenCalledTimes(1); + expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); }); it('Should poll bounded Hue bulb when receiving message from scene controller', async () => { - const remote = zigbeeHerdsman.devices.bj_scene_switch; + const remote = devices.bj_scene_switch; const data = {action: 'recall_2_row_1'}; - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => { + devices.bulb_color_2.getEndpoint(1)!.read.mockImplementationOnce(() => { throw new Error('failed'); }); const payload = { data, cluster: 'genScenes', device: remote, - endpoint: remote.getEndpoint(10), + endpoint: remote.getEndpoint(10)!, type: 'commandRecall', linkquality: 10, groupID: 0, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); // Calls to three clusters are expected in this case - expect(debounce).toHaveBeenCalledTimes(3); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('lightingColorCtrl', [ - 'currentX', - 'currentY', - 'colorTemperature', - ]); + expect(mockDebounce).toHaveBeenCalledTimes(3); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('lightingColorCtrl', ['currentX', 'currentY', 'colorTemperature']); }); it('Should poll grouped Hue bulb when receiving message from TRADFRI remote', async () => { - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockClear(); - zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read.mockClear(); - const remote = zigbeeHerdsman.devices.tradfri_remote; + devices.bulb_color_2.getEndpoint(1)!.read.mockClear(); + devices.bulb_2.getEndpoint(1)!.read.mockClear(); + const remote = devices.tradfri_remote; const data = {stepmode: 0, stepsize: 43, transtime: 5}; const payload = { data, cluster: 'genLevelCtrl', device: remote, - endpoint: remote.getEndpoint(1), + endpoint: remote.getEndpoint(1)!, type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(mockDebounce).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith('genOnOff', ['onOff']); // Should also only debounce once - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(4); + expect(mockDebounce).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(4); // Should only call Hue bulb, not e.g. tradfri - expect(zigbeeHerdsman.devices.bulb_2.getEndpoint(1).read).toHaveBeenCalledTimes(0); + expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); }); }); diff --git a/test/bridge.test.js b/test/extensions/bridge.test.ts similarity index 83% rename from test/bridge.test.js rename to test/extensions/bridge.test.ts index 7bd2ddfb5b..88a20ad4f9 100644 --- a/test/bridge.test.js +++ b/test/extensions/bridge.test.ts @@ -1,48 +1,52 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const fs = require('fs'); -const path = require('path'); -const flushPromises = require('./lib/flushPromises'); -const utils = require('../lib/util/utils').default; -const stringify = require('json-stable-stringify-without-jsonify'); - -const mockJSZipFile = jest.fn(); -const mockJSZipGenerateAsync = jest.fn().mockReturnValue('THISISBASE64'); - -jest.mock('jszip', () => - jest.fn().mockImplementation((path) => { - return { - file: mockJSZipFile, - generateAsync: mockJSZipGenerateAsync, - }; - }), -); - -const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM, zigfred_plus, bulb_custom_cluster} = - zigbeeHerdsman.devices; -zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); -zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr); -zigbeeHerdsman.returnDevices.push(WXKG11LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(remote.ieeeAddr); -zigbeeHerdsman.returnDevices.push(ZNCZ02LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb_color_2.ieeeAddr); -zigbeeHerdsman.returnDevices.push(WSDCGQ11LM.ieeeAddr); -zigbeeHerdsman.returnDevices.push(zigfred_plus.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb_custom_cluster.ieeeAddr); - -describe('Bridge', () => { - let controller; - let mockRestart; - let extension; - - let resetExtension = async () => { +import * as data from '../mocks/data'; +import {mockJSZipFile, mockJSZipGenerateAsync} from '../mocks/jszip'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises, JestMockAny} from '../mocks/utils'; +import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import type Bridge from '../../lib/extension/bridge'; + +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; +import utils from '../../lib/util/utils'; + +returnDevices.push(devices.coordinator.ieeeAddr); +returnDevices.push(devices.bulb.ieeeAddr); +returnDevices.push(devices.unsupported.ieeeAddr); +returnDevices.push(devices.WXKG11LM.ieeeAddr); +returnDevices.push(devices.remote.ieeeAddr); +returnDevices.push(devices.ZNCZ02LM.ieeeAddr); +returnDevices.push(devices.bulb_color_2.ieeeAddr); +returnDevices.push(devices.WSDCGQ11LM.ieeeAddr); +returnDevices.push(devices.zigfred_plus.ieeeAddr); +returnDevices.push(devices.bulb_custom_cluster.ieeeAddr); + +const mocksClear = [ + mockLogger.info, + mockLogger.warning, + mockMQTT.publish, + mockZHController.permitJoin, + devices.bulb.interview, + devices.bulb.removeFromDatabase, + devices.bulb.removeFromNetwork, +]; + +describe('Extension: Bridge', () => { + let controller: Controller; + let mockRestart: JestMockAny; + let extension: Bridge; + + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Bridge'); await controller.enableDisableExtension(true, 'Bridge'); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'Bridge'); }; @@ -52,26 +56,23 @@ describe('Bridge', () => { controller = new Controller(mockRestart, jest.fn()); await controller.start(); await flushPromises(); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'Bridge'); }); beforeEach(async () => { - MQTT.mock.reconnecting = false; + mockMQTT.reconnecting = false; data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultState(); - logger.info.mockClear(); - logger.warning.mockClear(); - logger.setTransportsEnabled(false); - MQTT.publish.mockClear(); - zigbeeHerdsman.permitJoin.mockClear(); - const device = zigbeeHerdsman.devices.bulb; - device.interview.mockClear(); - device.removeFromDatabase.mockClear(); - device.removeFromNetwork.mockClear(); - extension.lastJoinedDeviceIeeeAddr = null; + mocksClear.forEach((m) => m.mockClear()); + mockLogger.setTransportsEnabled(false); + // @ts-expect-error private + extension.lastJoinedDeviceIeeeAddr = undefined; + // @ts-expect-error private extension.restartRequired = false; - controller.state.state = {[zigbeeHerdsman.devices.bulb.ieeeAddr]: {brightness: 50}}; + // @ts-expect-error private + controller.state.state = {[devices.bulb.ieeeAddr]: {brightness: 50}}; }); afterAll(async () => { @@ -84,8 +85,8 @@ describe('Bridge', () => { const zhVersion = await utils.getDependencyVersion('zigbee-herdsman'); const zhcVersion = await utils.getDependencyVersion('zigbee-herdsman-converters'); const directory = settings.get().advanced.log_directory; - // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')[1]) + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', stringify({ commit: version.commitHash, @@ -209,7 +210,7 @@ describe('Bridge', () => { config_schema: settings.schema, coordinator: {ieee_address: '0x00124b00120144ae', meta: {revision: 20190425, version: 1}, type: 'z-Stack'}, log_level: 'info', - network: {channel: 15, extended_pan_id: [0, 11, 22], pan_id: 5674}, + network: {channel: 15, extended_pan_id: 0x001122, pan_id: 5674}, permit_join_timeout: 0, restart_required: false, version: version.version, @@ -223,27 +224,28 @@ describe('Bridge', () => { it('Should publish devices on startup', async () => { await resetExtension(); - // console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]); - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1]); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/devices', stringify([ { - date_code: null, + date_code: undefined, + // definition: undefined, disabled: false, endpoints: {1: {bindings: [], clusters: {input: [], output: []}, configured_reportings: [], scenes: []}}, friendly_name: 'Coordinator', ieee_address: '0x00124b00120144ae', interview_completed: false, interviewing: false, - model_id: null, + model_id: undefined, network_address: 0, - power_source: null, - software_build_id: null, + power_source: undefined, + software_build_id: undefined, supported: true, type: 'Coordinator', }, { - date_code: null, + date_code: undefined, definition: { description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', exposes: [ @@ -484,7 +486,7 @@ describe('Bridge', () => { model_id: 'TRADFRI bulb E27 WS opal 980lm', network_address: 40369, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, @@ -684,7 +686,7 @@ describe('Bridge', () => { type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'Hue dimmer switch', exposes: [ @@ -820,12 +822,12 @@ describe('Bridge', () => { model_id: 'RWL021', network_address: 6535, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Automatically generated definition', exposes: [ @@ -915,12 +917,12 @@ describe('Bridge', () => { model_id: 'notSupportedModelID', network_address: 6536, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: false, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Wireless mini switch', exposes: [ @@ -1037,12 +1039,12 @@ describe('Bridge', () => { model_id: 'lumi.sensor_switch.aq2', network_address: 6537, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Temperature and humidity sensor', exposes: [ @@ -1180,12 +1182,12 @@ describe('Bridge', () => { model_id: 'lumi.weather', network_address: 6539, power_source: 'Battery', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'EndDevice', }, { - date_code: null, + date_code: undefined, definition: { description: 'Mi smart plug', exposes: [ @@ -1329,12 +1331,12 @@ describe('Bridge', () => { model_id: 'lumi.plug', network_address: 6540, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'zigfred plus smart in-wall switch', exposes: [ @@ -1831,12 +1833,12 @@ describe('Bridge', () => { model_id: 'zigfred plus', network_address: 6589, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm', exposes: [ @@ -2065,11 +2067,11 @@ describe('Bridge', () => { ieee_address: '0x000b57fffec6a5c2', interview_completed: true, interviewing: false, - manufacturer: null, + manufacturer: undefined, model_id: 'TRADFRI bulb E27 WS opal 980lm', network_address: 40369, power_source: 'Mains (single phase)', - software_build_id: null, + software_build_id: undefined, supported: true, type: 'Router', }, @@ -2081,76 +2083,76 @@ describe('Bridge', () => { it('Should publish definitions on startup', async () => { await resetExtension(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); }); it('Should log to MQTT', async () => { - logger.setTransportsEnabled(true); - MQTT.publish.mockClear(); - logger.info.mockClear(); - logger.info('this is a test'); - logger.info('this is a test'); // Should not publish dupes - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); // Should not publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + mockMQTT.publish.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should log to MQTT including debug when enabled', async () => { settings.set(['advanced', 'log_debug_to_mqtt_frontend'], true); await resetExtension(); - logger.setTransportsEnabled(true); - MQTT.publish.mockClear(); - logger.info.mockClear(); - logger.info('this is a test'); - logger.info('this is a test'); // Should not publish dupes - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/logging', stringify({message: 'this is a test', level: 'info', namespace: 'z2m'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); // Should publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + mockMQTT.publish.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); settings.set(['advanced', 'log_debug_to_mqtt_frontend'], false); settings.reRead(); }); it('Shouldnt log to MQTT when not connected', async () => { - logger.setTransportsEnabled(true); - MQTT.mock.reconnecting = true; - MQTT.publish.mockClear(); - logger.info.mockClear(); - logger.error.mockClear(); - logger.info('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledTimes(0); + mockLogger.setTransportsEnabled(true); + mockMQTT.reconnecting = true; + mockMQTT.publish.mockClear(); + mockLogger.info.mockClear(); + mockLogger.error.mockClear(); + mockLogger.info('this is a test'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.info).toHaveBeenCalledTimes(1); + expect(mockLogger.error).toHaveBeenCalledTimes(0); }); it('Should publish groups on startup', async () => { await resetExtension(); - logger.setTransportsEnabled(true); - expect(MQTT.publish).toHaveBeenCalledWith( + mockLogger.setTransportsEnabled(true); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', stringify([ {friendly_name: 'group_1', id: 1, members: [], scenes: []}, @@ -2178,10 +2180,10 @@ describe('Bridge', () => { }); it('Should publish event when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2190,18 +2192,18 @@ describe('Bridge', () => { }); it('Should publish devices when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceNetworkAddressChanged({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event when device announces', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2210,11 +2212,11 @@ describe('Bridge', () => { }); it('Should publish event when device interview started', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'started'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'started'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, @@ -2223,27 +2225,27 @@ describe('Bridge', () => { }); it('Should publish event and devices when device interview failed', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'failed'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'failed'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should publish event and devices when device interview successful', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'successful'}); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.unsupported, status: 'successful'}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'successful'}); + await mockZHEvents.deviceInterview({device: devices.unsupported, status: 'successful'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(7); - // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(7); + // console.log(mockMQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2468,7 +2470,7 @@ describe('Bridge', () => { {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2554,23 +2556,28 @@ describe('Bridge', () => { {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/definitions', + expect.any(String), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Should publish event and devices when device leaves', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb.ieeeAddr}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_leave', data: {ieee_address: '0x000b57fffec6a5b2', friendly_name: 'bulb'}}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( // Defintitions should be updated on device event 'zigbee2mqtt/bridge/definitions', expect.any(String), @@ -2580,11 +2587,11 @@ describe('Bridge', () => { }); it('Should allow permit join on all', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, @@ -2593,11 +2600,11 @@ describe('Bridge', () => { }); it('Should disallow permit join on all', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(0, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(0, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 0}, status: 'ok'}), {retain: false, qos: 0}, @@ -2606,11 +2613,11 @@ describe('Bridge', () => { }); it('Should allow permit join with number string (automatically on all)', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', '1'); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', '1'); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(1, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), {retain: false, qos: 0}, @@ -2619,10 +2626,10 @@ describe('Bridge', () => { }); it('Should not allow permit join with invalid payload', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2631,28 +2638,33 @@ describe('Bridge', () => { }); it('Should republish bridge info when permit join changes', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); + mockMQTT.publish.mockClear(); + await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Shouldnt republish bridge info when permit join changes and hersman is stopping', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.isStopping.mockImplementationOnce(() => true); - await zigbeeHerdsman.events.permitJoinChanged({permitted: false, timeout: 10}); + mockMQTT.publish.mockClear(); + mockZHController.isStopping.mockImplementationOnce(() => true); + await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10}); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/info', + expect.any(String), + {retain: true, qos: 0}, + expect.any(Function), + ); }); it('Should allow permit join via device', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb'})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(123, device); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(123, device); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 123, device: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, @@ -2661,11 +2673,11 @@ describe('Bridge', () => { }); it('Should not allow permit join via non-existing device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 123, device: 'bulb_not_existing_woeeee'})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing_woeeee' does not exist"}), {retain: false, qos: 0}, @@ -2674,10 +2686,10 @@ describe('Bridge', () => { }); it('Should put transaction in response when request is done with transaction', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0, transaction: 22})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0, transaction: 22})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 0}, status: 'ok', transaction: 22}), {retain: false, qos: 0}, @@ -2686,13 +2698,13 @@ describe('Bridge', () => { }); it('Should put error in response when request fails', async () => { - zigbeeHerdsman.permitJoin.mockImplementationOnce(() => { + mockZHController.permitJoin.mockImplementationOnce(() => { throw new Error('Failed to connect to adapter'); }); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: 'Failed to connect to adapter'}), {retain: false, qos: 0}, @@ -2701,10 +2713,10 @@ describe('Bridge', () => { }); it('Should put error in response when format is incorrect', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: false})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: false})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2713,10 +2725,10 @@ describe('Bridge', () => { }); it('Coverage satisfaction', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); - const device = zigbeeHerdsman.devices.bulb; - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); + const device = devices.bulb; + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device, @@ -2728,10 +2740,10 @@ describe('Bridge', () => { }); it('Should allow a healthcheck', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/health_check', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/health_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/health_check', stringify({data: {healthy: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2740,11 +2752,11 @@ describe('Bridge', () => { }); it('Should allow a coordinator check', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.coordinatorCheck.mockReturnValueOnce({missingRouters: [zigbeeHerdsman.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); - MQTT.events.message('zigbee2mqtt/bridge/request/coordinator_check', ''); + mockMQTT.publish.mockClear(); + mockZHController.coordinatorCheck.mockReturnValueOnce({missingRouters: [mockZHController.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/coordinator_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/coordinator_check', stringify({data: {missing_routers: [{friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}]}, status: 'ok'}), {retain: false, qos: 0}, @@ -2753,7 +2765,7 @@ describe('Bridge', () => { }); it('Should allow to remove device by string', async () => { - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['groups'], { 1: { friendly_name: 'group_1', @@ -2770,16 +2782,17 @@ describe('Bridge', () => { ], }, }); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); await flushPromises(); + // @ts-expect-error private expect(controller.state[device.ieeeAddr]).toBeUndefined(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(device.removeFromDatabase).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2787,20 +2800,20 @@ describe('Bridge', () => { ); expect(settings.get().blocklist).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual(['0x999b57fffec6a5b9/1', 'other_bulb', 'bulb_1', 'bulb/room/2']); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); }); it('Should allow to remove device by object ID', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(device.removeFromDatabase).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2809,15 +2822,15 @@ describe('Bridge', () => { }); it('Should allow to force remove device', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', force: true})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(device.removeFromNetwork).not.toHaveBeenCalled(); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2826,14 +2839,14 @@ describe('Bridge', () => { }); it('Should allow to block device', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', block: true, force: true})); + const device = devices.bulb; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb', block: true, force: true})); await flushPromises(); expect(device.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getDevice('bulb')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: true, force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2843,14 +2856,14 @@ describe('Bridge', () => { }); it('Should allow to remove group', async () => { - const group = zigbeeHerdsman.groups.group_1; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', 'group_1'); + const group = groups.group_1; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', 'group_1'); await flushPromises(); expect(group.removeFromNetwork).toHaveBeenCalledTimes(1); expect(settings.getGroup('group_1')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2859,14 +2872,14 @@ describe('Bridge', () => { }); it('Should allow to force remove group', async () => { - const group = zigbeeHerdsman.groups.group_1; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'group_1', force: true})); + const group = groups.group_1; + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'group_1', force: true})); await flushPromises(); expect(group.removeFromDatabase).toHaveBeenCalledTimes(1); expect(settings.getGroup('group_1')).toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -2876,21 +2889,20 @@ describe('Bridge', () => { it('Should allow to add and remove from blocklist', async () => { expect(settings.get().blocklist).toStrictEqual([]); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123', '0x1234']}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123', '0x1234']}})); await flushPromises(); expect(settings.get().blocklist).toStrictEqual(['0x123', '0x1234']); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123']}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {blocklist: ['0x123']}})); await flushPromises(); expect(settings.get().blocklist).toStrictEqual(['0x123']); }); it('Should throw error on removing non-existing device', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {}, status: 'error', error: "Device 'non-existing-device' does not exist"}), {retain: false, qos: 0}, @@ -2899,14 +2911,14 @@ describe('Bridge', () => { }); it('Should throw error when remove device fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); + const device = devices.bulb; + mockMQTT.publish.mockClear(); device.removeFromNetwork.mockImplementationOnce(() => { throw new Error('device timeout'); }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {}, status: 'error', error: "Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}), {retain: false, qos: 0}, @@ -2915,8 +2927,8 @@ describe('Bridge', () => { }); it('Should allow rename device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeUndefined(); expect(settings.getDevice('bulb_new_name')).toStrictEqual({ @@ -2925,10 +2937,15 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name', stringify({brightness: 50}), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_new_name', + stringify({brightness: 50}), + expect.any(Object), + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2937,10 +2954,10 @@ describe('Bridge', () => { }); it('Shouldnt allow rename device with to not allowed name containing a wildcard', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'living_room/blinds#'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'living_room/blinds#'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: "MQTT wildcard (+ and #) not allowed in friendly_name ('living_room/blinds#')"}), {retain: false, qos: 0}, @@ -2949,13 +2966,13 @@ describe('Bridge', () => { }); it('Should allow rename group', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); await flushPromises(); expect(settings.getGroup('group_1')).toBeUndefined(); expect(settings.getGroup('group_new_name')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_new_name', retain: false}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/rename', stringify({data: {from: 'group_1', to: 'group_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -2964,10 +2981,10 @@ describe('Bridge', () => { }); it('Should throw error on invalid device rename payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from_bla: 'bulb', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from_bla: 'bulb', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -2976,10 +2993,10 @@ describe('Bridge', () => { }); it('Should throw error on non-existing device rename', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_not_existing', to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_not_existing', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, @@ -2988,9 +3005,9 @@ describe('Bridge', () => { }); it('Should allow to rename last joined device', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); expect(settings.getDevice('bulb')).toBeUndefined(); expect(settings.getDevice('bulb_new_name')).toStrictEqual({ @@ -2999,8 +3016,8 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3009,10 +3026,10 @@ describe('Bridge', () => { }); it('Should throw error when renaming device through not allowed friendlyName', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name/1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: `Friendly name cannot end with a "/DIGIT" ('bulb_new_name/1')`}), {retain: false, qos: 0}, @@ -3021,10 +3038,10 @@ describe('Bridge', () => { }); it('Should throw error when renaming last joined device but none has joined', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'No device has joined since start'}), {retain: false, qos: 0}, @@ -3033,12 +3050,12 @@ describe('Bridge', () => { }); it('Should allow interviewing a device by friendly name', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(devices.bulb.interview).toHaveBeenCalled(); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3046,20 +3063,22 @@ describe('Bridge', () => { ); // The following indicates that devices have published. - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should allow interviewing a device by ieeeAddr', async () => { - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; + assert('resolveDefinition' in device); device.resolveDefinition = jest.fn(); - MQTT.publish.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockClear(); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); expect(device.resolveDefinition).toHaveBeenCalledTimes(0); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: '0x000b57fffec6a5b2'})); await flushPromises(); - expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalledWith(true); + expect(devices.bulb.interview).toHaveBeenCalledWith(true); expect(device.resolveDefinition).toHaveBeenCalledWith(true); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: '0x000b57fffec6a5b2'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3067,14 +3086,14 @@ describe('Bridge', () => { ); // The following indicates that devices have published. - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should throw error on invalid device interview payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3083,10 +3102,10 @@ describe('Bridge', () => { }); it('Should throw error on non-existing device interview', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, @@ -3095,10 +3114,10 @@ describe('Bridge', () => { }); it('Should throw error on id is device endpoint', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb/1' does not exist"}), {retain: false, qos: 0}, @@ -3107,10 +3126,10 @@ describe('Bridge', () => { }); it('Should throw error on id is a group', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'group_1' does not exist"}), {retain: false, qos: 0}, @@ -3119,12 +3138,12 @@ describe('Bridge', () => { }); it('Should throw error on when interview fails', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockClear(); - zigbeeHerdsman.devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))); - MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + mockMQTT.publish.mockClear(); + devices.bulb.interview.mockClear(); + devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}), {retain: false, qos: 0}, @@ -3133,10 +3152,10 @@ describe('Bridge', () => { }); it('Should error when generate_external_definition is invalid', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: ZNCZ02LM.ieeeAddr})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, @@ -3145,10 +3164,10 @@ describe('Bridge', () => { }); it('Should error when generate_external_definition requested for unknown device', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: 'non_existing_device'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: 'non_existing_device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({data: {}, error: "Device 'non_existing_device' does not exist", status: 'error'}), {retain: false, qos: 0}, @@ -3157,10 +3176,10 @@ describe('Bridge', () => { }); it('Should allow to generate device definition', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: ZNCZ02LM.ieeeAddr})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({ data: { @@ -3187,14 +3206,14 @@ describe('Bridge', () => { }); it('Should allow change device options', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', friendly_name: 'bulb', retain: true, description: 'this is my bulb', }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {retain: false, transition: 1}, id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {retain: false, transition: 1}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3203,7 +3222,7 @@ describe('Bridge', () => { transition: 1, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3220,7 +3239,7 @@ describe('Bridge', () => { }); it('Should allow to remove device option', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); settings.set(['devices', '0x000b57fffec6a5b2', 'qos'], 1); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3229,7 +3248,7 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {qos: null}, id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {qos: null}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3237,7 +3256,7 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3254,14 +3273,14 @@ describe('Bridge', () => { }); it('Should allow change device options with restart required', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', friendly_name: 'bulb', retain: true, description: 'this is my bulb', }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {disabled: true}, id: 'bulb'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options: {disabled: true}, id: 'bulb'})); await flushPromises(); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3270,7 +3289,7 @@ describe('Bridge', () => { disabled: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3287,12 +3306,12 @@ describe('Bridge', () => { }); it('Should allow change group options', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); - MQTT.events.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {retain: true, transition: 1}, id: 'group_1'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {retain: true, transition: 1}, id: 'group_1'})); await flushPromises(); expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: true, transition: 1}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/options', stringify({data: {from: {retain: false}, to: {retain: true, transition: 1}, restart_required: false, id: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3301,9 +3320,9 @@ describe('Bridge', () => { }); it('Should allow change group options with restart required', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); - MQTT.events.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {off_state: 'all_members_off'}, id: 'group_1'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {off_state: 'all_members_off'}, id: 'group_1'})); await flushPromises(); expect(settings.getGroup('group_1')).toStrictEqual({ ID: 1, @@ -3312,7 +3331,7 @@ describe('Bridge', () => { retain: false, off_state: 'all_members_off', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/options', stringify({ data: {from: {retain: false}, to: {retain: false, off_state: 'all_members_off'}, restart_required: true, id: 'group_1'}, @@ -3324,10 +3343,10 @@ describe('Bridge', () => { }); it('Should throw error on invalid device change options payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/options', stringify({options_: {retain: true, transition: 1}, id: 'bulb'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options_: {retain: true, transition: 1}, id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3336,12 +3355,12 @@ describe('Bridge', () => { }); it('Should allow to add group by string', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); await flushPromises(); expect(settings.getGroup('group_193')).toStrictEqual({ID: 3, devices: [], friendly_name: 'group_193'}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 3}, status: 'ok'}), {retain: false, qos: 0}, @@ -3350,12 +3369,12 @@ describe('Bridge', () => { }); it('Should allow to add group with ID', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: 'group_193', id: 92})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: 'group_193', id: 92})); await flushPromises(); expect(settings.getGroup('group_193')).toStrictEqual({ID: 92, devices: [], friendly_name: 'group_193'}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 92}, status: 'ok'}), {retain: false, qos: 0}, @@ -3364,10 +3383,10 @@ describe('Bridge', () => { }); it('Shouldnt allow to add group with empty name', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: '', id: 9})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: '', id: 9})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {}, status: 'error', error: 'friendly_name must be at least 1 char long'}), {retain: false, qos: 0}, @@ -3376,10 +3395,10 @@ describe('Bridge', () => { }); it('Should throw error when add with invalid payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: 'group_193'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: 'group_193'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3388,13 +3407,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset (succeeds)', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(true); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'ok'}), {retain: false, qos: 0}, @@ -3403,14 +3422,14 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset specific device', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryReset.mockClear(); - zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', stringify({ieee_address: '0x1239', channel: 12})); - await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledWith('0x1239', 12); - expect(MQTT.publish).toHaveBeenCalledWith( + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryReset.mockClear(); + mockZHController.touchlinkFactoryReset.mockReturnValueOnce(true); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', stringify({ieee_address: '0x1239', channel: 12})); + await flushPromises(); + expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledWith('0x1239', 12); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, @@ -3419,15 +3438,15 @@ describe('Bridge', () => { }); it('Add install code', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // By object - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', stringify({value: 'my-code'})); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', stringify({value: 'my-code'})); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-code'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1); + expect(mockZHController.addInstallCode).toHaveBeenCalledWith('my-code'); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3435,12 +3454,12 @@ describe('Bridge', () => { ); // By string - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', 'my-string-code'); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', 'my-string-code'); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledWith('my-string-code'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(1); + expect(mockZHController.addInstallCode).toHaveBeenCalledWith('my-string-code'); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3449,12 +3468,12 @@ describe('Bridge', () => { }); it('Add install code error', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.addInstallCode.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/install_code/add', stringify({wrong: 'my-code'})); + mockMQTT.publish.mockClear(); + mockZHController.addInstallCode.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/install_code/add', stringify({wrong: 'my-code'})); await flushPromises(); - expect(zigbeeHerdsman.addInstallCode).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.addInstallCode).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3463,13 +3482,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink identify specific device', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkIdentify.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239', channel: 12})); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkIdentify.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239', channel: 12})); await flushPromises(); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledWith('0x1239', 12); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledWith('0x1239', 12); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, @@ -3478,12 +3497,12 @@ describe('Bridge', () => { }); it('Touchlink identify fails when payload is invalid', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkIdentify.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239'})); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkIdentify.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/identify', stringify({ieee_address: '0x1239'})); await flushPromises(); - expect(zigbeeHerdsman.touchlinkIdentify).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3492,13 +3511,13 @@ describe('Bridge', () => { }); it('Should allow to touchlink factory reset (fails)', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockClear(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(false); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); + mockMQTT.publish.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockClear(); + mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(false); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'error', error: 'Failed to factory reset device through Touchlink'}), {retain: false, qos: 0}, @@ -3507,16 +3526,16 @@ describe('Bridge', () => { }); it('Should allow to touchlink scan', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkScan.mockClear(); - zigbeeHerdsman.touchlinkScan.mockReturnValueOnce([ + mockMQTT.publish.mockClear(); + mockZHController.touchlinkScan.mockClear(); + mockZHController.touchlinkScan.mockReturnValueOnce([ {ieeeAddr: '0x123', channel: 12}, {ieeeAddr: '0x124', channel: 24}, ]); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/scan', ''); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/touchlink/scan', ''); await flushPromises(); - expect(zigbeeHerdsman.touchlinkScan).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.touchlinkScan).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/scan', stringify({ data: { @@ -3533,11 +3552,11 @@ describe('Bridge', () => { }); it('Should allow to configure reporting', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: '0x000b57fffec6a5b2/1', @@ -3550,14 +3569,14 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinator.endpoints[0]); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', devices.coordinator.endpoints[0]); expect(endpoint.configureReporting).toHaveBeenCalledTimes(1); expect(endpoint.configureReporting).toHaveBeenCalledWith( 'genLevelCtrl', [{attribute: 'currentLevel', maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}], undefined, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({ data: { @@ -3573,15 +3592,15 @@ describe('Bridge', () => { {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); }); it('Should throw error when configure reporting is called with malformed payload', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'bulb', @@ -3594,7 +3613,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, @@ -3603,11 +3622,11 @@ describe('Bridge', () => { }); it('Should throw error when configure reporting is called for non-existing device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'non_existing_device', @@ -3620,7 +3639,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: "Device 'non_existing_device' does not exist"}), {retain: false, qos: 0}, @@ -3629,11 +3648,11 @@ describe('Bridge', () => { }); it('Should throw error when configure reporting is called for non-existing endpoint', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; endpoint.configureReporting.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: '0x000b57fffec6a5b2/non_existing_endpoint', @@ -3646,7 +3665,7 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}), {retain: false, qos: 0}, @@ -3661,10 +3680,10 @@ describe('Bridge', () => { fs.writeFileSync(path.join(data.mockDir, 'log', 'log.log'), 'test123'); fs.mkdirSync(path.join(data.mockDir, 'ext_converters', '123')); fs.writeFileSync(path.join(data.mockDir, 'ext_converters', '123', 'myfile.js'), 'test123'); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/backup', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/backup', ''); await flushPromises(); - expect(zigbeeHerdsman.backup).toHaveBeenCalledTimes(1); + expect(mockZHController.backup).toHaveBeenCalledTimes(1); expect(mockJSZipFile).toHaveBeenCalledTimes(4); expect(mockJSZipFile).toHaveBeenNthCalledWith(1, 'configuration.yaml', expect.any(Object)); expect(mockJSZipFile).toHaveBeenNthCalledWith(2, path.join('ext_converters', '123', 'myfile.js'), expect.any(Object)); @@ -3672,7 +3691,7 @@ describe('Bridge', () => { expect(mockJSZipFile).toHaveBeenNthCalledWith(4, 'state.json', expect.any(Object)); expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1); expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: 'base64'}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/backup', stringify({data: {zip: 'THISISBASE64'}, status: 'ok'}), {retain: false, qos: 0}, @@ -3681,12 +3700,12 @@ describe('Bridge', () => { }); it('Should allow to restart', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/restart', ''); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/restart', ''); await flushPromises(); jest.runOnlyPendingTimers(); expect(mockRestart).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/restart', stringify({data: {}, status: 'ok'}), {retain: false, qos: 0}, @@ -3695,13 +3714,15 @@ describe('Bridge', () => { }); it('Change options and apply - homeassistant', async () => { + // @ts-expect-error private expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).toBeUndefined(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: true}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: true}})); await flushPromises(); + // @ts-expect-error private expect(controller.extensions.find((e) => e.constructor.name === 'HomeAssistant')).not.toBeUndefined(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3710,13 +3731,13 @@ describe('Bridge', () => { }); it('Change options and apply - log_level', async () => { - logger.setLevel('info'); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 'debug'}}})); + mockLogger.setLevel('info'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 'debug'}}})); await flushPromises(); - expect(logger.getLevel()).toStrictEqual('debug'); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.getLevel()).toStrictEqual('debug'); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3725,13 +3746,13 @@ describe('Bridge', () => { }); it('Change options and apply - log_debug_namespace_ignore', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const nsIgnore = '^zhc:legacy:fz:(tuya|moes)|^zh:ember:uart:|^zh:controller'; - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}})); await flushPromises(); - expect(logger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, @@ -3740,37 +3761,37 @@ describe('Bridge', () => { }); it('Change options and apply - log_namespaced_levels', async () => { - logger.setLevel('info'); + mockLogger.setLevel('info'); settings.apply({advanced: {log_namespaced_levels: {'zh:zstack': 'warning', 'z2m:mqtt': 'debug'}}}); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: {'z2m:mqtt': 'warning', 'zh:zstack': null}}}}), ); await flushPromises(); expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({'z2m:mqtt': 'warning'}); - expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockLogger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'}); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: {'z2m:mqtt': null}}}})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_namespaced_levels: {'z2m:mqtt': null}}}})); await flushPromises(); expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({}); - expect(logger.getNamespacedLevels()).toStrictEqual({}); + expect(mockLogger.getNamespacedLevels()).toStrictEqual({}); }); it('Change options restart required', async () => { settings.apply({serial: {port: '123'}}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); await flushPromises(); expect(settings.get().serial.port).toBe('/dev/newport'); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3780,14 +3801,14 @@ describe('Bridge', () => { it('Change options array', async () => { expect(settings.get().advanced.ext_pan_id).toStrictEqual([221, 221, 221, 221, 221, 221, 221, 221]); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {ext_pan_id: [220, 221, 221, 221, 221, 221, 221, 221]}}}), ); await flushPromises(); expect(settings.get().advanced.ext_pan_id).toStrictEqual([220, 221, 221, 221, 221, 221, 221, 221]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3797,11 +3818,11 @@ describe('Bridge', () => { it('Change options with null', async () => { expect(settings.get().serial).toStrictEqual({disable_led: false, port: '/dev/dummy'}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {disable_led: false, port: null}}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {disable_led: false, port: null}}})); await flushPromises(); expect(settings.get().serial).toStrictEqual({disable_led: false}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, @@ -3810,10 +3831,10 @@ describe('Bridge', () => { }); it('Change options invalid payload', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, @@ -3822,10 +3843,10 @@ describe('Bridge', () => { }); it('Change options not valid against schema', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {external_converters: 'true'}})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, error: 'external_converters must be array', status: 'error'}), {retain: false, qos: 0}, @@ -3834,68 +3855,84 @@ describe('Bridge', () => { }); it('Icon link handling', async () => { - const bridge = controller.extensions.find((e) => e.constructor.name === 'Bridge'); + // @ts-expect-error private + const bridge: Bridge = controller.extensions.find((e) => e.constructor.name === 'Bridge'); expect(bridge).not.toBeUndefined(); - const definition = {model: 'lumi.plug', fromZigbee: []}; - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const definition = { + fingerprint: [], + model: 'lumi.plug', + vendor: 'abcd', + description: 'abcd', + toZigbee: [], + fromZigbee: [], + exposes: [], + icon: '', + }; + const device = devices.ZNCZ02LM; const svg_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo'; const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg'; definition.icon = icon_link; + // @ts-expect-error bare minimum mock let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(icon_link); definition.icon = icon_link; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {icon: svg_icon}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); definition.icon = '_${model}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_'); definition.icon = '_${model}_${zigbeeModel}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_lumi.plug_lumi.plug_'); definition.icon = svg_icon; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe(svg_icon); device.modelID = '?._Z\\NC+Z02*LM'; definition.model = '&&&&*+'; definition.icon = '_${model}_${zigbeeModel}_'; + // @ts-expect-error bare minimum mock payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}}); - expect(payload).not.toBeUndefined(); + assert(payload); expect(payload['icon']).not.toBeUndefined(); expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_'); }); it('Should publish bridge info, devices and definitions when a device with custom_clusters joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster}); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster}); await flushPromises(); - // console.log(MQTT.publish.mock.calls); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({data: {friendly_name: '0x000b57fffec6a5c2', ieee_address: '0x000b57fffec6a5c2'}, type: 'device_joined'}), {retain: false, qos: 0}, @@ -3905,25 +3942,25 @@ describe('Bridge', () => { it('Should publish bridge info, devices and definitions when a device with custom_clusters is reconfigured', async () => { // Adding a device first - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster}); + await mockZHEvents.deviceJoined({device: devices.bulb_custom_cluster}); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // After cleaning, reconfigure it - MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', zigbeeHerdsman.devices.bulb_custom_cluster.ieeeAddr); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', devices.bulb_custom_cluster.ieeeAddr); await flushPromises(); - // console.log(MQTT.publish.mock.calls); - expect(MQTT.publish).toHaveBeenCalledTimes(4); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), + expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', expect.any(String), {retain: false, qos: 0}, diff --git a/test/configure.test.js b/test/extensions/configure.test.ts similarity index 63% rename from test/configure.test.js rename to test/extensions/configure.test.ts index d5a700deb3..4ab7bba995 100644 --- a/test/configure.test.js +++ b/test/extensions/configure.test.ts @@ -1,81 +1,83 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -const stringify = require('json-stable-stringify-without-jsonify'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -describe('Configure', () => { - let controller; - let coordinatorEndpoint; - - const expectRemoteConfigured = () => { - const device = zigbeeHerdsman.devices.remote; - const endpoint1 = device.getEndpoint(1); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {Device, devices, Endpoint, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Configure', () => { + let controller: Controller; + let coordinatorEndpoint: Endpoint; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'Configure'); + await controller.enableDisableExtension(true, 'Configure'); + }; + + const mockClear = (device: Device): void => { + for (const endpoint of device.endpoints) { + endpoint.read.mockClear(); + endpoint.write.mockClear(); + endpoint.configureReporting.mockClear(); + endpoint.bind.mockClear(); + } + }; + + const expectRemoteConfigured = (): void => { + const device = devices.remote; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.bind).toHaveBeenCalledTimes(2); expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); - const endpoint2 = device.getEndpoint(2); + const endpoint2 = device.getEndpoint(2)!; expect(endpoint2.write).toHaveBeenCalledTimes(1); expect(endpoint2.write).toHaveBeenCalledWith('genBasic', {49: {type: 25, value: 11}}, {disableDefaultResponse: true, manufacturerCode: 4107}); expect(device.meta.configured).toBe(332242049); }; - const expectBulbConfigured = () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint1 = device.getEndpoint(1); + const expectBulbConfigured = (): void => { + const device = devices.bulb; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.read).toHaveBeenCalledTimes(2); expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); expect(endpoint1.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorTempPhysicalMin', 'colorTempPhysicalMax']); }; - const expectBulbNotConfigured = () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint1 = device.getEndpoint(1); + const expectBulbNotConfigured = (): void => { + const device = devices.bulb; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.read).toHaveBeenCalledTimes(0); }; - const expectRemoteNotConfigured = () => { - const device = zigbeeHerdsman.devices.remote; - const endpoint1 = device.getEndpoint(1); + const expectRemoteNotConfigured = (): void => { + const device = devices.remote; + const endpoint1 = device.getEndpoint(1)!; expect(endpoint1.bind).toHaveBeenCalledTimes(0); }; - const mockClear = (device) => { - for (const endpoint of device.endpoints) { - endpoint.read.mockClear(); - endpoint.write.mockClear(); - endpoint.configureReporting.mockClear(); - endpoint.bind.mockClear(); - } - }; - - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'Configure'); - await controller.enableDisableExtension(true, 'Configure'); - }; + const wait = async (ms: number): Promise => await new Promise((resolve) => setTimeout(resolve, ms)); beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); + await jest.runOnlyPendingTimersAsync(); }); beforeEach(async () => { data.writeDefaultConfiguration(); settings.reRead(); mocksClear.forEach((m) => m.mockClear()); - coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + coordinatorEndpoint = devices.coordinator.getEndpoint(1)!; await resetExtension(); - await jest.runOnlyPendingTimers(); + await jest.runOnlyPendingTimersAsync(); }); afterAll(async () => { @@ -92,32 +94,33 @@ describe('Configure', () => { it('Should re-configure when device rejoins', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; await flushPromises(); mockClear(device); const payload = {device}; - zigbeeHerdsman.events.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); await flushPromises(); expectBulbConfigured(); }); it('Should not re-configure disabled devices', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; await flushPromises(); mockClear(device); settings.set(['devices', device.ieeeAddr, 'disabled'], true); - zigbeeHerdsman.events.deviceJoined({device}); + mockZHEvents.deviceJoined({device}); await flushPromises(); expectBulbNotConfigured(); }); it('Should reconfigure reporting on reconfigure event', async () => { expectBulbConfigured(); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; mockClear(device.zh); expectBulbNotConfigured(); + // @ts-expect-error private controller.eventBus.emitReconfigure({device}); await flushPromises(); expectBulbConfigured(); @@ -125,29 +128,29 @@ describe('Configure', () => { it('Should not configure twice', async () => { expectBulbConfigured(); - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; mockClear(device); - await zigbeeHerdsman.events.deviceInterview({device}); + await mockZHEvents.deviceInterview({device}); await flushPromises(); expectBulbNotConfigured(); }); it('Should configure on zigbee message when not configured yet', async () => { - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; delete device.meta.configured; mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expectBulbConfigured(); }); it('Should allow to configure via MQTT', async () => { - mockClear(zigbeeHerdsman.devices.remote); + mockClear(devices.remote); expectRemoteNotConfigured(); - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', 'remote'); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', 'remote'); await flushPromises(); expectRemoteConfigured(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: 'remote'}, status: 'ok'}), {retain: false, qos: 0}, @@ -156,9 +159,9 @@ describe('Configure', () => { }); it('Fail to configure via MQTT when device does not exist', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'})); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: 'not_existing_device'}, status: 'error', error: "Device 'not_existing_device' does not exist"}), {retain: false, qos: 0}, @@ -167,12 +170,12 @@ describe('Configure', () => { }); it('Fail to configure via MQTT when configure fails', async () => { - zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => { + devices.remote.getEndpoint(1)!.bind.mockImplementationOnce(async () => { throw new Error('Bind timeout after 10s'); }); - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'})); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: 'remote'}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}), {retain: false, qos: 0}, @@ -181,9 +184,9 @@ describe('Configure', () => { }); it('Fail to configure via MQTT when device has no configure', async () => { - await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20})); + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: '0x0017882104a44559'}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}), {retain: false, qos: 0}, @@ -192,61 +195,61 @@ describe('Configure', () => { }); it('Should not configure when interview not completed', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; device.interviewCompleted = false; - const endpoint = device.getEndpoint(1); mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expectRemoteNotConfigured(); device.interviewCompleted = true; }); it('Should not configure when already configuring', async () => { - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; endpoint.bind.mockImplementationOnce(async () => await wait(500)); mockClear(device); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); }); it('Should configure max 3 times when fails', async () => { + // @ts-expect-error private controller.extensions.find((e) => e.constructor.name === 'Configure').attempts = {}; - const device = zigbeeHerdsman.devices.remote; + const device = devices.remote; delete device.meta.configured; - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; mockClear(device); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); endpoint.bind.mockImplementationOnce(async () => { throw new Error('BLA'); }); - await zigbeeHerdsman.events.lastSeenChanged({device}); + await mockZHEvents.lastSeenChanged({device}); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); }); diff --git a/test/externalConverters.test.js b/test/extensions/externalConverters.test.ts similarity index 57% rename from test/externalConverters.test.js rename to test/extensions/externalConverters.test.ts index eec2fe4bc4..e5ba4fb0b9 100644 --- a/test/externalConverters.test.js +++ b/test/extensions/externalConverters.test.ts @@ -1,30 +1,16 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const path = require('path'); -const fs = require('fs'); - -zigbeeHerdsmanConverters.addDefinition = jest.fn(); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, mockController as mockZHController} from '../mocks/zigbeeHerdsman'; -const mocksClear = [ - zigbeeHerdsmanConverters.addDefinition, - zigbeeHerdsman.permitJoin, - mockExit, - MQTT.end, - zigbeeHerdsman.stop, - logger.debug, - MQTT.publish, - MQTT.connect, - zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, - logger.error, -]; +import fs from 'fs'; +import path from 'path'; + +import * as zhc from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; jest.mock( 'mock-external-converter-module', @@ -55,10 +41,26 @@ jest.mock( }, ); -describe('Loads external converters', () => { - let controller; +const mockZHCAddDefinition = jest.fn(); +// @ts-expect-error mock +zhc.addDefinition = mockZHCAddDefinition; + +const mocksClear = [ + mockZHCAddDefinition, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockZHController.permitJoin, + mockZHController.stop, + mockMQTT.end, + mockMQTT.publish, + mockLogger.debug, + mockLogger.error, +]; + +describe('Extension: ExternalConverters', () => { + let controller: Controller; - let resetExtension = async () => { + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'ExternalConverters'); await controller.enableDisableExtension(true, 'ExternalConverters'); }; @@ -84,15 +86,15 @@ describe('Loads external converters', () => { it('Does not load external converters', async () => { settings.set(['external_converters'], []); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(0); + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(0); }); it('Loads external converters', async () => { - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); + fs.copyFileSync(path.join(__dirname, '..', 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); settings.set(['external_converters'], ['mock-external-converter.js']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({ + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(1); + expect(mockZHCAddDefinition).toHaveBeenCalledWith({ mock: true, zigbeeModel: ['external_converter_device'], vendor: 'external', @@ -106,13 +108,13 @@ describe('Loads external converters', () => { it('Loads multiple external converters', async () => { fs.copyFileSync( - path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), + path.join(__dirname, '..', 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js'), ); settings.set(['external_converters'], ['mock-external-converter-multiple.js']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, { + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(2); + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(1, { mock: 1, model: 'external_converters_device_1', zigbeeModel: ['external_converter_device_1'], @@ -122,7 +124,7 @@ describe('Loads external converters', () => { toZigbee: [], exposes: [], }); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(2, { mock: 2, model: 'external_converters_device_2', zigbeeModel: ['external_converter_device_2'], @@ -137,8 +139,8 @@ describe('Loads external converters', () => { it('Loads external converters from package', async () => { settings.set(['external_converters'], ['mock-external-converter-module']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledWith({ + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(1); + expect(mockZHCAddDefinition).toHaveBeenCalledWith({ mock: true, }); }); @@ -146,22 +148,22 @@ describe('Loads external converters', () => { it('Loads multiple external converters from package', async () => { settings.set(['external_converters'], ['mock-multiple-external-converter-module']); await resetExtension(); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(1, { + expect(mockZHCAddDefinition).toHaveBeenCalledTimes(2); + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(1, { mock: 1, }); - expect(zigbeeHerdsmanConverters.addDefinition).toHaveBeenNthCalledWith(2, { + expect(mockZHCAddDefinition).toHaveBeenNthCalledWith(2, { mock: 2, }); }); it('Loads external converters with error', async () => { - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); + fs.copyFileSync(path.join(__dirname, '..', 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); settings.set(['external_converters'], ['mock-external-converter.js']); - zigbeeHerdsmanConverters.addDefinition.mockImplementationOnce(() => { + mockZHCAddDefinition.mockImplementationOnce(() => { throw new Error('Invalid definition!'); }); await resetExtension(); - expect(logger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`); + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to load external converter file 'mock-external-converter.js' (Invalid definition!)`); }); }); diff --git a/test/externalExtension.test.js b/test/extensions/externalExtension.test.ts similarity index 56% rename from test/externalExtension.test.js rename to test/extensions/externalExtension.test.ts index 38c7fafd71..20c3bbc6d9 100644 --- a/test/externalExtension.test.js +++ b/test/extensions/externalExtension.test.ts @@ -1,54 +1,55 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const path = require('path'); -const {rimrafSync} = require('rimraf'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const stringify = require('json-stable-stringify-without-jsonify'); -const flushPromises = require('./lib/flushPromises'); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, mockController as mockZHController, returnDevices} from '../mocks/zigbeeHerdsman'; + +import fs from 'fs'; +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import {rimrafSync} from 'rimraf'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + const mocksClear = [ - zigbeeHerdsman.permitJoin, - MQTT.end, - zigbeeHerdsman.stop, - logger.debug, - MQTT.publish, - MQTT.connect, - zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, - logger.error, + mockZHController.permitJoin, + mockZHController.stop, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockMQTT.end, + mockMQTT.publish, + mockLogger.debug, + mockLogger.error, ]; -const fs = require('fs'); -const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); -const unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); - -describe('User extensions', () => { - let controller; +describe('Extension: ExternalExtension', () => { + let controller: Controller; + let mkdirSyncSpy: jest.SpyInstance; + let unlinkSyncSpy: jest.SpyInstance; beforeAll(async () => { jest.useFakeTimers(); - }); - - beforeEach(async () => { - data.writeDefaultConfiguration(); - settings.reRead(); - mocksClear.forEach((m) => m.mockClear()); + mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); + unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); }); afterAll(async () => { jest.useRealTimers(); }); - beforeEach(() => { - zigbeeHerdsman.returnDevices.splice(0); - controller = new Controller(jest.fn(), jest.fn()); + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + mocksClear.forEach((m) => m.mockClear()); + returnDevices.splice(0); mocksClear.forEach((m) => m.mockClear()); data.writeDefaultConfiguration(); settings.reRead(); data.writeDefaultState(); }); + afterEach(() => { const extensionPath = path.join(data.mockDir, 'extension'); rimrafSync(extensionPath); @@ -56,14 +57,14 @@ describe('User extensions', () => { it('Load user extension', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), 'utf-8'); fs.mkdirSync(extensionPath); - fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js')); + fs.copyFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), path.join(extensionPath, 'exampleExtension.js')); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', stringify([{name: 'exampleExtension.js', code: extensionCode}]), {retain: true, qos: 0}, @@ -73,20 +74,20 @@ describe('User extensions', () => { it('Load user extension from api call', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), 'utf-8'); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', stringify([{name: 'foo.js', code: extensionCode}]), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0}, @@ -96,50 +97,49 @@ describe('User extensions', () => { }); it('Do not load corrupted extensions', async () => { - const extensionPath = path.join(data.mockDir, 'extension'); const extensionCode = 'definetly not a correct javascript code'; controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'foo.js', code: extensionCode})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/extension/save', expect.any(String), {retain: false, qos: 0}, expect.any(Function), ); - const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); + const payload = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(payload).toEqual(expect.objectContaining({data: {}, status: 'error'})); expect(payload.error).toMatch('Unexpected identifier'); }); it('Removes user extension', async () => { const extensionPath = path.join(data.mockDir, 'extension'); - const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + const extensionCode = fs.readFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), 'utf-8'); fs.mkdirSync(extensionPath); const extensionFilePath = path.join(extensionPath, 'exampleExtension.js'); - fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath); + fs.copyFileSync(path.join(__dirname, '..', 'assets', 'exampleExtension.js'), extensionFilePath); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/extensions', stringify([{name: 'exampleExtension.js', code: extensionCode}]), {retain: true, qos: 0}, expect.any(Function), ); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'exampleExtension.js'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'exampleExtension.js'})); await flushPromises(); expect(unlinkSyncSpy).toHaveBeenCalledWith(extensionFilePath); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'non existing.js'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: 'non existing.js'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/extension/remove', stringify({data: {}, status: 'error', error: "Extension non existing.js doesn't exists"}), {retain: false, qos: 0}, diff --git a/test/extensions/frontend.test.ts b/test/extensions/frontend.test.ts new file mode 100644 index 0000000000..fc7c634d59 --- /dev/null +++ b/test/extensions/frontend.test.ts @@ -0,0 +1,393 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {EventHandler, flushPromises} from '../mocks/utils'; +import {devices} from '../mocks/zigbeeHerdsman'; + +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import ws from 'ws'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +let mockHTTPOnRequest: (request: {url: string}, response: number) => void; +const mockHTTPEvents: Record = {}; +const mockHTTP = { + listen: jest.fn(), + on: (event: string, handler: EventHandler): void => { + mockHTTPEvents[event] = handler; + }, + close: jest.fn().mockImplementation((cb) => cb()), +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let mockHTTPSOnRequest: (request: {url: string}, response: number) => void; +const mockHTTPSEvents: Record = {}; +const mockHTTPS = { + listen: jest.fn(), + on: (event: string, handler: EventHandler): void => { + mockHTTPSEvents[event] = handler; + }, + close: jest.fn().mockImplementation((cb) => cb()), +}; + +const mockWSocket = { + close: jest.fn(), +}; + +const mockWSClientEvents: Record = {}; +const mockWSClient = { + on: (event: string, handler: EventHandler): void => { + mockWSClientEvents[event] = handler; + }, + send: jest.fn(), + terminate: jest.fn(), + readyState: 'close', +}; +const mockWSEvents: Record = {}; +const mockWSClients: (typeof mockWSClient)[] = []; +const mockWS = { + clients: mockWSClients, + on: (event: string, handler: EventHandler): void => { + mockWSEvents[event] = handler; + }, + handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => { + cb(mockWSocket); + }), + emit: jest.fn(), + close: jest.fn(), +}; + +let mockNodeStaticPath: string = ''; +const mockNodeStatic = jest.fn(); + +const mockFinalHandler = jest.fn(); + +jest.mock('http', () => ({ + createServer: jest.fn().mockImplementation((onRequest) => { + mockHTTPOnRequest = onRequest; + return mockHTTP; + }), + Agent: jest.fn(), +})); + +jest.mock('https', () => ({ + createServer: jest.fn().mockImplementation((onRequest) => { + mockHTTPSOnRequest = onRequest; + return mockHTTPS; + }), + Agent: jest.fn(), +})); + +jest.mock('express-static-gzip', () => + jest.fn().mockImplementation((path) => { + mockNodeStaticPath = path; + return mockNodeStatic; + }), +); + +jest.mock('zigbee2mqtt-frontend', () => ({ + getPath: (): string => 'my/dummy/path', +})); + +jest.mock('ws', () => ({ + OPEN: 'open', + Server: jest.fn().mockImplementation(() => { + return mockWS; + }), +})); + +jest.mock('finalhandler', () => + jest.fn().mockImplementation(() => { + return mockFinalHandler; + }), +); + +const mocksClear = [ + mockHTTP.close, + mockHTTP.listen, + mockHTTPS.close, + mockHTTPS.listen, + mockWSocket.close, + mockWS.close, + mockWS.handleUpgrade, + mockWS.emit, + mockWSClient.send, + mockWSClient.terminate, + mockNodeStatic, + mockFinalHandler, + mockMQTT.publishAsync, + mockLogger.error, +]; + +describe('Extension: Frontend', () => { + let controller: Controller; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + beforeEach(async () => { + mockWS.clients = []; + data.writeDefaultConfiguration(); + data.writeDefaultState(); + settings.reRead(); + settings.set(['frontend'], {port: 8081, host: '127.0.0.1'}); + settings.set(['homeassistant'], true); + devices.bulb.linkquality = 10; + mocksClear.forEach((m) => m.mockClear()); + mockWSClient.readyState = 'close'; + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + afterEach(async () => { + delete devices.bulb.linkquality; + }); + + it('Start/stop with defaults', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + it('Start/stop without host', async () => { + settings.set(['frontend'], {port: 8081}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + it('Start/stop unix socket', async () => { + settings.set(['frontend'], {host: '/tmp/zigbee2mqtt.sock'}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockNodeStaticPath).toBe('my/dummy/path'); + expect(mockHTTP.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock'); + mockWS.clients.push(mockWSClient); + await controller.stop(); + expect(mockWSClient.terminate).toHaveBeenCalledTimes(1); + expect(mockHTTP.close).toHaveBeenCalledTimes(1); + expect(mockWS.close).toHaveBeenCalledTimes(1); + }); + + it('Start/stop HTTPS valid', async () => { + settings.set(['frontend', 'ssl_cert'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.crt')); + settings.set(['frontend', 'ssl_key'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.key')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + it('Start/stop HTTPS invalid : missing config', async () => { + settings.set(['frontend', 'ssl_cert'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.crt')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + it('Start/stop HTTPS invalid : missing file', async () => { + settings.set(['frontend', 'ssl_cert'], 'filesNotExists.crt'); + settings.set(['frontend', 'ssl_key'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.key')); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); + await controller.stop(); + }); + + it('Websocket interaction', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + mockWSClient.readyState = 'open'; + mockWS.clients.push(mockWSClient); + await mockWSEvents.connection(mockWSClient); + + const allTopics = mockWSClient.send.mock.calls.map((m) => JSON.parse(m).topic); + expect(allTopics).toContain('bridge/devices'); + expect(allTopics).toContain('bridge/info'); + expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}})); + expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}})); + + // Message + mockMQTT.publishAsync.mockClear(); + mockWSClient.send.mockClear(); + mockWSClientEvents.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({ + state: 'ON', + power_on_behavior: null, + linkquality: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }), + {retain: false, qos: 0}, + ); + mockWSClientEvents.message(undefined, false); + mockWSClientEvents.message('', false); + mockWSClientEvents.message(null, false); + await flushPromises(); + + // Error + mockWSClientEvents.error(new Error('This is an error')); + expect(mockLogger.error).toHaveBeenCalledWith('WebSocket error: This is an error'); + + // Received message on socket + expect(mockWSClient.send).toHaveBeenCalledTimes(1); + expect(mockWSClient.send).toHaveBeenCalledWith( + stringify({ + topic: 'bulb_color', + payload: { + state: 'ON', + power_on_behavior: null, + linkquality: null, + update: {state: null, installed_version: -1, latest_version: -1}, + }, + }), + ); + + // Shouldnt set when not ready + mockWSClient.send.mockClear(); + mockWSClient.readyState = 'close'; + mockWSClientEvents.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); + expect(mockWSClient.send).toHaveBeenCalledTimes(0); + + // Send last seen on connect + mockWSClient.send.mockClear(); + mockWSClient.readyState = 'open'; + settings.set(['advanced'], {last_seen: 'ISO_8601'}); + mockWS.clients.push(mockWSClient); + await mockWSEvents.connection(mockWSClient); + expect(mockWSClient.send).toHaveBeenCalledWith( + stringify({topic: 'remote', payload: {brightness: 255, last_seen: '1970-01-01T00:00:01.000Z'}}), + ); + }); + + it('onRequest/onUpgrade', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + const mockSocket = {destroy: jest.fn()}; + mockHTTPEvents.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url: 'http://localhost:8080/api'}, mockSocket, 3, expect.any(Function)); + mockWS.handleUpgrade.mock.calls[0][3](99); + expect(mockWS.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'}); + + mockHTTPOnRequest({url: '/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/file.txt', path: '/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + }); + + it('Static server', async () => { + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); + }); + + it('Authentification', async () => { + const authToken = 'sample-secure-token'; + settings.set(['frontend'], {auth_token: authToken}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + const mockSocket = {destroy: jest.fn()}; + mockHTTPEvents.upgrade({url: '/api'}, mockSocket, mockWSocket); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url: '/api'}, mockSocket, mockWSocket, expect.any(Function)); + expect(mockWSocket.close).toHaveBeenCalledWith(4401, 'Unauthorized'); + + mockWSocket.close.mockClear(); + mockWS.emit.mockClear(); + + const url = `/api?token=${authToken}`; + mockWS.handleUpgrade.mockClear(); + mockHTTPEvents.upgrade({url: url}, mockSocket, 3); + expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1); + expect(mockSocket.destroy).toHaveBeenCalledTimes(0); + expect(mockWS.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function)); + expect(mockWSocket.close).toHaveBeenCalledTimes(0); + mockWS.handleUpgrade.mock.calls[0][3](mockWSocket); + expect(mockWS.emit).toHaveBeenCalledWith('connection', mockWSocket, {url}); + }); + + it.each(['/z2m/', '/z2m'])('Works with non-default base url %s', async (baseUrl) => { + settings.set(['frontend'], {base_url: baseUrl}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m/api'}); + + mockHTTPOnRequest({url: '/z2m'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m', path: '/', url: '/'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + mockHTTPOnRequest({url: '/z2m/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m/file.txt', path: '/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + mockHTTPOnRequest({url: '/z/file.txt'}, 2); + expect(mockNodeStatic).not.toHaveBeenCalled(); + expect(mockFinalHandler).toHaveBeenCalled(); + }); + + it('Works with non-default complex base url', async () => { + const baseUrl = '/z2m-more++/c0mplex.url/'; + settings.set(['frontend'], {base_url: baseUrl}); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + + expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m-more++/c0mplex.url/api'}); + + mockHTTPOnRequest({url: '/z2m-more++/c0mplex.url'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith({originalUrl: '/z2m-more++/c0mplex.url', path: '/', url: '/'}, 2, expect.any(Function)); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + mockHTTPOnRequest({url: '/z2m-more++/c0mplex.url/file.txt'}, 2); + expect(mockNodeStatic).toHaveBeenCalledTimes(1); + expect(mockNodeStatic).toHaveBeenCalledWith( + {originalUrl: '/z2m-more++/c0mplex.url/file.txt', path: '/file.txt', url: '/file.txt'}, + 2, + expect.any(Function), + ); + expect(mockFinalHandler).not.toHaveBeenCalledWith(); + + mockNodeStatic.mockReset(); + mockHTTPOnRequest({url: '/z/file.txt'}, 2); + expect(mockNodeStatic).not.toHaveBeenCalled(); + expect(mockFinalHandler).toHaveBeenCalled(); + }); +}); diff --git a/test/group.test.js b/test/extensions/groups.test.ts similarity index 52% rename from test/group.test.js rename to test/extensions/groups.test.ts index 4c160bb416..559f059b7a 100644 --- a/test/group.test.js +++ b/test/extensions/groups.test.ts @@ -1,25 +1,30 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const stringify = require('json-stable-stringify-without-jsonify'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45542'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4'); -zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45724'); - -const MQTT = require('./stub/mqtt'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const settings = require('../lib/util/settings'); - -describe('Groups', () => { - let controller; - - let resetExtension = async () => { +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {toZigbee as zhcToZigbee} from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +returnDevices.push( + devices.coordinator.ieeeAddr, + devices.bulb_color.ieeeAddr, + devices.bulb.ieeeAddr, + devices.QBKG03LM.ieeeAddr, + devices.bulb_color_2.ieeeAddr, + devices.bulb_2.ieeeAddr, + devices.GLEDOPTO_2ID.ieeeAddr, +); + +describe('Extension: Groups', () => { + let controller: Controller; + + const resetExtension = async (): Promise => { await controller.enableDisableExtension(false, 'Groups'); await controller.enableDisableExtension(true, 'Groups'); }; @@ -36,116 +41,115 @@ describe('Groups', () => { }); beforeEach(() => { - Object.values(zigbeeHerdsman.groups).forEach((g) => (g.members = [])); + Object.values(groups).forEach((g) => (g.members = [])); data.writeDefaultConfiguration(); settings.reRead(); - MQTT.publish.mockClear(); - zigbeeHerdsman.groups.gledopto_group.command.mockClear(); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + mockMQTT.publish.mockClear(); + groups.gledopto_group.command.mockClear(); + zhcToZigbee.__clearStore__(); + // @ts-expect-error private controller.state.state = {}; }); it('Apply group updates add', async () => { settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}}); - zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1)); + groups.group_1.members.push(devices.bulb.getEndpoint(1)!); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ - zigbeeHerdsman.devices.bulb.getEndpoint(1), - zigbeeHerdsman.devices.bulb_color.getEndpoint(1), - ]); + expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); }); it('Apply group updates remove', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const endpoint = devices.bulb_color.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); + expect(groups.group_1.members).toStrictEqual([]); }); it('Apply group updates remove handle fail', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const endpoint = devices.bulb_color.getEndpoint(1)!; endpoint.removeFromGroup.mockImplementationOnce(() => { throw new Error('failed!'); }); - const group = zigbeeHerdsman.groups.group_1; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); - logger.error.mockClear(); + mockLogger.error.mockClear(); await resetExtension(); - expect(logger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([endpoint]); + expect(mockLogger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); + expect(groups.group_1.members).toStrictEqual([endpoint]); }); it('Move to non existing group', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {3: {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); + expect(groups.group_1.members).toStrictEqual([]); }); it('Add non standard endpoint to group with name', async () => { - const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + const QBKG03LM = devices.QBKG03LM; settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); + expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); }); it('Add non standard endpoint to group with number', async () => { - const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + const QBKG03LM = devices.QBKG03LM; settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); + expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); }); it('Shouldnt crash on non-existing devices', async () => { - logger.error.mockClear(); + mockLogger.error.mockClear(); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); - expect(logger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); + expect(groups.group_1.members).toStrictEqual([]); + expect(mockLogger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); }); it('Should resolve device friendly names', async () => { - settings.set(['devices', zigbeeHerdsman.devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); + settings.set(['devices', devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}}); await resetExtension(); - expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ - zigbeeHerdsman.devices.bulb.getEndpoint(1), - zigbeeHerdsman.devices.bulb_color.getEndpoint(1), - ]); + expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); }); it('Should publish group state change when a device in it changes state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not republish identical optimistic group states', async () => { - const device1 = zigbeeHerdsman.devices.bulb_2; - const device2 = zigbeeHerdsman.devices.bulb_color_2; - const group = zigbeeHerdsman.groups.group_tradfri_remote; + const device1 = devices.bulb_2; + const device2 = devices.bulb_color_2; await resetExtension(); - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device: device1, @@ -153,7 +157,7 @@ describe('Groups', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device: device2, @@ -162,33 +166,33 @@ describe('Groups', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(6); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(6); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_tradfri_remote', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_with_tradfri', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/ha_discovery_group', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -197,83 +201,108 @@ describe('Groups', () => { }); it('Should publish state change of all members when a group changes its state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not publish state change when group changes state and device is disabled', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['devices', device.ieeeAddr, 'disabled'], true); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state change for group when members state change', async () => { // Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should publish state of device with endpoint name', async () => { - const group = zigbeeHerdsman.groups.gledopto_group; + const group = groups.gledopto_group; await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -284,20 +313,20 @@ describe('Groups', () => { }); it('Should publish state of group when specific state of specific endpoint is changed', async () => { - const group = zigbeeHerdsman.groups.gledopto_group; + const group = groups.gledopto_group; await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -307,47 +336,52 @@ describe('Groups', () => { }); it('Should publish state change of all members when a group changes its state, filtered', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 100}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Shouldnt publish group state change when a group is not optimistic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}}); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should publish state change of another group with shared device when a group changes its state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], { 1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}, @@ -356,21 +390,26 @@ describe('Groups', () => { }); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should not publish state change off if any lights within are still on when changed via device', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -378,22 +417,27 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -401,21 +445,21 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), @@ -425,11 +469,11 @@ describe('Groups', () => { }); it('Should not publish state change off if any lights within are still on when changed via shared group', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -438,23 +482,33 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_2', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should publish state change off if all lights within turn off', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -462,54 +516,64 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should only update group state with changed properties', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, }); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200})); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 300, state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), {retain: false, qos: 0}, @@ -518,11 +582,11 @@ describe('Groups', () => { }); it('Should publish state change off even when missing current state', async () => { - const device_1 = zigbeeHerdsman.devices.bulb_color; - const device_2 = zigbeeHerdsman.devices.bulb; - const endpoint_1 = device_1.getEndpoint(1); - const endpoint_2 = device_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device_1 = devices.bulb_color; + const device_2 = devices.bulb; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { @@ -530,33 +594,47 @@ describe('Groups', () => { }); await resetExtension(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.state.state = {}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({state: 'OFF'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Add to group via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const group = groups.group_1; settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({transaction: '123', group: 'group_1', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({transaction: '123', group: 'group_1', device: 'bulb_color'}), + ); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'bulb_color', group: 'group_1'}, transaction: '123', status: 'ok'}), {retain: false, qos: 0}, @@ -565,9 +643,9 @@ describe('Groups', () => { }); it('Add to group via MQTT fails', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); @@ -575,13 +653,13 @@ describe('Groups', () => { throw new Error('timeout'); }); await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'error', error: 'Failed to add from group (timeout)'}), {retain: false, qos: 0}, @@ -590,19 +668,19 @@ describe('Groups', () => { }); it('Add to group with slashes via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups['group/with/slashes']; + const group = groups['group/with/slashes']; settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}}); expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'bulb_color', group: 'group/with/slashes'}, status: 'ok'}), {retain: false, qos: 0}, @@ -611,18 +689,18 @@ describe('Groups', () => { }); it('Add to group via MQTT with postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -631,20 +709,20 @@ describe('Groups', () => { }); it('Add to group via MQTT with postfix shouldnt add it twice', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; expect(group.members.length).toBe(0); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -653,19 +731,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -674,22 +752,22 @@ describe('Groups', () => { }); it('Remove from group via MQTT keeping device reporting', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true}), ); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -698,19 +776,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT when in zigbee but not in settings', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -719,19 +797,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 1', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: '0x0017880104e45542/3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -740,19 +818,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 2', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: 'wall_switch_double/3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -761,19 +839,19 @@ describe('Groups', () => { }); it('Remove from group via MQTT with postfix variant 3', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({data: {device: '0x0017880104e45542/right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, @@ -782,19 +860,19 @@ describe('Groups', () => { }); it('Remove from group all', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_1; + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; group.members.push(endpoint); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); await resetExtension(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove_all', stringify({data: {device: '0x0017880104e45542/right'}, status: 'ok'}), {retain: false, qos: 0}, @@ -804,12 +882,12 @@ describe('Groups', () => { it('Error when adding to non-existing group', async () => { await resetExtension(); - logger.error.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', stringify({ data: {device: 'bulb_color', group: 'group_1_not_existing'}, @@ -823,12 +901,12 @@ describe('Groups', () => { it('Error when adding a non-existing device', async () => { await resetExtension(); - logger.error.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'})); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'})); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({ data: {device: 'bulb_color_not_existing', group: 'group_1'}, @@ -842,15 +920,15 @@ describe('Groups', () => { it('Error when adding a non-existing endpoint', async () => { await resetExtension(); - logger.error.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'}), ); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', stringify({ data: {device: 'bulb_color/not_existing_endpoint', group: 'group_1'}, @@ -863,54 +941,54 @@ describe('Groups', () => { }); it('Should only include relevant properties when publishing member states', async () => { - const bulbColor = zigbeeHerdsman.devices.bulb_color; - const bulbColorTemp = zigbeeHerdsman.devices.bulb; - const group = zigbeeHerdsman.groups.group_1; - group.members.push(bulbColor.getEndpoint(1)); - group.members.push(bulbColorTemp.getEndpoint(1)); + const bulbColor = devices.bulb_color; + const bulbColorTemp = devices.bulb; + const group = groups.group_1; + group.members.push(bulbColor.getEndpoint(1)!); + group.members.push(bulbColorTemp.getEndpoint(1)!); settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}}); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 50}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 50}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 50}), {retain: true, qos: 0}, expect.any(Function), ); - MQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}})); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/group_1', stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 548}), {retain: true, qos: 0}, diff --git a/test/homeassistant.test.js b/test/extensions/homeassistant.test.ts similarity index 85% rename from test/homeassistant.test.js rename to test/extensions/homeassistant.test.ts index 05687cc5e0..98be1a7097 100644 --- a/test/homeassistant.test.js +++ b/test/extensions/homeassistant.test.ts @@ -1,86 +1,107 @@ -const data = require('./stub/data'); -const settings = require('../lib/util/settings'); -const stringify = require('json-stable-stringify-without-jsonify'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const flushPromises = require('./lib/flushPromises'); -const MQTT = require('./stub/mqtt'); -const sleep = require('./stub/sleep'); -const Controller = require('../lib/controller'); - -describe('HomeAssistant extension', () => { - let version; - let z2m_version; - let controller; - let extension; - let origin; - - let resetExtension = async (runTimers = true) => { +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import assert from 'assert'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import HomeAssistant from '../../lib/extension/homeassistant'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.debug, mockLogger.warning, mockLogger.error]; + +describe('Extension: HomeAssistant', () => { + let controller: Controller; + let version: string; + let z2m_version: string; + let extension: HomeAssistant; + const origin = {name: 'Zigbee2MQTT', sw: '', url: 'https://www.zigbee2mqtt.io'}; + + const resetExtension = async (runTimers = true): Promise => { await controller.enableDisableExtension(false, 'HomeAssistant'); - MQTT.publish.mockClear(); + mocksClear.forEach((m) => m.mockClear()); await controller.enableDisableExtension(true, 'HomeAssistant'); + // @ts-expect-error private extension = controller.extensions.find((e) => e.constructor.name === 'HomeAssistant'); + if (runTimers) { await jest.runOnlyPendingTimersAsync(); } }; - let resetDiscoveryPayloads = (id) => { + const resetDiscoveryPayloads = (id: string): void => { // Change discovered payload, otherwise it's not re-published because it's the same. + // @ts-expect-error private Object.values(extension.discovered[id].messages).forEach((m) => (m.payload = 'changed')); }; - let clearDiscoveredTrigger = (id) => { + const clearDiscoveredTrigger = (id: string): void => { + // @ts-expect-error private extension.discovered[id].triggers = new Set(); }; - beforeEach(async () => { - data.writeDefaultConfiguration(); - settings.reRead(); - settings.set(['homeassistant'], true); - data.writeEmptyState(); - controller.state.load(); - await resetExtension(); - await flushPromises(); - }); - beforeAll(async () => { - z2m_version = (await require('../lib/util/utils').default.getZigbee2MQTTVersion()).version; - origin = {name: 'Zigbee2MQTT', sw: z2m_version, url: 'https://www.zigbee2mqtt.io'}; + const {getZigbee2MQTTVersion} = (await import('../../lib/util/utils')).default; + z2m_version = (await getZigbee2MQTTVersion()).version; version = `Zigbee2MQTT ${z2m_version}`; + origin.sw = z2m_version; jest.useFakeTimers(); settings.set(['homeassistant'], true); data.writeDefaultConfiguration(); settings.reRead(); data.writeEmptyState(); - MQTT.publish.mockClear(); - sleep.mock(); - controller = new Controller(false); + mockMQTT.publish.mockClear(); + mockSleep.mock(); + controller = new Controller(jest.fn(), jest.fn()); await controller.start(); }); afterAll(async () => { jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); + }); + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + settings.set(['homeassistant'], true); + data.writeEmptyState(); + // @ts-expect-error private + controller.state.load(); + await resetExtension(); + await flushPromises(); }); - it('Should not have duplicate type/object_ids in a mapping', () => { - const duplicated = []; - require('zigbee-herdsman-converters').definitions.forEach((d) => { - const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes; - const device = {definition: d, isDevice: () => true, isGroup: () => false, options: {}, exposes: () => exposes, zh: {endpoints: []}}; + it('Should not have duplicate type/object_ids in a mapping', async () => { + const duplicated: string[] = []; + (await import('zigbee-herdsman-converters')).definitions.forEach((d) => { + const exposes = typeof d.exposes == 'function' ? d.exposes(undefined, undefined) : d.exposes; + const device = { + definition: d, + isDevice: (): boolean => true, + isGroup: (): boolean => false, + options: {}, + exposes: (): unknown[] => exposes, + zh: {endpoints: []}, + }; + // @ts-expect-error private const configs = extension.getConfigs(device); - const cfg_type_object_ids = []; + const cfgTypeObjectIds: string[] = []; configs.forEach((c) => { const id = c['type'] + '/' + c['object_id']; - if (cfg_type_object_ids.includes(id)) { + if (cfgTypeObjectIds.includes(id)) { // A dynamic function must exposes all possible attributes for the docs if (typeof d.exposes != 'function') { duplicated.push(d.model); } } else { - cfg_type_object_ids.push(id); + cfgTypeObjectIds.push(id); } }); }); @@ -132,7 +153,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -161,7 +182,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_9/switch/config', stringify(payload), {retain: true, qos: 1}, @@ -181,7 +202,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -190,7 +210,7 @@ describe('HomeAssistant extension', () => { enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -211,7 +231,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -219,7 +238,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -240,7 +259,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -248,7 +266,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), {retain: true, qos: 1}, @@ -270,7 +288,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -278,7 +295,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', stringify(payload), {retain: true, qos: 1}, @@ -301,7 +318,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -309,7 +325,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', stringify(payload), {retain: true, qos: 1}, @@ -324,7 +340,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', model: 'Smart wall switch (no neutral, double rocker) (QBKG03LM)', name: 'wall_switch_double', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, json_attributes_topic: 'zigbee2mqtt/wall_switch_double', @@ -338,7 +353,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_left }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_left/config', stringify(payload), {retain: true, qos: 1}, @@ -353,7 +368,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', model: 'Smart wall switch (no neutral, double rocker) (QBKG03LM)', name: 'wall_switch_double', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, json_attributes_topic: 'zigbee2mqtt/wall_switch_double', @@ -367,7 +381,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_right }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/0x0017880104e45542/switch_right/config', stringify(payload), {retain: true, qos: 1}, @@ -387,7 +401,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'IKEA', model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', name: 'bulb', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, effect: true, @@ -401,7 +414,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), {retain: true, qos: 1}, @@ -477,7 +490,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -495,7 +507,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -503,21 +514,21 @@ describe('HomeAssistant extension', () => { }); // Should subscribe to `homeassistant/#` to find out what devices are already discovered. - expect(MQTT.subscribe).toHaveBeenCalledWith(`homeassistant/#`); + expect(mockMQTT.subscribe).toHaveBeenCalledWith(`homeassistant/#`); // Retained Home Assistant discovery message arrives - await MQTT.events.message(topic1, payload1); - await MQTT.events.message(topic2, payload2); + await mockMQTTEvents.message(topic1, payload1); + await mockMQTTEvents.message(topic2, payload2); await jest.runOnlyPendingTimersAsync(); // Should unsubscribe to not receive all messages that are going to be published to `homeassistant/#` again. - expect(MQTT.unsubscribe).toHaveBeenCalledWith(`homeassistant/#`); + expect(mockMQTT.unsubscribe).toHaveBeenCalledWith(`homeassistant/#`); - expect(MQTT.publish).not.toHaveBeenCalledWith(topic1, expect.anything(), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic1, expect.anything(), expect.any(Object), expect.any(Function)); // Device automation should not be cleared - expect(MQTT.publish).not.toHaveBeenCalledWith(topic2, '', expect.any(Object), expect.any(Function)); - expect(logger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic2, '', expect.any(Object), expect.any(Function)); + expect(mockLogger.debug).toHaveBeenCalledWith(`Skipping discovery of 'sensor/0x0017880104e45522/humidity/config', already discovered`); }); it('Should discover devices with precision', async () => { @@ -548,7 +559,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -556,7 +566,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -577,7 +587,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -585,7 +594,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -606,7 +615,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -614,7 +622,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), {retain: true, qos: 1}, @@ -675,7 +683,7 @@ describe('HomeAssistant extension', () => { icon: 'mdi:test', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -693,7 +701,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'custom model', manufacturer: 'Not from Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -705,7 +712,7 @@ describe('HomeAssistant extension', () => { object_id: 'weather_sensor_humidity', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -740,7 +747,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'Weather Sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -749,7 +755,7 @@ describe('HomeAssistant extension', () => { enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -770,7 +776,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'Weather Sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -778,7 +783,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), {retain: true, qos: 1}, @@ -803,10 +808,9 @@ describe('HomeAssistant extension', () => { await resetExtension(); - let payload; await flushPromises(); - payload = { + const payload = { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], command_topic: 'zigbee2mqtt/my_switch/set', device: { @@ -814,7 +818,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Aqara', model: 'Smart wall switch (no neutral, single rocker) (QBKG04LM)', name: 'my_switch', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, json_attributes_topic: 'zigbee2mqtt/my_switch', @@ -828,7 +831,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x0017880104e45541/light/config', stringify(payload), {retain: true, qos: 1}, @@ -846,13 +849,13 @@ describe('HomeAssistant extension', () => { await resetExtension(); await flushPromises(); - const topics = MQTT.publish.mock.calls.map((c) => c[0]); + const topics = mockMQTT.publish.mock.calls.map((c) => c[0]); expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config'); expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/temperature/config'); }); it('Shouldnt discover sensor when set to null', async () => { - logger.error.mockClear(); + mockLogger.error.mockClear(); settings.set(['devices', '0x0017880104e45522'], { homeassistant: {humidity: null}, friendly_name: 'weather_sensor', @@ -861,15 +864,13 @@ describe('HomeAssistant extension', () => { await resetExtension(); - const topics = MQTT.publish.mock.calls.map((c) => c[0]); + const topics = mockMQTT.publish.mock.calls.map((c) => c[0]); expect(topics).not.toContain('homeassistant/sensor/0x0017880104e45522/humidity/config'); expect(topics).toContain('homeassistant/sensor/0x0017880104e45522/temperature/config'); }); it('Should discover devices with fan', async () => { - let payload; - - payload = { + const payload = { state_topic: 'zigbee2mqtt/fan', state_value_template: '{{ value_json.fan_state }}', command_topic: 'zigbee2mqtt/fan/set/fan_state', @@ -891,7 +892,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45548'], name: 'fan', - sw_version: null, model: 'Universal wink enabled white ceiling fan premier remote control (99432)', manufacturer: 'Hampton Bay', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -899,7 +899,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/fan/0x0017880104e45548/fan/config', stringify(payload), {retain: true, qos: 1}, @@ -925,7 +925,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Tuya', model: 'Radiator valve with thermostat (TS0601_thermostat)', name: 'TS0601_thermostat', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, preset_mode_command_topic: 'zigbee2mqtt/TS0601_thermostat/set/preset', @@ -950,7 +949,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/climate/0x0017882104a44559/climate/config', stringify(payload), {retain: true, qos: 1}, @@ -994,7 +993,7 @@ describe('HomeAssistant extension', () => { unique_id: '0x18fc2600000d7ae2_climate_zigbee2mqtt', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/climate/0x18fc2600000d7ae2/climate/config', stringify(payload), {qos: 1, retain: true}, @@ -1024,7 +1023,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45551'], name: 'smart vent', - sw_version: null, model: 'Smart vent (SV01)', manufacturer: 'Keen Home', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1032,7 +1030,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0x0017880104e45551/cover/config', stringify(payload), {retain: true, qos: 1}, @@ -1047,7 +1045,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Siglis', model: 'zigfred plus smart in-wall switch (ZFP-1A-CH)', name: 'zigfred_plus', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, json_attributes_topic: 'zigbee2mqtt/zigfred_plus/l6', @@ -1069,7 +1066,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config', stringify(payload), {retain: true, qos: 1}, @@ -1081,9 +1078,7 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic'); await resetExtension(); - let payload; - - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -1097,7 +1092,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1105,7 +1099,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'my_custom_discovery_topic/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1117,27 +1111,27 @@ describe('HomeAssistant extension', () => { settings.set(['experimental', 'output'], 'attribute'); settings.set(['homeassistant'], true); expect(() => { - new Controller(false); + new Controller(jest.fn(), jest.fn()); }).toThrow('Home Assistant integration is not possible with attribute output!'); }); it('Should throw error when homeassistant.discovery_topic equals the mqtt.base_topic', async () => { settings.set(['mqtt', 'base_topic'], 'homeassistant'); expect(() => { - new Controller(false); + new Controller(jest.fn(), jest.fn()); }).toThrow("'homeassistant.discovery_topic' cannot not be equal to the 'mqtt.base_topic' (got 'homeassistant')"); }); it('Should warn when starting with cache_state false', async () => { settings.set(['advanced', 'cache_state'], false); - logger.warning.mockClear(); + mockLogger.warning.mockClear(); await resetExtension(); - expect(logger.warning).toHaveBeenCalledWith('In order for Home Assistant integration to work properly set `cache_state: true'); + expect(mockLogger.warning).toHaveBeenCalledWith('In order for Home Assistant integration to work properly set `cache_state: true'); }); it('Should set missing values to null', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/6987 - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1147,11 +1141,11 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/weather_sensor', stringify({battery: null, humidity: null, linkquality: null, pressure: null, temperature: -0.85, voltage: null}), {retain: false, qos: 1}, @@ -1160,14 +1154,14 @@ describe('HomeAssistant extension', () => { }); it('Should copy hue/saturtion to h/s if present', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const data = {currentHue: 0, currentSaturation: 254}; const payload = {data, cluster: 'lightingColorCtrl', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ color: {hue: 0, saturation: 100, h: 0, s: 100}, @@ -1184,14 +1178,14 @@ describe('HomeAssistant extension', () => { }); it('Should not copy hue/saturtion if properties are missing', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const data = {currentX: 29991, currentY: 26872}; const payload = {data, cluster: 'lightingColorCtrl', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ color: {x: 0.4576, y: 0.41}, @@ -1208,14 +1202,14 @@ describe('HomeAssistant extension', () => { }); it('Should not copy hue/saturtion if color is missing', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({ linkquality: null, @@ -1230,7 +1224,7 @@ describe('HomeAssistant extension', () => { }); it('Shouldt discover when already discovered', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1240,16 +1234,17 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); // 1 publish is the publish from receive - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); }); it('Should discover when not discovered yet', async () => { + // @ts-expect-error private extension.discovered = {}; - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -1259,8 +1254,8 @@ describe('HomeAssistant extension', () => { type: 'attributeReport', linkquality: 10, }; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload); await flushPromises(); const payloadHA = { unit_of_measurement: '°C', @@ -1276,7 +1271,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1284,7 +1278,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payloadHA), {retain: true, qos: 1}, @@ -1293,21 +1287,25 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt discover when device leaves', async () => { + // @ts-expect-error private extension.discovered = {}; - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceLeave(payload); + mockMQTT.publish.mockClear(); + await mockZHEvents.deviceLeave(payload); await flushPromises(); }); it('Should discover when options change', async () => { - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb)!; + assert('ieeeAddr' in device); resetDiscoveryPayloads(device.ieeeAddr); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitEntityOptionsChanged({entity: device, from: {}, to: {test: 123}}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/light/${device.ID}/light/config`, expect.any(String), expect.any(Object), @@ -1317,16 +1315,17 @@ describe('HomeAssistant extension', () => { it('Should send all status when home assistant comes online (default topic)', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - expect(MQTT.subscribe).toHaveBeenCalledWith('homeassistant/status'); + expect(mockMQTT.subscribe).toHaveBeenCalledWith('homeassistant/status'); await flushPromises(); - MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/status', 'online'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('homeassistant/status', 'online'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -1341,7 +1340,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({ action: null, @@ -1355,20 +1354,21 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); }); it('Should send all status when home assistant comes online', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - expect(MQTT.subscribe).toHaveBeenCalledWith('hass/status'); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status', 'online'); + expect(mockMQTT.subscribe).toHaveBeenCalledWith('hass/status'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status', 'online'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', stringify({ state: 'ON', @@ -1383,7 +1383,7 @@ describe('HomeAssistant extension', () => { {retain: true, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', stringify({ action: null, @@ -1401,36 +1401,36 @@ describe('HomeAssistant extension', () => { it('Shouldnt send all status when home assistant comes offline', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); await flushPromises(); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status', 'offline'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status', 'offline'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Shouldnt send all status when home assistant comes online with different topic', async () => { data.writeDefaultState(); + // @ts-expect-error private extension.state.load(); await resetExtension(); - MQTT.publish.mockClear(); - await MQTT.events.message('hass/status_different', 'offline'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('hass/status_different', 'offline'); await flushPromises(); await jest.runOnlyPendingTimersAsync(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should discover devices with availability', async () => { settings.set(['availability'], true); await resetExtension(); - let payload; - - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', enabled_by_default: true, @@ -1444,7 +1444,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1456,7 +1455,7 @@ describe('HomeAssistant extension', () => { ], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1465,35 +1464,35 @@ describe('HomeAssistant extension', () => { }); it('Should clear discovery when device is removed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', 'weather_sensor'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/humidity/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/pressure/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/battery/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/linkquality/config', '', {retain: true, qos: 1}, @@ -1502,11 +1501,11 @@ describe('HomeAssistant extension', () => { }); it('Should clear discovery when group is removed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', '', {retain: true, qos: 1}, @@ -1515,13 +1514,13 @@ describe('HomeAssistant extension', () => { }); it('Should refresh discovery when device is renamed', async () => { - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({topic: 'zigbee2mqtt/weather_sensor/action'}), ); await flushPromises(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/rename', stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: true}), ); @@ -1543,7 +1542,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1551,21 +1549,21 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', stringify({ automation_type: 'trigger', @@ -1577,7 +1575,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1589,8 +1586,8 @@ describe('HomeAssistant extension', () => { }); it('Should refresh discovery when group is renamed', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/rename', stringify({from: 'ha_discovery_group', to: 'ha_discovery_group_new', homeassistant_rename: true}), ); @@ -1636,14 +1633,14 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', '', {retain: true, qos: 1}, @@ -1652,14 +1649,14 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt refresh discovery when device is renamed and homeassistant_rename is false', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/rename', stringify({from: 'weather_sensor', to: 'weather_sensor_renamed', homeassistant_rename: false}), ); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', '', {retain: true, qos: 1}, @@ -1680,7 +1677,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor_renamed', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1688,7 +1684,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -1711,7 +1707,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x000b57fffec6a5b2'], name: 'bulb', - sw_version: null, model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', manufacturer: 'IKEA', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -1721,7 +1716,7 @@ describe('HomeAssistant extension', () => { entity_category: 'diagnostic', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify(payload), {retain: true, qos: 1}, @@ -1730,16 +1725,16 @@ describe('HomeAssistant extension', () => { }); it('Should discover trigger when click is published', async () => { - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(7); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/click/config'); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; const payload1 = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); await flushPromises(); const discoverPayloadAction = { @@ -1752,14 +1747,13 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45520'], name: 'button', - sw_version: null, model: 'Wireless mini switch (WXKG11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), {retain: true, qos: 1}, @@ -1776,25 +1770,24 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45520'], name: 'button', - sw_version: null, model: 'Wireless mini switch (WXKG11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({ action: 'single', @@ -1809,14 +1802,14 @@ describe('HomeAssistant extension', () => { expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({action: '', battery: null, linkquality: null, voltage: null, click: null, power_outage_count: null, device_temperature: null}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({click: '', action: null, battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), {retain: false, qos: 0}, @@ -1824,43 +1817,43 @@ describe('HomeAssistant extension', () => { ); // Should only discover it once - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload1); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload1); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayloadAction), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/click_single/config', stringify(discoverPayloadClick), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function)); // Shouldn't rediscover when already discovered in previous session clearDiscoveredTrigger('0x0017880104e45520'); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'}), ); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button/action'}), ); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const payload2 = {data: {32768: 2}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), @@ -1869,15 +1862,15 @@ describe('HomeAssistant extension', () => { // Should rediscover when already discovered in previous session but with different name clearDiscoveredTrigger('0x0017880104e45520'); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', stringify({topic: 'zigbee2mqtt/button_other_name/action'}), ); await flushPromises(); - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.message(payload2); + mockMQTT.publish.mockClear(); + await mockZHEvents.message(payload2); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_double/config', expect.any(String), expect.any(Object), @@ -1890,14 +1883,14 @@ describe('HomeAssistant extension', () => { homeassistant: {device_automation: null}, }); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; const payload1 = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith( + expect(mockMQTT.publish).not.toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', expect.any(String), expect.any(Object), @@ -1913,7 +1906,7 @@ describe('HomeAssistant extension', () => { }); await resetExtension(); - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(6); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config'); expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/battery/config'); @@ -1924,17 +1917,17 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_legacy_triggers'], false); await resetExtension(); - const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); + const discovered = mockMQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]); expect(discovered.length).toBe(5); expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/click/config'); expect(discovered).not.toContain('homeassistant/sensor/0x0017880104e45520/action/config'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); const discoverPayload = { @@ -1947,30 +1940,29 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45520'], name: 'button', - sw_version: null, model: 'Wireless mini switch (WXKG11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x0017880104e45520/action_single/config', stringify(discoverPayload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({action: 'single', battery: null, linkquality: null, voltage: null, power_outage_count: null, device_temperature: null}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); }); it('Should enable experimental event entities', async () => { @@ -2014,23 +2006,23 @@ describe('HomeAssistant extension', () => { }); it('Should republish payload to postfix topic with lightWithPostfix config', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message('zigbee2mqtt/U202DST600ZB/l2/set', stringify({state: 'ON', brightness: 20})); + await mockMQTTEvents.message('zigbee2mqtt/U202DST600ZB/l2/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB', stringify({state_l2: 'ON', brightness_l2: 20, linkquality: null, state_l1: null, power_on_behavior_l1: null, power_on_behavior_l2: null}), {qos: 0, retain: false}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB/l2', stringify({state: 'ON', brightness: 20, power_on_behavior: null}), {qos: 0, retain: false}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/U202DST600ZB/l1', stringify({state: null, power_on_behavior: null}), {qos: 0, retain: false}, @@ -2039,28 +2031,28 @@ describe('HomeAssistant extension', () => { }); it('Shouldnt crash in onPublishEntityState on group publish', async () => { - logger.error.mockClear(); - MQTT.publish.mockClear(); - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); - expect(logger.error).toHaveBeenCalledTimes(0); + expect(mockLogger.error).toHaveBeenCalledTimes(0); group.members.pop(); }); it('Should counter an action payload with an empty payload', async () => { - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.WXKG11LM; + mockMQTT.publish.mockClear(); + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(4); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({ action: 'single', click: null, battery: null, @@ -2069,9 +2061,9 @@ describe('HomeAssistant extension', () => { power_outage_count: null, device_temperature: null, }); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({ + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({ action: '', click: null, battery: null, @@ -2080,21 +2072,21 @@ describe('HomeAssistant extension', () => { power_outage_count: null, device_temperature: null, }); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config'); - expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config'); + expect(mockMQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); }); it('Should clear outdated configs', async () => { // Non-existing group -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_91231/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_91231/light/config', '', {qos: 1, retain: true}, @@ -2102,33 +2094,33 @@ describe('HomeAssistant extension', () => { ); // Existing group -> dont clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing group with old topic structure (1.20.0) -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/9/light/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith('homeassistant/light/9/light/config', '', {qos: 1, retain: true}, expect.any(Function)); // Existing group, non existing config -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/light/1221051039810110150109113116116_9/switch/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/switch/config', '', {qos: 1, retain: true}, @@ -2136,42 +2128,47 @@ describe('HomeAssistant extension', () => { ); // Non-existing device -> clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', '', {qos: 1, retain: true}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x123/temperature/config', + '', + {qos: 1, retain: true}, + expect.any(Function), + ); // Existing device -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Non-existing device of different instance -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x123/temperature/config', stringify({availability: [{topic: 'zigbee2mqtt_different/bridge/state'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing device but non-existing config -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', '', {qos: 1, retain: true}, @@ -2179,52 +2176,52 @@ describe('HomeAssistant extension', () => { ); // Non-existing device but invalid payload -> clear - MQTT.publish.mockClear(); - await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', '1}3'); + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message('homeassistant/sensor/0x123/temperature/config', '1}3'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Existing device, device automation -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Device automation of different instance -> don't clear - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/device_automation/0x000b57fffec6a5b2_not_existing/action_button_3_single/config', stringify({topic: 'zigbee2mqtt_different/0x000b57fffec6a5b2_not_existing/availability'}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Device was flagged to be excluded from homeassistant discovery settings.set(['devices', '0x000b57fffec6a5b2', 'homeassistant'], null); await resetExtension(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); - await MQTT.events.message( + await mockMQTTEvents.message( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}]}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', '', {qos: 1, retain: true}, expect.any(Function), ); - MQTT.publish.mockClear(); - await MQTT.events.message( + mockMQTT.publish.mockClear(); + await mockMQTTEvents.message( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', stringify({topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/device_automation/0x000b57fffec6a5b2/action_button_3_single/config', '', {qos: 1, retain: true}, @@ -2236,10 +2233,9 @@ describe('HomeAssistant extension', () => { settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false); await resetExtension(); - let payload; await flushPromises(); - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -2252,7 +2248,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', @@ -2260,7 +2255,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -2269,9 +2264,9 @@ describe('HomeAssistant extension', () => { }); it('Should rediscover group when device is added to it', async () => { - resetDiscoveryPayloads(9); - MQTT.publish.mockClear(); - MQTT.events.message( + resetDiscoveryPayloads('9'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'ha_discovery_group', device: 'wall_switch_double/left'}), ); @@ -2315,7 +2310,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2362,7 +2357,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2390,7 +2385,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'IKEA', model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', name: 'bulb', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, effect: true, @@ -2407,7 +2401,7 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), {retain: true, qos: 1}, @@ -2431,7 +2425,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'IKEA', model: 'TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)', name: 'bulb', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, enabled_by_default: false, @@ -2447,7 +2440,7 @@ describe('HomeAssistant extension', () => { entity_category: 'diagnostic', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x000b57fffec6a5b2/last_seen/config', stringify(payload), {retain: true, qos: 1}, @@ -2459,11 +2452,9 @@ describe('HomeAssistant extension', () => { settings.set(['frontend', 'url'], 'http://zigbee.mqtt'); await resetExtension(); - - let payload; await flushPromises(); - payload = { + const payload = { unit_of_measurement: '°C', device_class: 'temperature', state_class: 'measurement', @@ -2477,7 +2468,6 @@ describe('HomeAssistant extension', () => { device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', manufacturer: 'Aqara', configuration_url: 'http://zigbee.mqtt/#/device/0x0017880104e45522/info', @@ -2486,7 +2476,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), {retain: true, qos: 1}, @@ -2496,15 +2486,18 @@ describe('HomeAssistant extension', () => { it('Should rediscover scenes when a scene is changed', async () => { // Device/endpoint scenes. - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color_2); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color_2)!; + assert('ieeeAddr' in device); resetDiscoveryPayloads(device.ieeeAddr); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitScenesChanged({entity: device}); await flushPromises(); // Discovery messages for scenes have been purged. - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, '', {retain: true, qos: 1}, @@ -2531,24 +2524,26 @@ describe('HomeAssistant extension', () => { origin: origin, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/0x000b57fffec6a5b4/scene_1/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(12); + expect(mockMQTT.publish).toHaveBeenCalledTimes(12); // Group scenes. + // @ts-expect-error private const group = controller.zigbee.resolveEntity('ha_discovery_group'); - resetDiscoveryPayloads(9); + resetDiscoveryPayloads('9'); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); + // @ts-expect-error private controller.eventBus.emitScenesChanged({entity: group}); await flushPromises(); // Discovery messages for scenes have been purged. - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, '', {retain: true, qos: 1}, @@ -2575,17 +2570,17 @@ describe('HomeAssistant extension', () => { origin: origin, availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( `homeassistant/scene/1221051039810110150109113116116_9/scene_4/config`, stringify(payload), {retain: true, qos: 1}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledTimes(6); + expect(mockMQTT.publish).toHaveBeenCalledTimes(6); }); it('Should not clear bridge entities unnecessarily', async () => { - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); const topic = 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config'; const payload = { @@ -2608,13 +2603,14 @@ describe('HomeAssistant extension', () => { availability_mode: 'all', }; + // @ts-expect-error private controller.eventBus.emitMQTTMessage({ topic: topic, message: stringify(payload), }); await flushPromises(); - expect(MQTT.publish).not.toHaveBeenCalledWith(topic, '', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publish).not.toHaveBeenCalledWith(topic, '', {retain: true, qos: 1}, expect.any(Function)); }); it('Should discover bridge entities', async () => { @@ -2645,7 +2641,7 @@ describe('HomeAssistant extension', () => { origin: origin, device: devicePayload, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/connection_state/config', stringify(payload), {retain: true, qos: 1}, @@ -2668,7 +2664,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/binary_sensor/1221051039810110150109113116116_0x00124b00120144ae/restart_required/config', stringify(payload), {retain: true, qos: 1}, @@ -2688,7 +2684,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/button/1221051039810110150109113116116_0x00124b00120144ae/restart/config', stringify(payload), {retain: true, qos: 1}, @@ -2711,7 +2707,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/select/1221051039810110150109113116116_0x00124b00120144ae/log_level/config', stringify(payload), {retain: true, qos: 1}, @@ -2732,7 +2728,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/version/config', stringify(payload), {retain: true, qos: 1}, @@ -2753,7 +2749,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/coordinator_version/config', stringify(payload), {retain: true, qos: 1}, @@ -2775,7 +2771,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/network_map/config', stringify(payload), {retain: true, qos: 1}, @@ -2796,7 +2792,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/sensor/1221051039810110150109113116116_0x00124b00120144ae/permit_join_timeout/config', stringify(payload), {retain: true, qos: 1}, @@ -2821,7 +2817,7 @@ describe('HomeAssistant extension', () => { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], availability_mode: 'all', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_0x00124b00120144ae/permit_join/config', stringify(payload), {retain: true, qos: 1}, @@ -2830,14 +2826,14 @@ describe('HomeAssistant extension', () => { }); it('Should remove discovery entries for removed exposes when device options change', async () => { - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/options', stringify({id: '0xf4ce368a38be56a1', options: {dimmer_1_enabled: 'false', dimmer_1_dimming_enabled: 'false'}}), ); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/light/0xf4ce368a38be56a1/light_l2/config', '', {retain: true, qos: 1}, @@ -2846,12 +2842,12 @@ describe('HomeAssistant extension', () => { }); it('Should publish discovery message when a converter announces changed exposes', async () => { - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices['BMCT-SLZ']; + mockMQTT.publish.mockClear(); + const device = devices['BMCT-SLZ']; const data = {deviceMode: 0}; const msg = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; resetDiscoveryPayloads('0x18fc26000000cafe'); - await zigbeeHerdsman.events.message(msg); + await mockZHEvents.message(msg); await flushPromises(); const payload = { availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], @@ -2861,7 +2857,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Bosch', model: 'Light/shutter control unit II (BMCT-SLZ)', name: '0x18fc26000000cafe', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, entity_category: 'config', @@ -2876,7 +2871,7 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.device_mode }}', enabled_by_default: true, }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'homeassistant/select/0x18fc26000000cafe/device_mode/config', stringify(payload), {retain: true, qos: 1}, diff --git a/test/networkMap.test.js b/test/extensions/networkMap.test.ts similarity index 83% rename from test/networkMap.test.js rename to test/extensions/networkMap.test.ts index 0f553d7589..f8f347de97 100644 --- a/test/networkMap.test.js +++ b/test/extensions/networkMap.test.ts @@ -1,61 +1,34 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const stringify = require('json-stable-stringify-without-jsonify'); -const fs = require('fs'); -const path = require('path'); -const {coordinator, bulb, bulb_color, WXKG02LM_rev1, CC2530_ROUTER, unsupported_router, external_converter_device} = zigbeeHerdsman.devices; - -zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); -zigbeeHerdsman.returnDevices.push(bulb_color.ieeeAddr); -zigbeeHerdsman.returnDevices.push(WXKG02LM_rev1.ieeeAddr); -zigbeeHerdsman.returnDevices.push(CC2530_ROUTER.ieeeAddr); -zigbeeHerdsman.returnDevices.push(unsupported_router.ieeeAddr); -zigbeeHerdsman.returnDevices.push(external_converter_device.ieeeAddr); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; -const setTimeoutNative = setTimeout; - -describe('Networkmap', () => { - let controller; +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; - beforeAll(async () => { - jest.useFakeTimers(); - Date.now = jest.fn(); - Date.now.mockReturnValue(10000); - data.writeDefaultConfiguration(); - settings.reRead(); - data.writeEmptyState(); - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); - settings.set(['external_converters'], ['mock-external-converter.js']); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - mocksClear.forEach((m) => m.mockClear()); - await flushPromises(); - }); +import fs from 'fs'; +import path from 'path'; - beforeEach(async () => { - mocksClear.forEach((m) => m.mockClear()); - await flushPromises(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = 1000; - external_converter_device.lastSeen = 1000; - global.setTimeout = (r) => r(); - }); +import stringify from 'json-stable-stringify-without-jsonify'; - afterEach(async () => { - global.setTimeout = setTimeoutNative; - }); +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; - afterAll(async () => { - jest.useRealTimers(); - }); +returnDevices.push( + devices.coordinator.ieeeAddr, + devices.bulb.ieeeAddr, + devices.bulb_color.ieeeAddr, + devices.WXKG02LM_rev1.ieeeAddr, + devices.CC2530_ROUTER.ieeeAddr, + devices.unsupported_router.ieeeAddr, + devices.external_converter_device.ieeeAddr, +); + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; - function mock() { +describe('Extension: NetworkMap', () => { + let controller: Controller; + + const mock = (): void => { /** * Topology * | -> external_device @@ -66,49 +39,96 @@ describe('Networkmap', () => { * | -> CC2530_ROUTER -> WXKG02LM_rev1 * */ - coordinator.lqi = jest.fn().mockResolvedValue({ + devices.coordinator.lqi.mockResolvedValueOnce({ neighbors: [ - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120}, - {ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, { - ieeeAddr: external_converter_device.ieeeAddr, - networkAddress: external_converter_device.networkAddress, + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 2, + depth: 1, + linkquality: 120, + }, + {ieeeAddr: devices.bulb.ieeeAddr, networkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, + { + ieeeAddr: devices.external_converter_device.ieeeAddr, + networkAddress: devices.external_converter_device.networkAddress, relationship: 2, depth: 1, linkquality: 92, }, ], }); - coordinator.routingTable = jest.fn().mockResolvedValue({ - table: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], + devices.coordinator.routingTable.mockResolvedValueOnce({ + table: [{destinationAddress: devices.CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: devices.bulb.networkAddress}], }); - bulb.lqi = jest.fn().mockResolvedValue({ + devices.bulb.lqi.mockResolvedValueOnce({ neighbors: [ - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110}, - {ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100}, + { + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 1, + depth: 2, + linkquality: 110, + }, + { + ieeeAddr: devices.CC2530_ROUTER.ieeeAddr, + networkAddress: devices.CC2530_ROUTER.networkAddress, + relationship: 1, + depth: 2, + linkquality: 100, + }, ], }); - bulb.routingTable = jest.fn().mockResolvedValue({table: []}); - bulb_color.lqi = jest.fn().mockResolvedValue({neighbors: []}); - bulb_color.routingTable = jest.fn().mockResolvedValue({table: []}); - CC2530_ROUTER.lqi = jest.fn().mockResolvedValue({ + devices.CC2530_ROUTER.lqi.mockResolvedValueOnce({ neighbors: [ - {ieeeAddr: '0x0000000000000000', networkAddress: WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, - {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130}, + {ieeeAddr: '0x0000000000000000', networkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, + { + ieeeAddr: devices.bulb_color.ieeeAddr, + networkAddress: devices.bulb_color.networkAddress, + relationship: 4, + depth: 2, + linkquality: 130, + }, ], }); - CC2530_ROUTER.routingTable = jest.fn().mockResolvedValue({table: []}); - unsupported_router.lqi = jest.fn().mockRejectedValue(new Error('failed')); - unsupported_router.routingTable = jest.fn().mockRejectedValue(new Error('failed')); - } + devices.unsupported_router.lqi.mockRejectedValueOnce('failed').mockRejectedValueOnce('failed'); + devices.unsupported_router.routingTable.mockRejectedValueOnce('failed').mockRejectedValueOnce('failed'); + }; + + beforeAll(async () => { + jest.useFakeTimers(); + mockSleep.mock(); + jest.spyOn(Date, 'now').mockReturnValue(10000); + data.writeDefaultConfiguration(); + settings.reRead(); + data.writeEmptyState(); + fs.copyFileSync(path.join(__dirname, '..', 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); + settings.set(['external_converters'], ['mock-external-converter.js']); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + }); + + beforeEach(async () => { + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + const device = devices.bulb_color; + device.lastSeen = 1000; + devices.external_converter_device.lastSeen = 1000; + }); + + afterEach(async () => {}); + + afterAll(async () => { + mockSleep.restore(); + jest.useRealTimers(); + }); it('Output raw networkmap', async () => { mock(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -196,7 +216,7 @@ describe('Networkmap', () => { friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, + // modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -291,23 +311,22 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); it('Output graphviz networkmap', async () => { mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; + const device = devices.bulb_color; + device.lastSeen = undefined; const endpoint = device.getEndpoint(1); const data = {modelID: 'test'}; const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true})); + await mockZHEvents.message(payload); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = `digraph G { node[shape=record]; @@ -327,7 +346,7 @@ describe('Networkmap', () => { }`; const expectedLines = expected.split('\n'); - const actualLines = JSON.parse(call[1]).data.value.split('\n'); + const actualLines = JSON.parse(mockMQTT.publish.mock.calls[0][1]).data.value.split('\n'); for (let i = 0; i < expectedLines.length; i++) { expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); @@ -336,17 +355,16 @@ describe('Networkmap', () => { it('Output plantuml networkmap', async () => { mock(); - const device = zigbeeHerdsman.devices.bulb_color; - device.lastSeen = null; + const device = devices.bulb_color; + device.lastSeen = undefined; const endpoint = device.getEndpoint(1); const data = {modelID: 'test'}; const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true})); + await mockZHEvents.message(payload); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = `' paste into: https://www.planttext.com/ @@ -429,7 +447,7 @@ describe('Networkmap', () => { @enduml`; const expectedLines = expected.split('\n'); - const actualLines = JSON.parse(call[1]).data.value.split('\n'); + const actualLines = JSON.parse(mockMQTT.publish.mock.calls[0][1]).data.value.split('\n'); for (let i = 0; i < expectedLines.length; i++) { expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); @@ -438,10 +456,11 @@ describe('Networkmap', () => { it('Should throw error when requesting invalid type', async () => { mock(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/networkmap', stringify({data: {}, status: 'error', error: "Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}), {retain: false, qos: 0}, @@ -452,12 +471,11 @@ describe('Networkmap', () => { it('Should exclude disabled devices from networkmap', async () => { settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true); mock(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -516,12 +534,10 @@ describe('Networkmap', () => { ], nodes: [ { - // definition: null, failed: [], friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -600,20 +616,19 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); it('Handles retrying request when first attempt fails', async () => { settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true); mock(); - bulb.lqi.mockRejectedValueOnce(new Error('failed')); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); + devices.bulb.lqi.mockRejectedValueOnce('failed'); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - let call = MQTT.publish.mock.calls[0]; - expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); const expected = { data: { @@ -672,12 +687,10 @@ describe('Networkmap', () => { ], nodes: [ { - // definition: null, failed: [], friendlyName: 'Coordinator', ieeeAddr: '0x00124b00120144ae', lastSeen: 1000, - modelID: null, networkAddress: 0, type: 'Coordinator', }, @@ -756,7 +769,7 @@ describe('Networkmap', () => { }, status: 'ok', }; - const actual = JSON.parse(call[1]); + const actual = JSON.parse(mockMQTT.publish.mock.calls[0][1]); expect(actual).toStrictEqual(expected); }); }); diff --git a/test/extensions/onEvent.test.ts b/test/extensions/onEvent.test.ts new file mode 100644 index 0000000000..e98f2a022d --- /dev/null +++ b/test/extensions/onEvent.test.ts @@ -0,0 +1,88 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT} from '../mocks/mqtt'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import * as zhc from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mockOnEvent = jest.fn(); +const mockLivoloOnEvent = jest.fn(); +const mappedLivolo = zhc.findByModel('TI0001'); +mappedLivolo.onEvent = mockLivoloOnEvent; +// @ts-expect-error mock +zhc.onEvent = mockOnEvent; + +const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug]; + +describe('Extension: OnEvent', () => { + let controller: Controller; + + beforeEach(async () => { + jest.useFakeTimers(); + data.writeDefaultConfiguration(); + settings.reRead(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + await flushPromises(); + }); + + beforeEach(async () => { + // @ts-expect-error private + controller.state.state = {}; + data.writeDefaultConfiguration(); + settings.reRead(); + mocksClear.forEach((m) => m.mockClear()); + mockOnEvent.mockClear(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + it('Should call with start event', async () => { + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + const call = mockLivoloOnEvent.mock.calls[0]; + expect(call[0]).toBe('start'); + expect(call[1]).toStrictEqual({}); + expect(call[2]).toBe(devices.LIVOLO); + expect(call[3]).toStrictEqual(settings.getDevice(devices.LIVOLO.ieeeAddr)); + expect(call[4]).toStrictEqual({}); + }); + + it('Should call with stop event', async () => { + mockLivoloOnEvent.mockClear(); + await controller.stop(); + await flushPromises(); + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + const call = mockLivoloOnEvent.mock.calls[0]; + expect(call[0]).toBe('stop'); + expect(call[1]).toStrictEqual({}); + expect(call[2]).toBe(devices.LIVOLO); + }); + + it('Should call with zigbee event', async () => { + mockLivoloOnEvent.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.LIVOLO}); + await flushPromises(); + expect(mockLivoloOnEvent).toHaveBeenCalledTimes(1); + expect(mockLivoloOnEvent).toHaveBeenCalledWith( + 'deviceAnnounce', + {device: devices.LIVOLO}, + devices.LIVOLO, + settings.getDevice(devices.LIVOLO.ieeeAddr), + {}, + ); + }); + + it('Should call index onEvent with zigbee event', async () => { + mockOnEvent.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.LIVOLO}); + await flushPromises(); + expect(mockOnEvent).toHaveBeenCalledTimes(1); + expect(mockOnEvent).toHaveBeenCalledWith('deviceAnnounce', {device: devices.LIVOLO}, devices.LIVOLO); + }); +}); diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts new file mode 100644 index 0000000000..97e3a8aa14 --- /dev/null +++ b/test/extensions/otaUpdate.test.ts @@ -0,0 +1,426 @@ +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import path from 'path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import OTAUpdate from 'lib/extension/otaUpdate'; + +import * as zhc from 'zigbee-herdsman-converters'; +import {zigbeeOTA} from 'zigbee-herdsman-converters/lib/ota'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, devices.bulb.save, mockLogger.info]; + +describe('Extension: OTAUpdate', () => { + let controller: Controller; + let mapped: zhc.Definition; + let updateToLatestSpy: jest.SpyInstance; + let isUpdateAvailableSpy: jest.SpyInstance; + + const resetExtension = async (): Promise => { + await controller.enableDisableExtension(false, 'OTAUpdate'); + await controller.enableDisableExtension(true, 'OTAUpdate'); + }; + + beforeAll(async () => { + jest.useFakeTimers(); + mockSleep.mock(); + data.writeDefaultConfiguration(); + settings.reRead(); + settings.set(['ota', 'ikea_ota_use_test_url'], true); + settings.reRead(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + // @ts-expect-error minimal mock + mapped = await zhc.findByDevice(devices.bulb); + updateToLatestSpy = jest.spyOn(mapped.ota!, 'updateToLatest'); + isUpdateAvailableSpy = jest.spyOn(mapped.ota!, 'isUpdateAvailable'); + await flushPromises(); + }); + + afterAll(async () => { + mockSleep.restore(); + jest.useRealTimers(); + }); + + beforeEach(async () => { + // @ts-expect-error private + const extension: OTAUpdate = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate'); + // @ts-expect-error private + extension.lastChecked = {}; + // @ts-expect-error private + extension.inProgress = new Set(); + mocksClear.forEach((m) => m.mockClear()); + devices.bulb.save.mockClear(); + devices.bulb.endpoints[0].commandResponse.mockClear(); + updateToLatestSpy.mockClear(); + isUpdateAvailableSpy.mockClear(); + // @ts-expect-error private + controller.state.state = {}; + }); + + afterEach(async () => { + settings.set(['ota', 'disable_automatic_update_check'], false); + }); + + it('Should OTA update a device', async () => { + let count = 0; + devices.bulb.endpoints[0].read.mockImplementation(() => { + count++; + return {swBuildId: count, dateCode: '2019010' + count}; + }); + updateToLatestSpy.mockImplementationOnce((device, onProgress) => { + onProgress(0, null); + onProgress(10, 3600.2123); + return 90; + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); + await flushPromises(); + expect(mockLogger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(0); + expect(updateToLatestSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledWith(devices.bulb, expect.any(Function)); + expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); + expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); + expect(mockLogger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); + expect(mockLogger.info).toHaveBeenCalledWith( + `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, + ); + expect(devices.bulb.save).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); + expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'updating', progress: 0}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'updating', progress: 10, remaining: 3600}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/update', + stringify({ + data: {from: {date_code: '20190101', software_build_id: 1}, id: 'bulb', to: {date_code: '20190102', software_build_id: 2}}, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + }); + + it('Should handle when OTA update fails', async () => { + devices.bulb.endpoints[0].read.mockImplementation(() => { + return {swBuildId: 1, dateCode: '2019010'}; + }); + devices.bulb.save.mockClear(); + updateToLatestSpy.mockImplementationOnce(() => { + throw new Error('Update failed'); + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'})); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'available'}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/update', + stringify({data: {id: 'bulb'}, status: 'error', error: "Update of 'bulb' failed (Update failed)"}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should be able to check if OTA update is available', async () => { + isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + + mockMQTT.publish.mockClear(); + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(2); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should handle if OTA update check fails', async () => { + isUpdateAvailableSpy.mockImplementationOnce(() => { + throw new Error('RF signals disturbed because of dogs barking'); + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(updateToLatestSpy).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({ + data: {id: 'bulb'}, + status: 'error', + error: `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`, + }), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should fail when device does not exist', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo'); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'not_existing_deviceooo'}, status: 'error', error: `Device 'not_existing_deviceooo' does not exist`}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should not check for OTA when device does not support it', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch'); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'dimmer_wall_switch'}, status: 'error', error: `Device 'dimmer_wall_switch' does not support OTA updates`}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should refuse to check/update when already in progress', async () => { + isUpdateAvailableSpy.mockImplementationOnce(() => { + return new Promise((resolve) => { + setTimeout(() => resolve(), 99999); + }); + }); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + jest.runOnlyPendingTimers(); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/check', + stringify({data: {id: 'bulb'}, status: 'error', error: `Update or check for update already in progress for 'bulb'`}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Shouldnt crash when read modelID before/after OTA update fails', async () => { + devices.bulb.endpoints[0].read.mockRejectedValueOnce('Failed from').mockRejectedValueOnce('Failed to'); + updateToLatestSpy.mockImplementation(); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/ota_update/update', + stringify({data: {id: 'bulb', from: null, to: null}, status: 'ok'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should check for update when device requests it', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + mockLogger.info.mockClear(); + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(mockLogger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + + // Should not request again when device asks again after a short time + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + + mockLogger.info.mockClear(); + await mockZHEvents.message(payload); + await flushPromises(); + expect(mockLogger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockRejectedValueOnce('Nothing to find here'); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle'}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should check for update when device requests it and it is not available', async () => { + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1); + expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382}); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}), + {retain: true, qos: 0}, + expect.any(Function), + ); + }); + + it('Should not check for update when device requests it and disable_automatic_update_check is set to true', async () => { + settings.set(['ota', 'disable_automatic_update_check'], true); + const data = {imageType: 12382}; + isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13}); + const payload = { + data, + cluster: 'genOta', + device: devices.bulb, + endpoint: devices.bulb.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(0); + }); + + it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => { + const device = devices.HGZB04D; + const data = {imageType: 12382}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); + }); + + it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint to standard endpoint', async () => { + const device = devices.SV01; + const data = {imageType: 12382}; + const payload = { + data, + cluster: 'genOta', + device, + endpoint: device.getEndpoint(1)!, + type: 'commandQueryNextImageRequest', + linkquality: 10, + meta: {zclTransactionSequenceNumber: 10}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); + expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); + }); + + it('Set zigbee_ota_override_index_location', async () => { + const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride'); + settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json'); + await resetExtension(); + expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json')); + spyUseIndexOverride.mockClear(); + + settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json'); + await resetExtension(); + expect(spyUseIndexOverride).toHaveBeenCalledWith('http://my.site/index.json'); + spyUseIndexOverride.mockClear(); + }); + + it('Clear update state on startup', async () => { + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); + // @ts-expect-error private + controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}}); + await resetExtension(); + // @ts-expect-error private + expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}}); + }); +}); diff --git a/test/publish.test.js b/test/extensions/publish.test.ts similarity index 56% rename from test/publish.test.js rename to test/extensions/publish.test.ts index 36f5e060e8..e986597e55 100644 --- a/test/publish.test.js +++ b/test/extensions/publish.test.ts @@ -1,199 +1,203 @@ -const data = require('./stub/data'); -const sleep = require('./stub/sleep'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const {loadTopicGetSetRegex} = require('../lib/extension/publish'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const stringify = require('json-stable-stringify-without-jsonify'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -const expectNothingPublished = () => { - Object.values(zigbeeHerdsman.devices).forEach((d) => { - d.endpoints.forEach((e) => { - expect(e.command).toHaveBeenCalledTimes(0); - expect(e.read).toHaveBeenCalledTimes(0); - expect(e.write).toHaveBeenCalledTimes(0); - }); - }); - Object.values(zigbeeHerdsman.groups).forEach((g) => { - expect(g.command).toHaveBeenCalledTimes(0); - }); -}; +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {toZigbee} from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import {loadTopicGetSetRegex} from '../../lib/extension/publish'; +import * as settings from '../../lib/util/settings'; -describe('Publish', () => { - let controller; +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Publish', () => { + let controller: Controller; + + const expectNothingPublished = (): void => { + Object.values(devices).forEach((d) => { + d.endpoints.forEach((e) => { + expect(e.command).toHaveBeenCalledTimes(0); + expect(e.read).toHaveBeenCalledTimes(0); + expect(e.write).toHaveBeenCalledTimes(0); + }); + }); + Object.values(groups).forEach((g) => { + expect(g.command).toHaveBeenCalledTimes(0); + }); + }; beforeAll(async () => { jest.useFakeTimers(); data.writeEmptyState(); controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); + mockSleep.mock(); await controller.start(); await flushPromises(); }); beforeEach(async () => { data.writeDefaultConfiguration(); + // @ts-expect-error private controller.state.state = {}; settings.reRead(); loadTopicGetSetRegex(); mocksClear.forEach((m) => m.mockClear()); - Object.values(zigbeeHerdsman.devices).forEach((d) => { + Object.values(devices).forEach((d) => { d.endpoints.forEach((e) => { e.command.mockClear(); e.read.mockClear(); e.write.mockClear(); }); }); - Object.values(zigbeeHerdsman.groups).forEach((g) => { + Object.values(groups).forEach((g) => { g.command.mockClear(); }); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + toZigbee.__clearStore__(); }); afterAll(async () => { await jest.runOnlyPendingTimersAsync(); jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); }); it('Should publish messages to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should corretly handle mallformed messages', async () => { - await MQTT.events.message('zigbee2mqtt/foo', ''); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', ''); + await mockMQTTEvents.message('zigbee2mqtt/foo', ''); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', ''); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices when there is no converters', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_no: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness_no: '200'})); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices when there is a get converter but no set', async () => { - await MQTT.events.message('zigbee2mqtt/thermostat/set', stringify({relay_status_log_rsp: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/thermostat/set', stringify({relay_status_log_rsp: '200'})); await flushPromises(); expectNothingPublished(); }); it('Should publish messages to zigbee devices with complicated topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'wohnzimmer.light.wall.right'); - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/wohnzimmer.light.wall.right/set', stringify({state: 'ON'})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/wohnzimmer.light.wall.right/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in %', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_percent: '92'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness_percent: '92'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 235, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices when brightness is in number', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 230})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 230})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 230, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: '222'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: '222'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 222, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color_temp in %', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: 100})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp_percent: 100})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 500, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep', async () => { - const device = zigbeeHerdsman.devices.QBKG04LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch/set', stringify({state: 'OFF'})); + const device = devices.QBKG04LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with non-default ep and postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/right/set', stringify({state: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/right/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'}); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with endpoint ID', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/3/set', stringify({state: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/3/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/wall_switch_double', stringify({state_right: 'OFF'}), {qos: 0, retain: false}, @@ -202,26 +206,26 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices to non default-ep with state_[EP]', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/set', stringify({state_right: 'OFF'})); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_right: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices to non default-ep with brightness_[EP]', async () => { - const device = zigbeeHerdsman.devices.QS_Zigbee_D02_TRIAC_2C_LN; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/0x0017882194e45543/set', stringify({state_l2: 'ON', brightness_l2: 50})); + const device = devices.QS_Zigbee_D02_TRIAC_2C_LN; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/0x0017882194e45543/set', stringify({state_l2: 'ON', brightness_l2: 50})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN', stringify({brightness_l2: 50, state_l2: 'ON'}), {retain: false, qos: 0}, @@ -230,9 +234,9 @@ describe('Publish', () => { }); it('Should publish messages to Tuya switch with dummy endpoints', async () => { - const device = zigbeeHerdsman.devices.TS0601_switch; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/TS0601_switch/set', stringify({state_l2: 'ON'})); + const device = devices.TS0601_switch; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/TS0601_switch/set', stringify({state_l2: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -241,7 +245,7 @@ describe('Publish', () => { {dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_switch', stringify({state_l2: 'ON'}), {retain: false, qos: 0}, @@ -250,11 +254,11 @@ describe('Publish', () => { }); it('Should publish messages to Tuya cover switch with dummy endpoints', async () => { - const device = zigbeeHerdsman.devices.TS0601_cover_switch; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state: 'OPEN'})); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state_l1: 'ON'})); - await MQTT.events.message('zigbee2mqtt/TS0601_cover_switch/l2/set', stringify({state: 'OFF'})); + const device = devices.TS0601_cover_switch; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state: 'OPEN'})); + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/set', stringify({state_l1: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/TS0601_cover_switch/l2/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command).toHaveBeenCalledWith( @@ -275,7 +279,7 @@ describe('Publish', () => { {dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/TS0601_cover_switch', stringify({state_l2: 'OFF', state_l1: 'ON', state: 'OPEN'}), {retain: false, qos: 0}, @@ -284,28 +288,28 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices with color xy and state', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON'}), {retain: false, qos: 0}, @@ -314,15 +318,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy and brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON', brightness: 20}), {retain: false, qos: 0}, @@ -331,15 +335,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy, brightness and state on', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color: {x: 100, y: 50}, state: 'ON', brightness: 20, color_mode: 'xy'}), {retain: false, qos: 0}, @@ -348,15 +352,15 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color xy, brightness and state off', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command).toHaveBeenNthCalledWith(1, 'lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {}); expect(endpoint.command).toHaveBeenNthCalledWith(2, 'genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'OFF'}), {retain: false, qos: 0}, @@ -365,70 +369,70 @@ describe('Publish', () => { }); it('Should publish messages to zigbee devices with color rgb object', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {r: 100, g: 200, b: 10}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {r: 100, g: 200, b: 10}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should publish messages to zigbee devices with color rgb string', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {rgb: '100,200,10'}})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {rgb: '100,200,10'}})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should publish messages to zigbee devices with brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: '50'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: '50'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); }); it('Should publish messages groups', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); group.members.pop(); }); it('Should publish messages to groups with brightness_percent', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({brightness_percent: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 128, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128}); group.members.pop(); }); it('Should publish messages to groups when converter is not in the default list but device in it supports it', async () => { - const group = zigbeeHerdsman.groups.thermostat_group; - await MQTT.events.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'})); + const group = groups.thermostat_group; + await mockMQTTEvents.message('zigbee2mqtt/thermostat_group/set', stringify({child_lock: 'LOCK'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith( @@ -440,132 +444,134 @@ describe('Publish', () => { }); it('Should publish messages to groups with on and brightness', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50}); group.members.pop(); }); it('Should publish messages to groups with off and brightness', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF', brightness: 50})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'OFF'}); group.members.pop(); }); it('Should publish messages to groups color', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.37, y: 0.28}})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 24248, colory: 18350, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'}); group.members.pop(); }); it('Should publish messages to groups color temperature', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100})); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'}); group.members.pop(); }); it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => { - settings.addGroup('group_12312', 12312); - expect(Object.values(zigbeeHerdsman.groups).length).toBe(10); - await MQTT.events.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'})); + settings.addGroup('group_12312', '12312'); + expect(Object.values(groups).length).toBe(10); + await mockMQTTEvents.message('zigbee2mqtt/group_12312/set', stringify({state: 'ON'})); await flushPromises(); - expect(Object.values(zigbeeHerdsman.groups).length).toBe(11); + expect(Object.values(groups).length).toBe(11); // group contains no device - expect(zigbeeHerdsman.groups.group_12312.command).toHaveBeenCalledTimes(0); - delete zigbeeHerdsman.groups.group_12312; + // @ts-expect-error runtime mock + expect(groups.group_12312.command).toHaveBeenCalledTimes(0); + // @ts-expect-error runtime mock + delete groups.group_12312; }); it('Shouldnt publish new state when optimistic = false', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; settings.set(['devices', device.ieeeAddr, 'optimistic'], false); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Shouldnt publish new brightness state when filtered_optimistic is used', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; settings.set(['devices', device.ieeeAddr, 'filtered_optimistic'], ['brightness']); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); }); it('Shouldnt publish new state when optimistic = false for group', async () => { settings.set(['groups', '2', 'optimistic'], false); - await MQTT.events.message('zigbee2mqtt/group_2/set', stringify({brightness: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({brightness: '200'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt1/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt1/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt1/bulb_color/sett', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt1/bulb_color/sett', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/write', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/write', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('zigbee2mqtt/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle non-valid topics', async () => { - await MQTT.events.message('set', stringify({state: 'ON'})); + await mockMQTTEvents.message('set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should handle get', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/get', stringify({state: '', brightness: ''})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/get', stringify({state: '', brightness: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(2); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -573,10 +579,10 @@ describe('Publish', () => { }); it('Should handle get with multiple endpoints', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - const endpoint3 = device.getEndpoint(3); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_left: '', state_right: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + const endpoint3 = device.getEndpoint(3)!; + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_left: '', state_right: ''})); await flushPromises(); expect(endpoint2.read).toHaveBeenCalledTimes(1); expect(endpoint2.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -585,10 +591,10 @@ describe('Publish', () => { }); it('Should handle get with multiple cover endpoints', async () => { - const device = zigbeeHerdsman.devices.zigfred_plus; - const endpoint11 = device.getEndpoint(11); - const endpoint12 = device.getEndpoint(12); - await MQTT.events.message('zigbee2mqtt/zigfred_plus/get', stringify({state_l6: '', state_l7: ''})); + const device = devices.zigfred_plus; + const endpoint11 = device.getEndpoint(11)!; + const endpoint12 = device.getEndpoint(12)!; + await mockMQTTEvents.message('zigbee2mqtt/zigfred_plus/get', stringify({state_l6: '', state_l7: ''})); await flushPromises(); expect(endpoint11.read).toHaveBeenCalledTimes(1); expect(endpoint11.read).toHaveBeenCalledWith('closuresWindowCovering', ['currentPositionLiftPercentage']); @@ -597,31 +603,31 @@ describe('Publish', () => { }); it('Should log error when device has no such endpoint (via topic)', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/center/get', stringify({state: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/center/get', stringify({state: ''})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Device 'wall_switch_double' has no endpoint 'center'`); + expect(mockLogger.error).toHaveBeenCalledWith(`Device 'wall_switch_double' has no endpoint 'center'`); expect(endpoint2.read).toHaveBeenCalledTimes(0); }); it('Should log error when device has no definition', async () => { - const device = zigbeeHerdsman.devices.interviewing; - logger.error.mockClear(); - await MQTT.events.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'})); + const device = devices.interviewing; + mockLogger.error.mockClear(); + await mockMQTTEvents.message(`zigbee2mqtt/${device.ieeeAddr}/set`, stringify({state: 'OFF'})); await flushPromises(); - expect(logger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m'); + expect(mockLogger.log).toHaveBeenCalledWith('error', `Cannot publish to unsupported device 'button_double_key_interviewing'`, 'z2m'); }); it('Should log error when device has no such endpoint (via property)', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint2 = device.getEndpoint(2); - const endpoint3 = device.getEndpoint(3); - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_center: '', state_right: ''})); + const device = devices.QBKG03LM; + const endpoint2 = device.getEndpoint(2)!; + const endpoint3 = device.getEndpoint(3)!; + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/0x0017880104e45542/get', stringify({state_center: '', state_right: ''})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`No converter available for 'state_center' ("")`); + expect(mockLogger.error).toHaveBeenCalledWith(`No converter available for 'state_center' ("")`); expect(endpoint2.read).toHaveBeenCalledTimes(0); expect(endpoint3.read).toHaveBeenCalledTimes(1); expect(endpoint3.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -630,19 +636,19 @@ describe('Publish', () => { it('Should parse topic with when base topic has multiple slashes', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/at/my/home/bulb_color/get', stringify({state: ''})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/at/my/home/bulb_color/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); }); it('Should parse topic with when deviceID has multiple slashes', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/floor0/basement/my_device_id2/get', stringify({state: ''})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/floor0/basement/my_device_id2/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); @@ -651,57 +657,57 @@ describe('Publish', () => { it('Should parse topic with when base and deviceID have multiple slashes', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/basement'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/get', stringify({state: ''})); + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/get', stringify({state: ''})); await flushPromises(); expect(endpoint.read).toHaveBeenCalledTimes(1); expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); }); it('Should parse set with attribute topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set/state', 'ON'); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set/state', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with color attribute topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A'); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set/color', '#64C80A'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'}); }); it('Should parse set with ieeeAddr topic', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/0x000b57fffec6a5b3/set', stringify({state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/0x000b57fffec6a5b3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with non-existing postfix', async () => { - await MQTT.events.message('zigbee2mqtt/wall_switch_double/invalid/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/invalid/set', stringify({state: 'ON'})); await flushPromises(); expectNothingPublished(); }); it('Should allow to invert cover', async () => { - const device = zigbeeHerdsman.devices.J1; - const endpoint = device.getEndpoint(1); + const device = devices.J1_cover; + const endpoint = device.getEndpoint(1)!; // Non-inverted (open = 100, close = 0) - await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); + await mockMQTTEvents.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 10}, {}); @@ -709,30 +715,30 @@ describe('Publish', () => { // // Inverted endpoint.command.mockClear(); settings.set(['devices', device.ieeeAddr, 'invert_cover'], true); - await MQTT.events.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); + await mockMQTTEvents.message('zigbee2mqtt/J1_cover/set', stringify({position: 90})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'goToLiftPercentage', {percentageliftvalue: 90}, {}); }); it('Should send state update on toggle specific endpoint', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'TOGGLE'); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'TOGGLE'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/wall_switch_double', stringify({state_left: 'ON'}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF'}), {qos: 0, retain: false}, @@ -741,17 +747,17 @@ describe('Publish', () => { }); it('Should not use state converter on non-json message when value is not on/off/toggle', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON_RANDOM'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON_RANDOM'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(0); }); it('Should parse set with postfix topic and attribute', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); @@ -760,39 +766,37 @@ describe('Publish', () => { it('Should parse set with and slashes in base and deviceID postfix topic', async () => { settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); loadTopicGetSetRegex(); - const device = zigbeeHerdsman.devices.QBKG03LM; + const device = devices.QBKG03LM; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'in/basement/wall_switch_double'); - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/at/my/home/in/basement/wall_switch_double/left/set', stringify({state: 'ON'})); + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/at/my/home/in/basement/wall_switch_double/left/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should parse set with number at the end of friendly_name and postfix', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; + const device = devices.QBKG03LM; settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'ground_floor/kitchen/wall_switch/2'); - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', stringify({state: 'ON'})); + const endpoint = device.getEndpoint(2)!; + await mockMQTTEvents.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); }); it('Should not publish messages to zigbee devices when payload is invalid', async () => { - const device = zigbeeHerdsman.devices.QBKG03LM; - const endpoint = device.getEndpoint(2); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: true})); - await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: 1})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: true})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/left/set', stringify({state: 1})); await flushPromises(); expectNothingPublished(); }); it('Should set state before color', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', color: {x: 0.701, y: 0.299}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); @@ -800,29 +804,29 @@ describe('Publish', () => { }); it('Should also use on/off cluster when controlling group with switch', async () => { - const group = zigbeeHerdsman.groups.group_with_switch; + const group = groups.group_with_switch; - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'ON', brightness: 100})); + await mockMQTTEvents.message('zigbee2mqtt/switch_group/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(2); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 0}, {}); expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON', brightness: 100}), {retain: false, qos: 0}, expect.any(Function), ); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {}); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'OFF', brightness: 100}), {retain: false, qos: 0}, @@ -831,36 +835,36 @@ describe('Publish', () => { }); it('Should use transition when brightness with group', async () => { - const group = zigbeeHerdsman.groups.group_1; - group.members.push(zigbeeHerdsman.devices.bulb_color.getEndpoint(1)); + const group = groups.group_1; + group.members.push(devices.bulb_color.getEndpoint(1)!); settings.set(['groups', '1', 'transition'], 20); - await MQTT.events.message('zigbee2mqtt/group_1/set', stringify({brightness: 100})); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({brightness: 100})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 200}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100}); group.members.pop(); }); it('Should use transition on brightness command', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 20}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 200}, {}]); }); it('Should use transition from device_options on brightness command', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['device_options'], {transition: 20}); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 20}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 200}, {}]); @@ -868,13 +872,13 @@ describe('Publish', () => { it('Should turn bulb on with correct brightness when device is turned off twice and brightness is reported', async () => { // Test case for: https://github.com/Koenkk/zigbee2mqtt/issues/5413 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 200})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 200})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); await flushPromises(); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, @@ -883,41 +887,41 @@ describe('Publish', () => { linkquality: 10, }); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(5); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(5); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 3, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 4, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 5, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), @@ -934,20 +938,20 @@ describe('Publish', () => { it('Should turn bulb on with full brightness when transition is used and no brightness is known', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3799 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0.5})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 0.5})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0.5})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0.5})); await flushPromises(); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 254}), @@ -961,13 +965,13 @@ describe('Publish', () => { it('Transition parameter should not influence brightness on state ON', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3563 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 50})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 50})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); @@ -976,21 +980,21 @@ describe('Publish', () => { }); it('Should use transition when color temp', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {color_temp: 200}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 200, transtime: 200}, {}]); }); it('Should use transition only once when setting brightness and color temperature for TRADFRI', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', brightness: 20, color_temp: 200, transition: 20}; - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); @@ -998,8 +1002,8 @@ describe('Publish', () => { }); it('Should use transition only once when setting brightness and color temperature for group which contains TRADFRI', async () => { - const group = zigbeeHerdsman.groups.group_with_tradfri; - await MQTT.events.message('zigbee2mqtt/group_with_tradfri/set', stringify({state: 'ON', transition: 60, brightness: 20, color_temp: 400})); + const group = groups.group_with_tradfri; + await mockMQTTEvents.message('zigbee2mqtt/group_with_tradfri/set', stringify({state: 'ON', transition: 60, brightness: 20, color_temp: 400})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(2); expect(group.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); @@ -1007,21 +1011,21 @@ describe('Publish', () => { }); it('Message transition should overrule options transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; + const device = devices.bulb_color; settings.set(['devices', device.ieeeAddr, 'transition'], 20); - const endpoint = device.getEndpoint(1); + const endpoint = device.getEndpoint(1)!; const payload = {brightness: 200, transition: 10}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 100}, {}]); }); it('Should set state with brightness before color', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', color: {x: 0.701, y: 0.299}, transition: 3, brightness: 100}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 30}, {}]); @@ -1029,32 +1033,32 @@ describe('Publish', () => { }); it('Should turn device off when brightness 0 is send', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 0}, {}]); expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 0}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[2]).toEqual([ + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, @@ -1063,32 +1067,32 @@ describe('Publish', () => { }); it('Should turn device off when brightness 0 is send with transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 50, state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0, transition: 3})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 0, transition: 3})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 0}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[2]).toEqual([ + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 50}), {qos: 0, retain: false}, @@ -1097,10 +1101,10 @@ describe('Publish', () => { }); it('Should allow to set color via hue and saturation', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {color: {hue: 250, saturation: 50}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual([ @@ -1109,100 +1113,100 @@ describe('Publish', () => { {direction: 0, enhancehue: 45510, saturation: 127, transtime: 0}, {}, ]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {hue: 250, saturation: 50}, color_mode: 'hs'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {hue: 250, saturation: 50}, color_mode: 'hs'}); }); it('ZNCLDJ11LM open', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'OPEN'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 100}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {position: 10}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 10}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'CLOSE'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.write).toHaveBeenCalledTimes(1); expect(endpoint.write).toHaveBeenCalledWith('genAnalogOutput', {presentValue: 0}); }); it('ZNCLDJ11LM position', async () => { - const device = zigbeeHerdsman.devices.ZNCLDJ11LM; - const endpoint = device.getEndpoint(1); + const device = devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'STOP'}; - await MQTT.events.message('zigbee2mqtt/curtain/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/curtain/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('closuresWindowCovering', 'stop', {}, {}); }); it('Should turn device on with on/off when transition is provided', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', transition: 3}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 254, transtime: 30}, {}]); }); it('Should turn device on with on/off with transition when transition 0 is provided', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON', transition: 0}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 254, transtime: 0}, {}]); }); it('Should turn device off with onOff on off with transition', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'OFF', transition: 1}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 10}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); }); it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; // Set initial brightness in state - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); await flushPromises(); endpoint.command.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // Turn off - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {qos: 0, retain: false}, @@ -1210,7 +1214,7 @@ describe('Publish', () => { ]); // Bulb reports brightness while decreasing brightness - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, @@ -1219,8 +1223,8 @@ describe('Publish', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {qos: 0, retain: false}, @@ -1228,12 +1232,12 @@ describe('Publish', () => { ]); // Turn on again - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 3})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 30}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[2]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: false}, @@ -1242,21 +1246,21 @@ describe('Publish', () => { }); it('When device is turned off with transition and turned on WITHOUT transition it should restore the brightness', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; // Set initial brightness in state - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200})); await flushPromises(); endpoint.command.mockClear(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); // Turn off - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {qos: 0, retain: false}, @@ -1264,12 +1268,12 @@ describe('Publish', () => { ]); // Turn on again - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: false}, @@ -1279,48 +1283,54 @@ describe('Publish', () => { it('Home Assistant: should set state', async () => { settings.set(['homeassistant'], true); - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {state: 'ON'}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); }); it('Home Assistant: should not set state when color temperature is also set and device is already on', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'ON'}); - const endpoint = device.zh.getEndpoint(1); + const endpoint = device.zh.getEndpoint(1)!; const payload = {state: 'ON', color_temp: 100}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'}); }); it('Home Assistant: should set state when color temperature is also set and device is off', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'OFF'}); - const endpoint = device.zh.getEndpoint(1); + const endpoint = device.zh.getEndpoint(1)!; const payload = {state: 'ON', color_temp: 100}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', stringify({state: 'ON', color_temp: 100, color_mode: 'color_temp'}), {retain: false, qos: 0}, @@ -1330,35 +1340,38 @@ describe('Publish', () => { it('Home Assistant: should not set state when color is also set', async () => { settings.set(['homeassistant'], true); - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.remove(device); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr)!; + // @ts-expect-error private + controller.state.remove(devices.bulb_color.ieeeAddr); + // @ts-expect-error private controller.state.set(device, {state: 'ON'}); - const endpoint = device.zh.getEndpoint(1); + const endpoint = device.zh.getEndpoint(1)!; const payload = {state: 'ON', color: {x: 0.41, y: 0.25}}; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 26869, colory: 16384, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'}); }); it('Should publish correct state on toggle command to zigbee bulb', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); await flushPromises(); - // At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to MQTT. + // At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to mockMQTT. expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); // Turn bulb off so that the bulb gets a state. - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), @@ -1372,11 +1385,11 @@ describe('Publish', () => { return {currentLevel: 100}; } }); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), @@ -1386,49 +1399,48 @@ describe('Publish', () => { }); it('Should publish messages with options disableDefaultResponse', async () => { - const device = zigbeeHerdsman.devices.GLEDOPTO1112; - const endpoint = device.getEndpoint(11); - await MQTT.events.message('zigbee2mqtt/led_controller_1/set', stringify({state: 'OFF'})); + const device = devices.GLEDOPTO1112; + const endpoint = device.getEndpoint(11)!; + await mockMQTTEvents.message('zigbee2mqtt/led_controller_1/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {disableDefaultResponse: true}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish messages to zigbee devices', async () => { settings.set(['advanced', 'last_seen'], 'ISO_8601'); - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toStrictEqual('string'); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toStrictEqual('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish brightness_move up to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: -40})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: -40})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'move', {movemode: 1, rate: 40}, {}); }); it('Should publish brightness_move down to zigbee devices', async () => { - const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: 30})); + const endpoint = devices.bulb_color.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness_move: 30})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'move', {movemode: 0, rate: 30}, {}); }); it('HS2WD-E burglar warning', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1440,9 +1452,9 @@ describe('Publish', () => { }); it('HS2WD-E emergency warning', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 10, mode: 'emergency', strobe: false, level: 'very_high'}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1454,9 +1466,9 @@ describe('Publish', () => { }); it('HS2WD-E emergency without level', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: {duration: 10, mode: 'emergency', strobe: false}}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1468,9 +1480,9 @@ describe('Publish', () => { }); it('HS2WD-E wrong payload (should use defaults)', async () => { - const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const endpoint = devices.HS2WD.getEndpoint(1)!; const payload = {warning: 'wrong'}; - await MQTT.events.message('zigbee2mqtt/siren/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/siren/set', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1483,27 +1495,27 @@ describe('Publish', () => { it('Shouldnt do anything when device is not supported', async () => { const payload = {state: 'ON'}; - await MQTT.events.message('zigbee2mqtt/unsupported2/set', stringify(payload)); + await mockMQTTEvents.message('zigbee2mqtt/unsupported2/set', stringify(payload)); await flushPromises(); expectNothingPublished(); }); it('Should publish state to roller shutter', async () => { - const endpoint = zigbeeHerdsman.devices.roller_shutter.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/roller_shutter/set', stringify({state: 'OPEN'})); + const endpoint = devices.roller_shutter.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/roller_shutter/set', stringify({state: 'OPEN'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: '255', transtime: 0}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish to MKS-CM-W5', async () => { - const device = zigbeeHerdsman.devices['MKS-CM-W5']; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'})); + const device = devices['MKS-CM-W5']; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/MKS-CM-W5/l3/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command).toHaveBeenCalledWith( @@ -1512,23 +1524,23 @@ describe('Publish', () => { {dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish separate genOnOff to GL-S-007ZS when setting state and brightness as bulb doesnt turn on with moveToLevelWithOnOff', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/2757 - const device = zigbeeHerdsman.devices['GL-S-007ZS']; - const endpoint = device.getEndpoint(1); - await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20})); + const device = devices['GL-S-007ZS']; + const endpoint = device.getEndpoint(1)!; + await mockMQTTEvents.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20})); await flushPromises(); await jest.runOnlyPendingTimersAsync(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/GL-S-007ZS', stringify({state: 'ON', brightness: 20}), {qos: 0, retain: false}, @@ -1537,28 +1549,28 @@ describe('Publish', () => { }); it('Should log as error when setting property with no defined converter', async () => { - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; const payload = {brightness_move: 20}; - logger.error.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/get', stringify(payload)); + mockLogger.error.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/get', stringify(payload)); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(0); - expect(logger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)"); + expect(mockLogger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)"); }); it('Should restore brightness when its turned on with transition, Z2M is restarted and turned on again', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/7106 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; endpoint.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 20, transition: 0.0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 20, transition: 0.0})); await flushPromises(); - zigbeeHerdsmanConverters.toZigbee.__clearStore__(); + toZigbee.__clearStore__(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); @@ -1568,26 +1580,26 @@ describe('Publish', () => { it('Should restore brightness when its turned off without transition and is turned on with', async () => { // https://github.com/Koenkk/zigbee-herdsman-converters/issues/1097 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; endpoint.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 123})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', brightness: 123})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(2); expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 123, transtime: 0}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(3); expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 1.0})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(4); expect(endpoint.command.mock.calls[3]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 123, transtime: 10}, {}]); @@ -1595,16 +1607,16 @@ describe('Publish', () => { it('Shouldnt use moveToLevelWithOnOff on turn on when no transition has been used as some devices do not turn on in that case', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3332 - const device = zigbeeHerdsman.devices.bulb_color; - const endpoint = device.getEndpoint(1); + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 150})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 150})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(4); @@ -1613,26 +1625,26 @@ describe('Publish', () => { expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]); expect(endpoint.command.mock.calls[3]).toEqual(['genOnOff', 'on', {}, {}]); - expect(MQTT.publish).toHaveBeenCalledTimes(4); - expect(MQTT.publish.mock.calls[0]).toEqual([ + expect(mockMQTT.publish).toHaveBeenCalledTimes(4); + expect(mockMQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[1]).toEqual([ + expect(mockMQTT.publish.mock.calls[1]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 150}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[2]).toEqual([ + expect(mockMQTT.publish.mock.calls[2]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 150}), {qos: 0, retain: false}, expect.any(Function), ]); - expect(MQTT.publish.mock.calls[3]).toEqual([ + expect(mockMQTT.publish.mock.calls[3]).toEqual([ 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 150}), {qos: 0, retain: false}, @@ -1641,81 +1653,79 @@ describe('Publish', () => { }); it('Scenes', async () => { - const bulb_color_2 = zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1); - const bulb_2 = zigbeeHerdsman.devices.bulb_2.getEndpoint(1); - const group = zigbeeHerdsman.groups.group_tradfri_remote; - await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 50, color_temp: 290})); - await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 100})); + const group = groups.group_tradfri_remote; + await mockMQTTEvents.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 50, color_temp: 290})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 100})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_store: 1})); + await mockMQTTEvents.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_store: 1})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genScenes', 'store', {groupid: 15071, sceneid: 1}, {}); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 250, color_temp: 20})); - await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 110})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 250, color_temp: 20})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 110})); await flushPromises(); - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); group.command.mockClear(); - await MQTT.events.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_recall: 1})); + await mockMQTTEvents.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_recall: 1})); await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genScenes', 'recall', {groupid: 15071, sceneid: 1}, {}); - expect(MQTT.publish).toHaveBeenCalledTimes(8); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(8); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/group_tradfri_remote', stringify({brightness: 50, color_temp: 290, state: 'ON', color_mode: 'color_temp'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color_2', stringify({color_mode: 'color_temp', brightness: 50, color_temp: 290, state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 3, 'zigbee2mqtt/ha_discovery_group', stringify({brightness: 50, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 4, 'zigbee2mqtt/group_tradfri_remote', stringify({brightness: 100, color_temp: 290, state: 'ON', color_mode: 'color_temp'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 5, 'zigbee2mqtt/bulb_2', stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 6, 'zigbee2mqtt/group_with_tradfri', stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 7, 'zigbee2mqtt/switch_group', stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 8, 'zigbee2mqtt/ha_discovery_group', stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}), @@ -1725,28 +1735,28 @@ describe('Publish', () => { }); it('Should sync colors', async () => { - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 100})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 100})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 0.1, y: 0.5}})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color: {x: 0.1, y: 0.5}})); await flushPromises(); - await MQTT.events.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300})); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 1, 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 100}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 2, 'zigbee2mqtt/bulb_color', stringify({color: {x: 0.1, y: 0.5}, color_mode: 'xy', color_temp: 79}), {retain: false, qos: 0}, expect.any(Function), ); - expect(MQTT.publish).toHaveBeenNthCalledWith( + expect(mockMQTT.publish).toHaveBeenNthCalledWith( 3, 'zigbee2mqtt/bulb_color', stringify({color: {x: 0.4152, y: 0.3954}, color_mode: 'color_temp', color_temp: 300}), @@ -1756,8 +1766,8 @@ describe('Publish', () => { }); it('Log an error when entity is not found', async () => { - await MQTT.events.message('zigbee2mqtt/an_unknown_entity/set', stringify({})); + await mockMQTTEvents.message('zigbee2mqtt/an_unknown_entity/set', stringify({})); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith("Entity 'an_unknown_entity' is unknown"); + expect(mockLogger.error).toHaveBeenCalledWith("Entity 'an_unknown_entity' is unknown"); }); }); diff --git a/test/receive.test.js b/test/extensions/receive.test.ts old mode 100755 new mode 100644 similarity index 55% rename from test/receive.test.js rename to test/extensions/receive.test.ts index 2064aaa10a..bc013b7a08 --- a/test/receive.test.js +++ b/test/extensions/receive.test.ts @@ -1,49 +1,51 @@ -const data = require('./stub/data'); -const sleep = require('./stub/sleep'); -const logger = require('./stub/logger'); -const stringify = require('json-stable-stringify-without-jsonify'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); +import * as data from '../mocks/data'; +import {mockLogger} from '../mocks/logger'; +import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; +import * as mockSleep from '../mocks/sleep'; +import {flushPromises} from '../mocks/utils'; +import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman'; -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; +import stringify from 'json-stable-stringify-without-jsonify'; -describe('Receive', () => { - let controller; +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const mocksClear = [mockMQTT.publish, mockLogger.warning, mockLogger.debug]; + +describe('Extension: Receive', () => { + let controller: Controller; beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); + mockSleep.mock(); await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); + await jest.runOnlyPendingTimersAsync(); }); beforeEach(async () => { + // @ts-expect-error private controller.state.state = {}; data.writeDefaultConfiguration(); settings.reRead(); mocksClear.forEach((m) => m.mockClear()); - delete zigbeeHerdsman.devices.WXKG11LM.linkquality; + delete devices.WXKG11LM.linkquality; }); afterAll(async () => { jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); }); it('Should handle a zigbee message', async () => { - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; device.linkquality = 10; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/button', stringify({action: 'single', click: 'single', linkquality: 10}), {retain: false, qos: 0}, @@ -52,31 +54,31 @@ describe('Receive', () => { }); it('Should handle a zigbee message which uses ep (left)', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle a zigbee message which uses ep (right)', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle a zigbee message with default precision', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; const data = {measuredValue: -85}; const payload = { data, @@ -86,19 +88,19 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should allow to invert cover', async () => { - const device = zigbeeHerdsman.devices.J1; + const device = devices.J1_cover; // Non-inverted (open = 100, close = 0) - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, @@ -107,8 +109,8 @@ describe('Receive', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, @@ -116,9 +118,9 @@ describe('Receive', () => { ); // Inverted - MQTT.publish.mockClear(); + mockMQTT.publish.mockClear(); settings.set(['devices', device.ieeeAddr, 'invert_cover'], true); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, @@ -127,8 +129,8 @@ describe('Receive', () => { linkquality: 10, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, @@ -137,18 +139,23 @@ describe('Receive', () => { }); it('Should allow to disable the legacy integration', async () => { - const device = zigbeeHerdsman.devices.WXKG11LM; + const device = devices.WXKG11LM; settings.set(['devices', device.ieeeAddr, 'legacy'], false); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single'}), {retain: false, qos: 0}, expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/button', + stringify({action: 'single'}), + {retain: false, qos: 0}, + expect.any(Function), + ); }); it('Should debounce messages', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); const data1 = {measuredValue: 8}; const payload1 = { @@ -159,7 +166,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 1}; const payload2 = { data: data2, @@ -169,7 +176,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 2}; const payload3 = { data: data3, @@ -179,20 +186,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should debounce and retain messages when set via device_options', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'debounce'], 0.1); settings.set(['device_options', 'retain'], true); delete settings.get().devices['0x0017880104e45522']['retain']; @@ -205,7 +212,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 1}; const payload2 = { data: data2, @@ -215,7 +222,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 2}; const payload3 = { data: data3, @@ -225,20 +232,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true}); }); it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']); const tempMsg = { @@ -249,7 +256,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(tempMsg); + await mockZHEvents.message(tempMsg); const pressureMsg = { data: {measuredValue: 2}, cluster: 'msPressureMeasurement', @@ -258,7 +265,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(pressureMsg); + await mockZHEvents.message(pressureMsg); const tempMsg2 = { data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', @@ -267,7 +274,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(tempMsg2); + await mockZHEvents.message(tempMsg2); const humidityMsg = { data: {measuredValue: 3}, cluster: 'msRelativeHumidity', @@ -276,25 +283,25 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 13, }; - await zigbeeHerdsman.events.message(humidityMsg); + await mockZHEvents.message(humidityMsg); await flushPromises(); jest.advanceTimersByTime(50); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2}); jest.runOnlyPendingTimers(); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03}); }); it('Should NOT publish old messages from State cache during debouncing', async () => { // Summary: - // First send multiple measurements to device that is debouncing. Make sure only one message is sent out to MQTT. This also ensures first message is cached to "State". + // First send multiple measurements to device that is debouncing. Make sure only one message is sent out to mockMQTT. This also ensures first message is cached to "State". // Then send another measurement to that same device and trigger asynchronous event to push data from Cache. Newest value should be sent out. - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, @@ -302,7 +309,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 1}, cluster: 'msRelativeHumidity', device, @@ -310,7 +317,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, @@ -321,17 +328,17 @@ describe('Receive', () => { await flushPromises(); jest.advanceTimersByTime(50); // Test that measurements are combined(=debounced) - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); jest.runOnlyPendingTimers(); await flushPromises(); // Test that only one MQTT is sent out and test its values. - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); // Send another Zigbee message... - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {measuredValue: 9}, cluster: 'msTemperatureMeasurement', device, @@ -339,6 +346,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); + // @ts-expect-error private const realDevice = controller.zigbee.resolveEntity(device); // Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache. @@ -347,16 +355,16 @@ describe('Receive', () => { await flushPromises(); // Total of 3 messages should have triggered. - expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); // Test that message pushed by asynchronous message contains NEW measurement and not old. - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); // Test that messages after debouncing contains NEW measurement and not old. - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); + expect(JSON.parse(mockMQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2}); }); it('Should throttle multiple messages from spamming devices', async () => { - const device = zigbeeHerdsman.devices.SPAMMER; + const device = devices.SPAMMER; const throttle_for_testing = 1; settings.set(['device_options', 'throttle'], throttle_for_testing); settings.set(['device_options', 'retain'], true); @@ -370,7 +378,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload1); + await mockZHEvents.message(payload1); const data2 = {measuredValue: 2}; const payload2 = { data: data2, @@ -380,7 +388,7 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload2); + await mockZHEvents.message(payload2); const data3 = {measuredValue: 3}; const payload3 = { data: data3, @@ -390,25 +398,25 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload3); + await mockZHEvents.message(payload3); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.01}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.01}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true}); // Now we try after elapsed time to see if it publishes next message const timeshift = throttle_for_testing * 2000; jest.advanceTimersByTime(timeshift); - expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); await flushPromises(); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.03}); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.03}); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true}); const data4 = {measuredValue: 4}; const payload4 = { @@ -419,20 +427,20 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload4); + await mockZHEvents.message(payload4); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1'); - expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.04}); - expect(MQTT.publish.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1'); + expect(JSON.parse(mockMQTT.publish.mock.calls[2][1])).toStrictEqual({temperature: 0.04}); + expect(mockMQTT.publish.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true}); }); it('Shouldnt republish old state', async () => { // https://github.com/Koenkk/zigbee2mqtt/issues/3572 - const device = zigbeeHerdsman.devices.bulb; + const device = devices.bulb; settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); - await zigbeeHerdsman.events.message({ + await mockZHEvents.message({ data: {onOff: 0}, cluster: 'genOnOff', device, @@ -440,16 +448,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }); - await MQTT.events.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'})); await flushPromises(); jest.runOnlyPendingTimers(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON'}); }); it('Should handle a zigbee message with 1 precision', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1); const data = {measuredValue: -85}; const payload = { @@ -460,16 +468,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 0 precision', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); const data = {measuredValue: -85}; const payload = { @@ -480,16 +488,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 1 precision when set via device_options', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); const data = {measuredValue: -85}; const payload = { @@ -500,16 +508,16 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with 2 precision when overrides device_options', async () => { - const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const device = devices.WSDCGQ11LM; settings.set(['device_options', 'temperature_precision'], 1); settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); const data = {measuredValue: -85}; @@ -521,35 +529,35 @@ describe('Receive', () => { type: 'attributeReport', linkquality: 10, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false}); }); it('Should handle a zigbee message with voltage 2990', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; const data = {65281: {1: 2990}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish 1 message when converted twice', async () => { - const device = zigbeeHerdsman.devices.RTCGQ11LM; + const device = devices.RTCGQ11LM; const data = {65281: {1: 3045, 3: 19, 5: 35, 6: [0, 3], 11: 381, 100: 0}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({ + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({ battery: 100, illuminance: 381, illuminance_lux: 381, @@ -557,66 +565,71 @@ describe('Receive', () => { device_temperature: 19, power_outage_count: 34, }); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish 1 message when converted twice', async () => { - const device = zigbeeHerdsman.devices.RTCGQ11LM; + const device = devices.RTCGQ11LM; const data = {9999: {1: 3045}}; const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should publish last_seen epoch', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'epoch'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number'); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('number'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'ISO_8601'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should publish last_seen ISO_8601_local', async () => { - const device = zigbeeHerdsman.devices.WXKG02LM_rev1; + const device = devices.WXKG02LM_rev1; settings.set(['advanced', 'last_seen'], 'ISO_8601_local'); const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); - expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(mockMQTT.publish.mock.calls[0][1]).last_seen).toBe('string'); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should handle messages from Xiaomi router devices', async () => { - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const device = devices.ZNCZ02LM; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/power_plug', stringify({state: 'ON'}), {retain: false, qos: 0}, expect.any(Function)); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/power_plug', + stringify({state: 'ON'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0}, @@ -625,27 +638,27 @@ describe('Receive', () => { }); it('Should not handle messages from coordinator', async () => { - const device = zigbeeHerdsman.devices.coordinator; + const device = devices.coordinator; const data = {onOff: 1}; const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); }); it('Should not handle messages from still interviewing devices with unknown definition', async () => { - const device = zigbeeHerdsman.devices.interviewing; + const device = devices.interviewing; const data = {onOff: 1}; - logger.debug.mockClear(); + mockLogger.debug.mockClear(); const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`); + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`); }); it('Should handle a command', async () => { - const device = zigbeeHerdsman.devices.E1743; + const device = devices.E1743; const data = {}; const payload = { data, @@ -656,45 +669,41 @@ describe('Receive', () => { linkquality: 10, meta: {zclTransactionSequenceNumber: 1}, }; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should add elapsed', async () => { settings.set(['advanced', 'elapsed'], true); - const device = zigbeeHerdsman.devices.E1743; + const device = devices.E1743; const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'}; - const oldNow = Date.now; - Date.now = jest.fn(); - Date.now.mockReturnValue(new Date(150)); - await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 2}}); + jest.spyOn(Date, 'now').mockReturnValueOnce(150).mockReturnValueOnce(200); + await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 2}}); await flushPromises(); - Date.now.mockReturnValue(new Date(200)); - await zigbeeHerdsman.events.message({...payload, meta: {zclTransactionSequenceNumber: 3}}); + await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 3}}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); - expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); - expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); - expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'}); - expect(JSON.parse(MQTT.publish.mock.calls[1][1]).elapsed).toBe(50); - expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); - Date.now = oldNow; + expect(mockMQTT.publish).toHaveBeenCalledTimes(2); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'}); + expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'}); + expect(JSON.parse(mockMQTT.publish.mock.calls[1][1]).elapsed).toBe(50); + expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); }); it('Should log when message is from supported device but has no converters', async () => { - const device = zigbeeHerdsman.devices.ZNCZ02LM; + const device = devices.ZNCZ02LM; const data = {inactiveText: 'hello'}; const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; - await zigbeeHerdsman.events.message(payload); + await mockZHEvents.message(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(0); - expect(logger.debug).toHaveBeenCalledWith( + expect(mockMQTT.publish).toHaveBeenCalledTimes(0); + expect(mockLogger.debug).toHaveBeenCalledWith( "No converter available for 'ZNCZ02LM' with cluster 'genBinaryOutput' and type 'attributeReport' and data '{\"inactiveText\":\"hello\"}'", ); }); @@ -704,8 +713,8 @@ describe('Receive', () => { // divisor of OLD is not correct and therefore underreports by factor 10. const data = {instantaneousDemand: 496, currentSummDelivered: 6648}; - const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW; - await zigbeeHerdsman.events.message({ + const SP600_NEW = devices.SP600_NEW; + await mockZHEvents.message({ data, cluster: 'seMetering', device: SP600_NEW, @@ -715,13 +724,13 @@ describe('Receive', () => { meta: {zclTransactionSequenceNumber: 1}, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6}); - MQTT.publish.mockClear(); - const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD; - await zigbeeHerdsman.events.message({ + mockMQTT.publish.mockClear(); + const SP600_OLD = devices.SP600_OLD; + await mockZHEvents.message({ data, cluster: 'seMetering', device: SP600_OLD, @@ -731,16 +740,16 @@ describe('Receive', () => { meta: {zclTransactionSequenceNumber: 2}, }); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD'); - expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496}); + expect(mockMQTT.publish).toHaveBeenCalledTimes(1); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD'); + expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496}); }); it('Should emit DevicesChanged event when a converter announces changed exposes', async () => { - const device = zigbeeHerdsman.devices['BMCT-SLZ']; + const device = devices['BMCT-SLZ']; const data = {deviceMode: 0}; const payload = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; - await zigbeeHerdsman.events.message(payload); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); + await mockZHEvents.message(payload); + expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); }); }); diff --git a/test/frontend.test.js b/test/frontend.test.js deleted file mode 100644 index f75743fecc..0000000000 --- a/test/frontend.test.js +++ /dev/null @@ -1,426 +0,0 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const stringify = require('json-stable-stringify-without-jsonify'); -const flushPromises = require('./lib/flushPromises'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const path = require('path'); -const finalhandler = require('finalhandler'); -const ws = require('ws'); -jest.spyOn(process, 'exit').mockImplementation(() => {}); - -afterEach(() => { - jest.clearAllMocks(); -}); - -const mockHTTP = { - implementation: { - listen: jest.fn(), - on: (event, handler) => { - mockHTTP.events[event] = handler; - }, - close: jest.fn().mockImplementation((cb) => cb()), - }, - variables: {}, - events: {}, -}; - -const mockHTTPS = { - implementation: { - listen: jest.fn(), - on: (event, handler) => { - mockHTTPS.events[event] = handler; - }, - close: jest.fn().mockImplementation((cb) => cb()), - }, - variables: {}, - events: {}, -}; - -const mockWSocket = { - close: jest.fn(), -}; - -const mockWS = { - implementation: { - clients: [], - on: (event, handler) => { - mockWS.events[event] = handler; - }, - handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => { - cb(mockWSocket); - }), - emit: jest.fn(), - close: jest.fn(), - }, - variables: {}, - events: {}, -}; - -const mockNodeStatic = { - implementation: jest.fn(), - variables: {}, - events: {}, -}; - -const mockFinalHandler = { - implementation: jest.fn(), -}; - -jest.mock('http', () => ({ - createServer: jest.fn().mockImplementation((onRequest) => { - mockHTTP.variables.onRequest = onRequest; - return mockHTTP.implementation; - }), - Agent: jest.fn(), -})); - -jest.mock('https', () => ({ - createServer: jest.fn().mockImplementation((onRequest) => { - mockHTTPS.variables.onRequest = onRequest; - return mockHTTPS.implementation; - }), - Agent: jest.fn(), -})); - -jest.mock('express-static-gzip', () => - jest.fn().mockImplementation((path) => { - mockNodeStatic.variables.path = path; - return mockNodeStatic.implementation; - }), -); - -jest.mock('zigbee2mqtt-frontend', () => ({ - getPath: () => 'my/dummy/path', -})); - -jest.mock('ws', () => ({ - OPEN: 'open', - Server: jest.fn().mockImplementation(() => { - return mockWS.implementation; - }), -})); - -jest.mock('finalhandler', () => - jest.fn().mockImplementation(() => { - return mockFinalHandler.implementation; - }), -); - -describe('Frontend', () => { - let controller; - - beforeAll(async () => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - mockWS.implementation.clients = []; - data.writeDefaultConfiguration(); - data.writeDefaultState(); - settings.reRead(); - settings.set(['frontend'], {port: 8081, host: '127.0.0.1'}); - settings.set(['homeassistant'], true); - zigbeeHerdsman.devices.bulb.linkquality = 10; - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - afterEach(async () => { - delete zigbeeHerdsman.devices.bulb.linkquality; - }); - - it('Start/stop', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - it('Start/stop without host', async () => { - settings.set(['frontend'], {port: 8081}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - it('Start/stop unix socket', async () => { - settings.set(['frontend'], {host: '/tmp/zigbee2mqtt.sock'}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockNodeStatic.variables.path).toBe('my/dummy/path'); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock'); - const mockWSClient = { - implementation: { - terminate: jest.fn(), - send: jest.fn(), - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await controller.stop(); - expect(mockWSClient.implementation.terminate).toHaveBeenCalledTimes(1); - expect(mockHTTP.implementation.close).toHaveBeenCalledTimes(1); - expect(mockWS.implementation.close).toHaveBeenCalledTimes(1); - }); - - it('Start/stop HTTPS valid', async () => { - settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt')); - settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key')); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockHTTP.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); - expect(mockHTTPS.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - await controller.stop(); - }); - - it('Start/stop HTTPS invalid : missing config', async () => { - settings.set(['frontend', 'ssl_cert'], path.join(__dirname, 'assets', 'certs', 'dummy.crt')); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); - await controller.stop(); - }); - - it('Start/stop HTTPS invalid : missing file', async () => { - settings.set(['frontend', 'ssl_cert'], 'filesNotExists.crt'); - settings.set(['frontend', 'ssl_key'], path.join(__dirname, 'assets', 'certs', 'dummy.key')); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - expect(mockHTTPS.implementation.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1'); - await controller.stop(); - }); - - it('Websocket interaction', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - // Connect - const mockWSClient = { - implementation: { - on: (event, handler) => { - mockWSClient.events[event] = handler; - }, - send: jest.fn(), - readyState: 'open', - }, - events: {}, - }; - mockWS.implementation.clients.push(mockWSClient.implementation); - await mockWS.events.connection(mockWSClient.implementation); - - const allTopics = mockWSClient.implementation.send.mock.calls.map((m) => JSON.parse(m).topic); - expect(allTopics).toContain('bridge/devices'); - expect(allTopics).toContain('bridge/info'); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}})); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}})); - - // Message - MQTT.publish.mockClear(); - mockWSClient.implementation.send.mockClear(); - mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb_color', - stringify({ - state: 'ON', - power_on_behavior: null, - linkquality: null, - update_available: null, - update: {state: null, installed_version: -1, latest_version: -1}, - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - mockWSClient.events.message(undefined, false); - mockWSClient.events.message('', false); - mockWSClient.events.message(null, false); - await flushPromises(); - - // Error - mockWSClient.events.error(new Error('This is an error')); - expect(logger.error).toHaveBeenCalledWith('WebSocket error: This is an error'); - - // Received message on socket - expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith( - stringify({ - topic: 'bulb_color', - payload: { - state: 'ON', - power_on_behavior: null, - linkquality: null, - update_available: null, - update: {state: null, installed_version: -1, latest_version: -1}, - }, - }), - ); - - // Shouldnt set when not ready - mockWSClient.implementation.send.mockClear(); - mockWSClient.implementation.readyState = 'close'; - mockWSClient.events.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false); - expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(0); - - // Send last seen on connect - mockWSClient.implementation.send.mockClear(); - mockWSClient.implementation.readyState = 'open'; - settings.set(['advanced'], {last_seen: 'ISO_8601'}); - mockWS.implementation.clients.push(mockWSClient.implementation); - await mockWS.events.connection(mockWSClient.implementation); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith( - stringify({topic: 'remote', payload: {brightness: 255, last_seen: '1970-01-01T00:00:01.000Z'}}), - ); - }); - - it('onReques/onUpgrade', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - const mockSocket = {destroy: jest.fn()}; - mockHTTP.events.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); - expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: 'http://localhost:8080/api'}, mockSocket, 3, expect.any(Function)); - mockWS.implementation.handleUpgrade.mock.calls[0][3](99); - expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {url: 'http://localhost:8080/api'}); - - mockHTTP.variables.onRequest({url: '/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/file.txt', url: '/file.txt', path: '/file.txt'}, - 2, - expect.any(Function), - ); - }); - - it('Static server', async () => { - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(mockHTTP.implementation.listen).toHaveBeenCalledWith(8081, '127.0.0.1'); - }); - - it('Authentification', async () => { - const authToken = 'sample-secure-token'; - settings.set(['frontend'], {auth_token: authToken}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - const mockSocket = {destroy: jest.fn()}; - mockHTTP.events.upgrade({url: '/api'}, mockSocket, mockWSocket); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); - expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url: '/api'}, mockSocket, mockWSocket, expect.any(Function)); - expect(mockWSocket.close).toHaveBeenCalledWith(4401, 'Unauthorized'); - - mockWSocket.close.mockClear(); - mockWS.implementation.emit.mockClear(); - - const url = `/api?token=${authToken}`; - mockWS.implementation.handleUpgrade.mockClear(); - mockHTTP.events.upgrade({url: url}, mockSocket, 3); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledTimes(1); - expect(mockSocket.destroy).toHaveBeenCalledTimes(0); - expect(mockWS.implementation.handleUpgrade).toHaveBeenCalledWith({url}, mockSocket, 3, expect.any(Function)); - expect(mockWSocket.close).toHaveBeenCalledTimes(0); - mockWS.implementation.handleUpgrade.mock.calls[0][3](mockWSocket); - expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', mockWSocket, {url}); - }); - - it.each(['/z2m/', '/z2m'])('Works with non-default base url %s', async (baseUrl) => { - settings.set(['frontend'], {base_url: baseUrl}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m/api'}); - - mockHTTP.variables.onRequest({url: '/z2m'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m', url: '/', path: '/'}, 2, expect.any(Function)); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - mockHTTP.variables.onRequest({url: '/z2m/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/z2m/file.txt', url: '/file.txt', path: '/file.txt'}, - 2, - expect.any(Function), - ); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - mockHTTP.variables.onRequest({url: '/z/file.txt'}, 2); - expect(mockNodeStatic.implementation).not.toHaveBeenCalled(); - expect(mockFinalHandler.implementation).toHaveBeenCalled(); - }); - - it('Works with non-default complex base url', async () => { - const baseUrl = '/z2m-more++/c0mplex.url/'; - settings.set(['frontend'], {base_url: baseUrl}); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - - expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m-more++/c0mplex.url/api'}); - - mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/z2m-more++/c0mplex.url', url: '/', path: '/'}, - 2, - expect.any(Function), - ); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url/file.txt'}, 2); - expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/z2m-more++/c0mplex.url/file.txt', url: '/file.txt', path: '/file.txt'}, - 2, - expect.any(Function), - ); - expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); - - mockNodeStatic.implementation.mockReset(); - mockHTTP.variables.onRequest({url: '/z/file.txt'}, 2); - expect(mockNodeStatic.implementation).not.toHaveBeenCalled(); - expect(mockFinalHandler.implementation).toHaveBeenCalled(); - }); -}); diff --git a/test/lib/flushPromises.js b/test/lib/flushPromises.js deleted file mode 100644 index 4b040f5df8..0000000000 --- a/test/lib/flushPromises.js +++ /dev/null @@ -1,2 +0,0 @@ -const globalSetImmediate = setImmediate; -module.exports = () => new Promise(globalSetImmediate); diff --git a/test/logger.test.js b/test/logger.test.ts similarity index 79% rename from test/logger.test.js rename to test/logger.test.ts index 81e52d1b21..98973d1ab1 100644 --- a/test/logger.test.js +++ b/test/logger.test.ts @@ -1,17 +1,27 @@ -const tmp = require('tmp'); -const dir = tmp.dirSync(); -let settings; -const fs = require('fs'); -const path = require('path'); -const data = require('./stub/data'); -const {rimrafSync} = require('rimraf'); -const Transport = require('winston-transport'); +import * as data from './mocks/data'; + +import fs from 'fs'; +import {platform} from 'os'; +import path from 'path'; + +import {rimrafSync} from 'rimraf'; +import tmp from 'tmp'; +import Transport from 'winston-transport'; + +import logger from '../lib/util/logger'; +import * as settings from '../lib/util/settings'; describe('Logger', () => { - let logger; - let consoleWriteSpy; + let consoleWriteSpy: jest.SpyInstance; + const dir = tmp.dirSync(); + + const getCachedNamespacedLevels = (): Record => { + // @ts-expect-error private + return logger.cachedNamespacedLevels; + }; beforeAll(() => { + // @ts-expect-error private consoleWriteSpy = jest.spyOn(console._stdout, 'write').mockImplementation(() => {}); }); @@ -21,11 +31,8 @@ describe('Logger', () => { beforeEach(async () => { data.writeDefaultConfiguration(); - jest.resetModules(); - settings = require('../lib/util/settings'); - settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); settings.reRead(); - logger = require('../lib/util/logger').default; + settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); logger.init(); consoleWriteSpy.mockClear(); }); @@ -81,7 +88,7 @@ describe('Logger', () => { it('Add/remove transport', () => { class DummyTransport extends Transport { - log(info, callback) {} + log(): void {} } expect(logger.winston.transports.length).toBe(2); @@ -93,6 +100,7 @@ describe('Logger', () => { }); it('Logger should be console and file by default', () => { + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.length).toBe(2); expect(pipes[0].constructor.name).toBe('Console'); @@ -104,6 +112,7 @@ describe('Logger', () => { it('Logger can be file only', () => { settings.set(['advanced', 'log_output'], ['file']); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.length).toBe(2); expect(pipes[0].constructor.name).toBe('Console'); @@ -115,6 +124,7 @@ describe('Logger', () => { it('Logger can be console only', () => { settings.set(['advanced', 'log_output'], ['console']); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.constructor.name).toBe('Console'); expect(pipes.silent).toBe(false); @@ -123,6 +133,7 @@ describe('Logger', () => { it('Logger can be nothing', () => { settings.set(['advanced', 'log_output'], []); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes.constructor.name).toBe('Console'); expect(pipes.silent).toBe(true); @@ -131,6 +142,7 @@ describe('Logger', () => { it('Should allow to disable log rotation', () => { settings.set(['advanced', 'log_rotation'], false); logger.init(); + // @ts-expect-error private const pipes = logger.winston._readableState.pipes; expect(pipes[1].constructor.name).toBe('File'); expect(pipes[1].maxFiles).toBeNull(); @@ -139,11 +151,17 @@ describe('Logger', () => { }); it('Should allow to symlink logs to current directory', () => { - settings.set(['advanced', 'log_symlink_current'], true); - logger.init(); - expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy(); + try { + settings.set(['advanced', 'log_symlink_current'], true); + logger.init(); + expect(fs.readdirSync(dir.name).includes('current')).toBeTruthy(); + } catch (error) { + if (platform() !== 'win32' || !(error as Error).message.startsWith('EPERM')) { + throw error; + } - jest.resetModules(); + // ignore 'operation not permitted' failure on Windows + } }); it.each([ @@ -158,20 +176,25 @@ describe('Logger', () => { consoleWriteSpy.mockClear(); let i = 1; + // @ts-expect-error dynamic logger[level]('msg'); expect(logSpy).toHaveBeenLastCalledWith(level, 'z2m: msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[level]('msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(level, 'abcd: msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[level](() => 'func msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(level, 'abcd: func msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); for (const higherLevel of otherLevels.higher) { + // @ts-expect-error dynamic logger[higherLevel]('higher msg'); expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'z2m: higher msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); + // @ts-expect-error dynamic logger[higherLevel]('higher msg', 'abcd'); expect(logSpy).toHaveBeenLastCalledWith(higherLevel, 'abcd: higher msg'); expect(consoleWriteSpy).toHaveBeenCalledTimes(i++); @@ -181,23 +204,17 @@ describe('Logger', () => { consoleWriteSpy.mockClear(); for (const lowerLevel of otherLevels.lower) { + // @ts-expect-error dynamic logger[lowerLevel]('lower msg'); expect(logSpy).not.toHaveBeenCalled(); expect(consoleWriteSpy).not.toHaveBeenCalled(); + // @ts-expect-error dynamic logger[lowerLevel]('lower msg', 'abcd'); expect(logSpy).not.toHaveBeenCalled(); expect(consoleWriteSpy).not.toHaveBeenCalled(); } }); - it('Logs Error object', () => { - const logSpy = jest.spyOn(logger.winston, 'log'); - - logger.error(new Error('msg')); // test for stack=true - expect(logSpy).toHaveBeenLastCalledWith('error', `z2m: ${new Error('msg')}`); - expect(consoleWriteSpy).toHaveBeenCalledTimes(1); - }); - it.each([ [ '^zhc:legacy:fz:(tuya|moes)', @@ -243,6 +260,7 @@ describe('Logger', () => { logger.setLevel('debug'); const logSpy = jest.spyOn(logger.winston, 'log'); logger.setDebugNamespaceIgnore(ignore); + // @ts-expect-error private expect(logger.debugNamespaceIgnoreRegex).toStrictEqual(expected); expect(logger.getDebugNamespaceIgnore()).toStrictEqual(ignore); @@ -308,52 +326,53 @@ describe('Logger', () => { it('Logs with namespaced levels hierarchy', () => { const nsLevels = {'zh:zstack': 'debug', 'zh:zstack:unpi:writer': 'error'}; - let cachedNSLevels = Object.assign({}, nsLevels); + let cachedNSLevels; + cachedNSLevels = Object.assign({}, nsLevels); logger.setNamespacedLevels(nsLevels); logger.setLevel('warning'); consoleWriteSpy.mockClear(); logger.debug(`--- parseNext [] debug picked from hierarchy`, 'zh:zstack:unpi:parser'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:parser': 'debug'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:parser': 'debug'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.warning(`--> frame [36,15] warning explicitely supressed`, 'zh:zstack:unpi:writer'); - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels); + expect(getCachedNamespacedLevels()).toStrictEqual(cachedNSLevels); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.warning(`Another supressed warning message in a sub namespace`, 'zh:zstack:unpi:writer:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(1); logger.error(`but error should go through`, 'zh:zstack:unpi:writer:another:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(2); logger.warning(`new unconfigured namespace warning`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(3); logger.info(`cached unconfigured namespace info should be supressed`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(cachedNSLevels); + expect(getCachedNamespacedLevels()).toStrictEqual(cachedNSLevels); expect(consoleWriteSpy).toHaveBeenCalledTimes(3); logger.setLevel('info'); - expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = Object.assign({}, nsLevels))); + expect(getCachedNamespacedLevels()).toStrictEqual((cachedNSLevels = Object.assign({}, nsLevels))); logger.info(`unconfigured namespace info should now pass after default level change and cache reset`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(4); logger.error(`configured namespace hierachy should still work after the cache reset`, 'zh:zstack:unpi:writer:another:sub:ns'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer:another:sub:ns': 'error'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(5); logger.setNamespacedLevels({'zh:zstack': 'warning'}); - expect(logger.cachedNamespacedLevels).toStrictEqual((cachedNSLevels = {'zh:zstack': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual((cachedNSLevels = {'zh:zstack': 'warning'})); logger.error(`error logged`, 'zh:zstack:unpi:writer'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi:writer': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(6); logger.debug(`debug suppressed`, 'zh:zstack:unpi'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack:unpi': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(6); logger.warning(`warning logged`, 'zh:zstack'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack': 'warning'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'zh:zstack': 'warning'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(7); logger.info(`unconfigured namespace`, 'z2m:mqtt'); - expect(logger.cachedNamespacedLevels).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); + expect(getCachedNamespacedLevels()).toStrictEqual(Object.assign(cachedNSLevels, {'z2m:mqtt': 'info'})); expect(consoleWriteSpy).toHaveBeenCalledTimes(8); }); @@ -363,13 +382,13 @@ describe('Logger', () => { const logSpy = jest.spyOn(logger.winston, 'log'); consoleWriteSpy.mockClear(); - let net_map = '%d'; - logger.debug(net_map, 'z2m:mqtt'); - expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:mqtt: ${net_map}`); - expect(consoleWriteSpy.mock.calls[0][0]).toMatch(new RegExp(`^.*\tz2m:mqtt: ${net_map}`)); - net_map = 'anything %s goes here'; - logger.debug(net_map, 'z2m:test'); - expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:test: ${net_map}`); - expect(consoleWriteSpy.mock.calls[1][0]).toMatch(new RegExp(`^.*\tz2m:test: ${net_map}`)); + let splatChars = '%d'; + logger.debug(splatChars, 'z2m:mqtt'); + expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:mqtt: ${splatChars}`); + expect(consoleWriteSpy.mock.calls[0][0]).toMatch(new RegExp(`^.*\tz2m:mqtt: ${splatChars}`)); + splatChars = 'anything %s goes here'; + logger.debug(splatChars, 'z2m:test'); + expect(logSpy).toHaveBeenLastCalledWith('debug', `z2m:test: ${splatChars}`); + expect(consoleWriteSpy.mock.calls[1][0]).toMatch(new RegExp(`^.*\tz2m:test: ${splatChars}`)); }); }); diff --git a/test/stub/data.js b/test/mocks/data.ts similarity index 91% rename from test/stub/data.js rename to test/mocks/data.ts index 8e00f0d2eb..2fc10cc4db 100644 --- a/test/stub/data.js +++ b/test/mocks/data.ts @@ -1,13 +1,15 @@ -const tmp = require('tmp'); -const yaml = require('../../lib/util/yaml').default; -const path = require('path'); -const fs = require('fs'); -const stringify = require('json-stable-stringify-without-jsonify'); +import fs from 'fs'; +import path from 'path'; -const mockDir = tmp.dirSync().name; +import stringify from 'json-stable-stringify-without-jsonify'; +import tmp from 'tmp'; + +import yaml from '../../lib/util/yaml'; + +export const mockDir: string = tmp.dirSync().name; const stateFile = path.join(mockDir, 'state.json'); -function writeDefaultConfiguration() { +export function writeDefaultConfiguration(): void { const config = { homeassistant: false, mqtt: { @@ -244,17 +246,17 @@ function writeDefaultConfiguration() { yaml.writeIfChanged(path.join(mockDir, 'configuration.yaml'), config); } -function writeEmptyState() { +export function writeEmptyState(): void { fs.writeFileSync(stateFile, stringify({})); } -function removeState() { +export function removeState(): void { if (stateExists()) { fs.unlinkSync(stateFile); } } -function stateExists() { +export function stateExists(): boolean { return fs.existsSync(stateFile); } @@ -273,29 +275,22 @@ const defaultState = { }, }; -function getDefaultState() { +export function getDefaultState(): typeof defaultState { return defaultState; } -function writeDefaultState() { +export function writeDefaultState(): void { fs.writeFileSync(path.join(mockDir, 'state.json'), stringify(defaultState)); } +export function read(): ReturnType { + return yaml.read(path.join(mockDir, 'configuration.yaml')); +} + jest.mock('../../lib/util/data', () => ({ - joinPath: (file) => require('path').join(mockDir, file), - getPath: () => mockDir, + joinPath: (file: string): string => jest.requireActual('path').join(mockDir, file), + getPath: (): string => mockDir, })); writeDefaultConfiguration(); writeDefaultState(); - -module.exports = { - mockDir, - read: () => yaml.read(path.join(mockDir, 'configuration.yaml')), - writeDefaultConfiguration, - writeDefaultState, - removeState, - writeEmptyState, - stateExists, - getDefaultState, -}; diff --git a/test/mocks/debounce.ts b/test/mocks/debounce.ts new file mode 100644 index 0000000000..25c19be50b --- /dev/null +++ b/test/mocks/debounce.ts @@ -0,0 +1,3 @@ +export const mockDebounce = jest.fn((fn) => fn); + +jest.mock('debounce', () => mockDebounce); diff --git a/test/mocks/jszip.ts b/test/mocks/jszip.ts new file mode 100644 index 0000000000..835418ec1c --- /dev/null +++ b/test/mocks/jszip.ts @@ -0,0 +1,11 @@ +export const mockJSZipFile = jest.fn(); +export const mockJSZipGenerateAsync = jest.fn().mockReturnValue('THISISBASE64'); + +jest.mock('jszip', () => + jest.fn().mockImplementation(() => { + return { + file: mockJSZipFile, + generateAsync: mockJSZipGenerateAsync, + }; + }), +); diff --git a/test/mocks/logger.ts b/test/mocks/logger.ts new file mode 100644 index 0000000000..4f6f21461a --- /dev/null +++ b/test/mocks/logger.ts @@ -0,0 +1,53 @@ +import type {LogLevel} from 'lib/util/settings'; +import type Transport from 'winston-transport'; + +let level = 'info'; +let debugNamespaceIgnore: string = ''; +let namespacedLevels: Record = {}; +let transports: Transport[] = []; +let transportsEnabled: boolean = false; +const getMessage = (messageOrLambda: string | (() => string)): string => (messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda); + +export const mockLogger = { + log: jest.fn().mockImplementation((level, message, namespace = 'z2m') => { + if (transportsEnabled) { + for (const transport of transports) { + transport.log!({level, message, namespace}, () => {}); + } + } + }), + init: jest.fn(), + info: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('info', getMessage(messageOrLambda), namespace)), + warning: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('warning', getMessage(messageOrLambda), namespace)), + error: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('error', getMessage(messageOrLambda), namespace)), + debug: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('debug', getMessage(messageOrLambda), namespace)), + cleanup: jest.fn(), + logOutput: jest.fn(), + add: (transport: Transport): void => { + transports.push(transport); + }, + addTransport: (transport: Transport): void => { + transports.push(transport); + }, + removeTransport: (transport: Transport): void => { + transports = transports.filter((t) => t !== transport); + }, + setLevel: (newLevel: LogLevel): void => { + level = newLevel; + }, + getLevel: (): LogLevel => level, + setNamespacedLevels: (nsLevels: Record): void => { + namespacedLevels = nsLevels; + }, + getNamespacedLevels: (): Record => namespacedLevels, + setDebugNamespaceIgnore: (newIgnore: string): void => { + debugNamespaceIgnore = newIgnore; + }, + getDebugNamespaceIgnore: (): string => debugNamespaceIgnore, + setTransportsEnabled: (value: boolean): void => { + transportsEnabled = value; + }, + end: jest.fn(), +}; + +jest.mock('../../lib/util/logger', () => mockLogger); diff --git a/test/stub/mqtt.js b/test/mocks/mqtt.ts similarity index 50% rename from test/stub/mqtt.js rename to test/mocks/mqtt.ts index c3287b89a3..617f942ee8 100644 --- a/test/stub/mqtt.js +++ b/test/mocks/mqtt.ts @@ -1,37 +1,24 @@ -const events = {}; +import {EventHandler} from './utils'; -const mock = { +export const events: Record = {}; + +export const mockMQTT = { publish: jest.fn().mockImplementation((topic, payload, options, cb) => cb()), end: jest.fn(), subscribe: jest.fn(), unsubscribe: jest.fn(), reconnecting: false, - on: jest.fn(), - stream: {setMaxListeners: jest.fn()}, -}; - -const mockConnect = jest.fn().mockReturnValue(mock); - -jest.mock('mqtt', () => { - return {connect: mockConnect}; -}); - -const restoreOnMock = () => { - mock.on.mockImplementation((type, handler) => { + on: jest.fn((type, handler) => { if (type === 'connect') { handler(); } events[type] = handler; - }); + }), + stream: {setMaxListeners: jest.fn()}, }; +export const mockMQTTConnect = jest.fn().mockReturnValue(mockMQTT); -restoreOnMock(); - -module.exports = { - events, - ...mock, - connect: mockConnect, - mock, - restoreOnMock, -}; +jest.mock('mqtt', () => { + return {connect: mockMQTTConnect}; +}); diff --git a/test/mocks/sleep.ts b/test/mocks/sleep.ts new file mode 100644 index 0000000000..3484c2808f --- /dev/null +++ b/test/mocks/sleep.ts @@ -0,0 +1,11 @@ +import utils from '../../lib/util/utils'; + +const spy = jest.spyOn(utils, 'sleep'); + +export function mock(): void { + spy.mockImplementation(); +} + +export function restore(): void { + spy.mockRestore(); +} diff --git a/test/mocks/types.d.ts b/test/mocks/types.d.ts new file mode 100644 index 0000000000..50a8b627cd --- /dev/null +++ b/test/mocks/types.d.ts @@ -0,0 +1,15 @@ +declare module 'json-stable-stringify-without-jsonify' { + export default function (obj: unknown): string; +} + +declare module 'tmp' { + export function dirSync(): { + name: string; + removeCallback: (err: Error | undefined, name: string, fd: number, cleanupFn: () => void) => void; + }; + export function fileSync(): { + name: string; + fd: number; + removeCallback: (err: Error | undefined, name: string, fd: number, cleanupFn: () => void) => void; + }; +} diff --git a/test/mocks/utils.ts b/test/mocks/utils.ts new file mode 100644 index 0000000000..95bee98d65 --- /dev/null +++ b/test/mocks/utils.ts @@ -0,0 +1,15 @@ +export type EventHandler = (...args: unknown[]) => unknown; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type JestMockAny = jest.Mock; + +export function flushPromises(): Promise { + return new Promise(jest.requireActual('timers').setImmediate); +} + +// https://github.com/jestjs/jest/issues/6028#issuecomment-567669082 +export function defuseRejection(promise: Promise): Promise { + promise.catch(() => {}); + + return promise; +} diff --git a/test/stub/zigbeeHerdsman.js b/test/mocks/zigbeeHerdsman.ts similarity index 62% rename from test/stub/zigbeeHerdsman.js rename to test/mocks/zigbeeHerdsman.ts index 75a12437ca..377dcf920f 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/mocks/zigbeeHerdsman.ts @@ -1,40 +1,43 @@ -const events = {}; -const assert = require('assert'); +import assert from 'assert'; -function getKeyByValue(object, value, fallback) { - const key = Object.keys(object).find((k) => object[k] === value); - return key != null ? key : fallback; -} +import {Zcl} from 'zigbee-herdsman'; +import {CoordinatorVersion, DeviceType, NetworkParameters, StartResult} from 'zigbee-herdsman/dist/adapter/tstype'; -class Group { - constructor(groupID, members) { - this.groupID = groupID; - this.command = jest.fn(); - this.meta = {}; - this.members = members; - this.removeFromDatabase = jest.fn(); - this.removeFromNetwork = jest.fn(); - this.hasMember = (endpoint) => this.members.includes(endpoint); - } -} +import {EventHandler, JestMockAny} from './utils'; + +type ZHConfiguredReporting = { + cluster: {name: string}; + attribute: {name: string | undefined; ID?: number}; + minimumReportInterval: number; + maximumReportInterval: number; + reportableChange: number; +}; +type ZHEndpointCluster = { + ID?: number; + name: string; +}; +type ZHBind = { + target: Endpoint | Group; + cluster: ZHEndpointCluster; +}; -const clusters = { - genBasic: 0, - genOta: 25, - genScenes: 5, - genOnOff: 6, - genLevelCtrl: 8, - lightingColorCtrl: 768, - closuresWindowCovering: 258, - hvacThermostat: 513, - msIlluminanceMeasurement: 1024, - msTemperatureMeasurement: 1026, - msRelativeHumidity: 1029, - msSoilMoisture: 1032, - msCO2: 1037, +const CLUSTERS = { + genBasic: Zcl.Clusters.genBasic.ID, + genOta: Zcl.Clusters.genOta.ID, + genScenes: Zcl.Clusters.genScenes.ID, + genOnOff: Zcl.Clusters.genOnOff.ID, + genLevelCtrl: Zcl.Clusters.genLevelCtrl.ID, + lightingColorCtrl: Zcl.Clusters.lightingColorCtrl.ID, + closuresWindowCovering: Zcl.Clusters.closuresWindowCovering.ID, + hvacThermostat: Zcl.Clusters.hvacThermostat.ID, + msIlluminanceMeasurement: Zcl.Clusters.msIlluminanceMeasurement.ID, + msTemperatureMeasurement: Zcl.Clusters.msTemperatureMeasurement.ID, + msRelativeHumidity: Zcl.Clusters.msRelativeHumidity.ID, + msSoilMoisture: Zcl.Clusters.msSoilMoisture.ID, + msCO2: Zcl.Clusters.msCO2.ID, }; -const custom_clusters = { +export const CUSTOM_CLUSTERS = { custom_1: { ID: 64672, manufacturerCode: 4617, @@ -48,7 +51,7 @@ const custom_clusters = { }, }; -const customClusterBTHRA = { +const CUSTOM_CLUSTER_BTHRA = { custom_1: { ID: 513, attributes: { @@ -75,18 +78,50 @@ const customClusterBTHRA = { }, }; -class Endpoint { +function getClusterKey(value: unknown): string | undefined { + for (const key in CLUSTERS) { + if (CLUSTERS[key as keyof typeof CLUSTERS] === value) { + return key; + } + } + + return undefined; +} + +export class Endpoint { + deviceIeeeAddress: string; + clusterValues: Record>; + ID: number; + inputClusters: number[]; + outputClusters: number[]; + command: JestMockAny; + commandResponse: JestMockAny; + read: JestMockAny; + write: JestMockAny; + bind: JestMockAny; + unbind: JestMockAny; + save: JestMockAny; + configureReporting: JestMockAny; + meta: Record; + binds: ZHBind[]; + profileID: number | undefined; + deviceID: number | undefined; + configuredReportings: ZHConfiguredReporting[]; + addToGroup: JestMockAny; + removeFromGroup: JestMockAny; + getClusterAttributeValue: JestMockAny; + constructor( - ID, - inputClusters, - outputClusters, - deviceIeeeAddress, - binds = [], - clusterValues = {}, - configuredReportings = [], - profileID = null, - deviceID = null, - meta = {}, + ID: number, + inputClusters: number[], + outputClusters: number[], + deviceIeeeAddress: string, + binds: ZHBind[] = [], + clusterValues: Record> = {}, + configuredReportings: ZHConfiguredReporting[] = [], + profileID: number | undefined = undefined, + deviceID: number | undefined = undefined, + meta: Record = {}, ) { this.deviceIeeeAddress = deviceIeeeAddress; this.clusterValues = clusterValues; @@ -106,73 +141,111 @@ class Endpoint { this.profileID = profileID; this.deviceID = deviceID; this.configuredReportings = configuredReportings; - this.getInputClusters = () => - inputClusters - .map((c) => { - return {ID: c, name: getKeyByValue(clusters, c)}; - }) - .filter((c) => c.name); - - this.getOutputClusters = () => - outputClusters - .map((c) => { - return {ID: c, name: getKeyByValue(clusters, c)}; - }) - .filter((c) => c.name); - - this.supportsInputCluster = (cluster) => { - assert(clusters[cluster] !== undefined, `Undefined '${cluster}'`); - return this.inputClusters.includes(clusters[cluster]); - }; - - this.supportsOutputCluster = (cluster) => { - assert(clusters[cluster], `Undefined '${cluster}'`); - return this.outputClusters.includes(clusters[cluster]); - }; - - this.addToGroup = jest.fn(); - this.addToGroup.mockImplementation((group) => { - if (!group.members.includes(this)) group.members.push(this); - }); - this.getDevice = () => { - return Object.values(devices).find((d) => d.ieeeAddr === deviceIeeeAddress); - }; - - this.removeFromGroup = jest.fn(); - this.removeFromGroup.mockImplementation((group) => { + this.addToGroup = jest.fn((group: Group) => { + if (!group.members.includes(this)) { + group.members.push(this); + } + }); + this.removeFromGroup = jest.fn((group: Group) => { const index = group.members.indexOf(this); if (index != -1) { group.members.splice(index, 1); } }); - this.removeFromAllGroups = () => { - Object.values(groups).forEach((g) => this.removeFromGroup(g)); - }; + this.getClusterAttributeValue = jest.fn((cluster: string, value: string) => + !(cluster in this.clusterValues) ? undefined : this.clusterValues[cluster][value], + ); + } - this.getClusterAttributeValue = jest.fn(); - this.getClusterAttributeValue.mockImplementation((cluster, value) => { - if (!(cluster in this.clusterValues)) return undefined; - return this.clusterValues[cluster][value]; - }); + getInputClusters(): ZHEndpointCluster[] { + const clusters: ZHEndpointCluster[] = []; + + for (const clusterId of this.inputClusters) { + const name = getClusterKey(clusterId); + + if (name) { + clusters.push({ID: clusterId, name}); + } + } + + return clusters; + } + + getOutputClusters(): ZHEndpointCluster[] { + const clusters: ZHEndpointCluster[] = []; + + for (const clusterId of this.outputClusters) { + const name = getClusterKey(clusterId); + + if (name) { + clusters.push({ID: clusterId, name}); + } + } + + return clusters; + } + + supportsInputCluster(cluster: keyof typeof CLUSTERS): boolean { + assert(CLUSTERS[cluster] !== undefined, `Undefined '${cluster}'`); + return this.inputClusters.includes(CLUSTERS[cluster]); + } + + supportsOutputCluster(cluster: keyof typeof CLUSTERS): boolean { + assert(CLUSTERS[cluster], `Undefined '${cluster}'`); + return this.outputClusters.includes(CLUSTERS[cluster]); + } + + getDevice(): Device | undefined { + return Object.values(devices).find((d) => d.ieeeAddr === this.deviceIeeeAddress); + } + + removeFromAllGroups(): void { + Object.values(groups).forEach((g) => this.removeFromGroup(g)); } } -class Device { +export class Device { + type: string; + ieeeAddr: string; + dateCode: string | undefined; + networkAddress: number; + manufacturerID: number; + endpoints: Endpoint[]; + powerSource: string | undefined; + softwareBuildID: string | undefined; + interviewCompleted: boolean; + modelID: string | undefined; + interview: JestMockAny; + interviewing: boolean; + meta: Record; + ping: JestMockAny; + removeFromNetwork: JestMockAny; + removeFromDatabase: JestMockAny; + customClusters: Record; + addCustomCluster: JestMockAny; + save: JestMockAny; + manufacturerName: string | undefined; + lastSeen: number | undefined; + isDeleted: boolean; + linkquality?: number; + lqi: JestMockAny; + routingTable: JestMockAny; + constructor( - type, - ieeeAddr, - networkAddress, - manufacturerID, - endpoints, - interviewCompleted, - powerSource = null, - modelID = null, - interviewing = false, - manufacturerName, - dateCode = null, - softwareBuildID = null, + type: string, + ieeeAddr: string, + networkAddress: number, + manufacturerID: number, + endpoints: Endpoint[], + interviewCompleted: boolean, + powerSource: string | undefined = undefined, + modelID: string | undefined = undefined, + interviewing: boolean = false, + manufacturerName: string | undefined = undefined, + dateCode: string | undefined = undefined, + softwareBuildID: string | undefined = undefined, customClusters = {}, ) { this.type = type; @@ -196,14 +269,40 @@ class Device { this.save = jest.fn(); this.manufacturerName = manufacturerName; this.lastSeen = 1000; + this.isDeleted = false; + this.lqi = jest.fn(() => ({neighbors: []})); + this.routingTable = jest.fn(() => ({table: []})); } - getEndpoint(ID) { + getEndpoint(ID: number): Endpoint | undefined { return this.endpoints.find((e) => e.ID === ID); } } -const returnDevices = []; +export class Group { + groupID: number; + command: JestMockAny; + meta: Record; + members: Endpoint[]; + removeFromDatabase: JestMockAny; + removeFromNetwork: JestMockAny; + + constructor(groupID: number, members: Endpoint[]) { + this.groupID = groupID; + this.command = jest.fn(); + this.meta = {}; + this.members = members; + this.removeFromDatabase = jest.fn(); + this.removeFromNetwork = jest.fn(); + } + + hasMember(endpoint: Endpoint): boolean { + return this.members.includes(endpoint); + } +} + +export const events: Record = {}; +export const returnDevices: string[] = []; const bulb_color = new Device( 'Router', @@ -233,8 +332,8 @@ const bulb_color_2 = new Device( [], {lightingColorCtrl: {colorCapabilities: 254}}, [], - null, - null, + undefined, + undefined, {scenes: {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}}, ), ], @@ -350,7 +449,7 @@ const zigfred_plus = new Device( 'Siglis', ); -const groups = { +export const groups = { group_1: new Group(1, []), group_tradfri_remote: new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]), 'group/with/slashes': new Group(99, []), @@ -362,7 +461,7 @@ const groups = { ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]), }; -const devices = { +export const devices = { coordinator: new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false), bulb: new Device( 'Router', @@ -405,7 +504,7 @@ const devices = { 'BOSCH', '20231122', '3.05.09', - customClusterBTHRA, + CUSTOM_CLUSTER_BTHRA, ), bulb_color: bulb_color, bulb_2: bulb_2, @@ -423,7 +522,7 @@ const devices = { {target: groups.group_1, cluster: {ID: 6, name: 'genOnOff'}}, {target: groups.group_1, cluster: {ID: 6, name: 'genLevelCtrl'}}, ]), - new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6]), + new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6], '0x0017880104e45517'), ], true, 'Battery', @@ -434,7 +533,7 @@ const devices = { '0x0017880104e45518', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45518')], true, 'Battery', 'notSupportedModelID', @@ -446,7 +545,7 @@ const devices = { '0x0017880104e45529', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45529')], true, 'Battery', 'notSupportedModelID', @@ -456,7 +555,7 @@ const devices = { '0x0017880104e45530', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45530')], true, 'Battery', undefined, @@ -467,7 +566,7 @@ const devices = { '0x0017880104e45519', 6537, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45519')], true, 'Battery', 'lumi.sensor_switch.aq2', @@ -497,23 +596,59 @@ const devices = { '0x0017880104e45521', 6538, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45521'), new Endpoint(2, [0], [], '0x0017880104e45521')], true, 'Battery', 'lumi.sensor_86sw2.es1', ), - WSDCGQ11LM: new Device('EndDevice', '0x0017880104e45522', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), + WSDCGQ11LM: new Device( + 'EndDevice', + '0x0017880104e45522', + 6539, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45522')], + true, + 'Battery', + 'lumi.weather', + ), // This are not a real spammer device, just copy of previous to test the throttle filter - SPAMMER: new Device('EndDevice', '0x0017880104e455fe', 6539, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.weather'), - RTCGQ11LM: new Device('EndDevice', '0x0017880104e45523', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Battery', 'lumi.sensor_motion.aq2'), + SPAMMER: new Device( + 'EndDevice', + '0x0017880104e455fe', + 6539, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e455fe')], + true, + 'Battery', + 'lumi.weather', + ), + RTCGQ11LM: new Device( + 'EndDevice', + '0x0017880104e45523', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45523')], + true, + 'Battery', + 'lumi.sensor_motion.aq2', + ), ZNCZ02LM: ZNCZ02LM, - E1743: new Device('Router', '0x0017880104e45540', 6540, 4476, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'TRADFRI on/off switch'), + E1743: new Device( + 'Router', + '0x0017880104e45540', + 6540, + 4476, + [new Endpoint(1, [0], [], '0x0017880104e45540')], + true, + 'Mains (single phase)', + 'TRADFRI on/off switch', + ), QBKG04LM: new Device( 'Router', '0x0017880104e45541', 6549, 4151, - [new Endpoint(1, [0], [25]), new Endpoint(2, [0, 6], [])], + [new Endpoint(1, [0], [25], '0x0017880104e45541'), new Endpoint(2, [0, 6], [], '0x0017880104e45541')], true, 'Mains (single phase)', 'lumi.ctrl_neutral1', @@ -534,7 +669,11 @@ const devices = { '0x0017880104e45544', 6540, 4151, - [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], + [ + new Endpoint(11, [0], [], '0x0017880104e45544'), + new Endpoint(13, [0], [], '0x0017880104e45544'), + new Endpoint(12, [0], [], '0x0017880104e45544'), + ], true, 'Mains (single phase)', 'GL-C-008', @@ -555,7 +694,7 @@ const devices = { '0x0017880104e45547', 6540, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45547'), new Endpoint(2, [0], [], '0x0017880104e45547')], true, 'Mains (single phase)', 'lumi.curtain', @@ -565,22 +704,67 @@ const devices = { '0x0017880104e45548', 6540, 4151, - [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], + [new Endpoint(1, [0], [], '0x0017880104e45548'), new Endpoint(2, [0], [], '0x0017880104e45548')], true, 'Mains (single phase)', 'HDC52EastwindFan', ), - HS2WD: new Device('Router', '0x0017880104e45549', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'WarningDevice'), - '1TST_EU': new Device('Router', '0x0017880104e45550', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'Thermostat'), - SV01: new Device('Router', '0x0017880104e45551', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'SV01-410-MP-1.0'), - J1: new Device('Router', '0x0017880104e45552', 6540, 4151, [new Endpoint(1, [0], [])], true, 'Mains (single phase)', 'J1 (5502)'), - E11_G13: new Device('EndDevice', '0x0017880104e45553', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'E11-G13'), + HS2WD: new Device( + 'Router', + '0x0017880104e45549', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45549')], + true, + 'Mains (single phase)', + 'WarningDevice', + ), + '1TST_EU': new Device( + 'Router', + '0x0017880104e45550', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45550')], + true, + 'Mains (single phase)', + 'Thermostat', + ), + SV01: new Device( + 'Router', + '0x0017880104e45551', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45551')], + true, + 'Mains (single phase)', + 'SV01-410-MP-1.0', + ), + J1: new Device( + 'Router', + '0x0017880104e45552', + 6540, + 4151, + [new Endpoint(1, [0], [], '0x0017880104e45552')], + true, + 'Mains (single phase)', + 'J1 (5502)', + ), + E11_G13: new Device( + 'EndDevice', + '0x0017880104e45553', + 6540, + 4151, + [new Endpoint(1, [0, 6], [], '0x0017880104e45553')], + true, + 'Mains (single phase)', + 'E11-G13', + ), nomodel: new Device( 'Router', '0x0017880104e45535', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45535')], true, 'Mains (single phase)', undefined, @@ -591,15 +775,33 @@ const devices = { '0x0017880104e45525', 6536, 0, - [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5])], + [new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], '0x0017880104e45525')], true, 'Mains (single phase)', 'notSupportedModelID', false, 'Boef', ), - CC2530_ROUTER: new Device('Router', '0x0017880104e45559', 6540, 4151, [new Endpoint(1, [0, 6], [])], true, 'Mains (single phase)', 'lumi.router'), - LIVOLO: new Device('Router', '0x0017880104e45560', 6541, 4152, [new Endpoint(6, [0, 6], [])], true, 'Mains (single phase)', 'TI0001 '), + CC2530_ROUTER: new Device( + 'Router', + '0x0017880104e45559', + 6540, + 4151, + [new Endpoint(1, [0, 6], [], '0x0017880104e45559')], + true, + 'Mains (single phase)', + 'lumi.router', + ), + LIVOLO: new Device( + 'Router', + '0x0017880104e45560', + 6541, + 4152, + [new Endpoint(6, [0, 6], [], '0x0017880104e45560')], + true, + 'Mains (single phase)', + 'TI0001 ', + ), tradfri_remote: new Device( 'EndDevice', '0x90fd9ffffe4b64ae', @@ -712,7 +914,7 @@ const devices = { false, 'Centralite', ), - J1: new Device( + J1_cover: new Device( 'Router', '0x0017880104a44559', 6543, @@ -729,10 +931,10 @@ const devices = { 'EndDevice', '0x0017880104e45511', 1114, - 'external', + 0xffff, [new Endpoint(1, [], [], '0x0017880104e45511')], false, - null, + undefined, 'external_converter_device', ), QS_Zigbee_D02_TRIAC_2C_LN: new Device( @@ -753,7 +955,7 @@ const devices = { '0x0017880104e45561', 6544, 4151, - [new Endpoint(1, [0, 3, 4, 1026], [])], + [new Endpoint(1, [0, 3, 4, 1026], [], '0x0017880104e45561')], true, 'Battery', 'temperature.sensor', @@ -763,7 +965,7 @@ const devices = { '0x0017880104e45562', 6545, 4151, - [new Endpoint(1, [0, 3, 4, 513], [1026])], + [new Endpoint(1, [0, 3, 4, 513], [1026], '0x0017880104e45562')], true, 'Mains (single phase)', 'heating.actuator', @@ -819,10 +1021,10 @@ const devices = { 'Mains (single phase)', 'RBSH-MMS-ZB-EU', false, - null, - null, - null, - custom_clusters, + undefined, + undefined, + undefined, + CUSTOM_CLUSTERS, ), bulb_custom_cluster: new Device( 'Router', @@ -834,89 +1036,75 @@ const devices = { 'Mains (single phase)', 'TRADFRI bulb E27 WS opal 980lm', false, - null, - null, - null, - custom_clusters, + undefined, + undefined, + undefined, + CUSTOM_CLUSTERS, ), }; -const mock = { - setTransmitPower: jest.fn(), +export const mockController = { + on: (type: string, handler: EventHandler): void => { + events[type] = handler; + }, + start: jest.fn((): Promise => Promise.resolve('reset')), + stop: jest.fn(), + touchlinkIdentify: jest.fn(), + touchlinkScan: jest.fn(), touchlinkFactoryReset: jest.fn(), touchlinkFactoryResetFirst: jest.fn(), - touchlinkScan: jest.fn(), - touchlinkIdentify: jest.fn(), - start: jest.fn(), + addInstallCode: jest.fn(), + permitJoin: jest.fn(), + getPermitJoinTimeout: jest.fn((): number => 0), + isStopping: jest.fn((): boolean => false), backup: jest.fn(), coordinatorCheck: jest.fn(), - isStopping: jest.fn(), - permitJoin: jest.fn(), - addInstallCode: jest.fn(), - getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1, revision: 20190425}}), - getNetworkParameters: jest.fn().mockReturnValue({panID: 0x162a, extendedPanID: [0, 11, 22], channel: 15}), - on: (type, handler) => { - events[type] = handler; - }, - stop: jest.fn(), - getDevicesIterator: jest.fn().mockImplementation(function* (predicate) { + getCoordinatorVersion: jest.fn((): Promise => Promise.resolve({type: 'z-Stack', meta: {version: 1, revision: 20190425}})), + getNetworkParameters: jest.fn((): Promise => Promise.resolve({panID: 0x162a, extendedPanID: 0x001122, channel: 15})), + getDevices: jest.fn((): Device[] => []), + getDevicesIterator: jest.fn(function* (predicate?: (value: Device) => boolean): Generator { for (const key in devices) { - const device = devices[key]; + const device = devices[key as keyof typeof devices]; if ((returnDevices.length === 0 || returnDevices.includes(device.ieeeAddr)) && !device.isDeleted && (!predicate || predicate(device))) { yield device; } } }), - getDevicesByType: jest.fn().mockImplementation((type) => { - return Object.values(devices) + getDevicesByType: jest.fn((type: DeviceType): Device[] => + Object.values(devices) .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) - .filter((d) => d.type === type); - }), - getDeviceByIeeeAddr: jest.fn().mockImplementation((ieeeAddr) => { - return Object.values(devices) + .filter((d) => d.type === type), + ), + getDeviceByIeeeAddr: jest.fn((ieeeAddr: string): Device | undefined => + Object.values(devices) .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) - .find((d) => d.ieeeAddr === ieeeAddr); - }), - getDeviceByNetworkAddress: jest.fn().mockImplementation((networkAddress) => { - return Object.values(devices) + .find((d) => d.ieeeAddr === ieeeAddr), + ), + getDeviceByNetworkAddress: jest.fn((networkAddress: number): Device | undefined => + Object.values(devices) .filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) - .find((d) => d.networkAddress === networkAddress); - }), - getGroupsIterator: jest.fn().mockImplementation(function* (predicate) { + .find((d) => d.networkAddress === networkAddress), + ), + getGroupByID: jest.fn((groupID: number): Group | undefined => Object.values(groups).find((g) => g.groupID === groupID)), + getGroups: jest.fn((): Group[] => []), + getGroupsIterator: jest.fn(function* (predicate?: (value: Group) => boolean): Generator { for (const key in groups) { - const group = groups[key]; + const group = groups[key as keyof typeof groups]; if (!predicate || predicate(group)) { yield group; } } }), - getGroupByID: jest.fn().mockImplementation((groupID) => { - return Object.values(groups).find((d) => d.groupID === groupID); - }), - getPermitJoinTimeout: jest.fn().mockReturnValue(0), - reset: jest.fn(), - createGroup: jest.fn().mockImplementation((groupID) => { + createGroup: jest.fn((groupID: number): Group => { const group = new Group(groupID, []); - groups[`group_${groupID}`] = group; + groups[`group_${groupID}` as keyof typeof groups] = group; return group; }), }; -const mockConstructor = jest.fn().mockImplementation(() => mock); - jest.mock('zigbee-herdsman', () => ({ ...jest.requireActual('zigbee-herdsman'), - Controller: mockConstructor, + Controller: jest.fn().mockImplementation(() => mockController), })); - -module.exports = { - events, - ...mock, - constructor: mockConstructor, - devices, - groups, - returnDevices, - custom_clusters, -}; diff --git a/test/onEvent.test.js b/test/onEvent.test.js deleted file mode 100644 index b84dbccfde..0000000000 --- a/test/onEvent.test.js +++ /dev/null @@ -1,96 +0,0 @@ -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); -zigbeeHerdsman.returnDevices.push('0x0017880104e45560'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); - -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; - -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const mockOnEvent = jest.fn(); -const mappedLivolo = zigbeeHerdsmanConverters.findByModel('TI0001'); -mappedLivolo.onEvent = mockOnEvent; -zigbeeHerdsmanConverters.onEvent = jest.fn(); - -describe('On event', () => { - let controller; - const device = zigbeeHerdsman.devices.LIVOLO; - - beforeEach(async () => { - jest.useFakeTimers(); - data.writeDefaultConfiguration(); - settings.reRead(); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - await flushPromises(); - }); - - beforeEach(async () => { - controller.state.state = {}; - data.writeDefaultConfiguration(); - settings.reRead(); - mocksClear.forEach((m) => m.mockClear()); - zigbeeHerdsmanConverters.onEvent.mockClear(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - it('Should call with start event', async () => { - expect(mockOnEvent).toHaveBeenCalledTimes(1); - const call = mockOnEvent.mock.calls[0]; - expect(call[0]).toBe('start'); - expect(call[1]).toStrictEqual({}); - expect(call[2]).toBe(device); - expect(call[3]).toStrictEqual(settings.getDevice(device.ieeeAddr)); - expect(call[4]).toStrictEqual({}); - }); - - it('Should call with stop event', async () => { - mockOnEvent.mockClear(); - await controller.stop(); - await flushPromises(); - expect(mockOnEvent).toHaveBeenCalledTimes(1); - const call = mockOnEvent.mock.calls[0]; - expect(call[0]).toBe('stop'); - expect(call[1]).toStrictEqual({}); - expect(call[2]).toBe(device); - }); - - it('Should call with zigbee event', async () => { - mockOnEvent.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device}); - await flushPromises(); - expect(mockOnEvent).toHaveBeenCalledTimes(1); - expect(mockOnEvent).toHaveBeenCalledWith( - 'deviceAnnounce', - {device}, - device, - settings.getDevice(device.ieeeAddr), - {}, - { - deviceExposesChanged: expect.any(Function), - }, - ); - - // Test deviceExposesChanged - MQTT.publish.mockClear(); - console.log(mockOnEvent.mock.calls[0][5].deviceExposesChanged()); - expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices'); - }); - - it('Should call index onEvent with zigbee event', async () => { - zigbeeHerdsmanConverters.onEvent.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device}); - await flushPromises(); - expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledWith('deviceAnnounce', {device}, device, { - deviceExposesChanged: expect.any(Function), - }); - }); -}); diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js deleted file mode 100644 index 812d555f24..0000000000 --- a/test/otaUpdate.test.js +++ /dev/null @@ -1,459 +0,0 @@ -const path = require('path'); - -const data = require('./stub/data'); -const logger = require('./stub/logger'); -const sleep = require('./stub/sleep'); -const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); -const MQTT = require('./stub/mqtt'); -const settings = require('../lib/util/settings'); -const Controller = require('../lib/controller'); -const flushPromises = require('./lib/flushPromises'); -const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const stringify = require('json-stable-stringify-without-jsonify'); -const zigbeeOTA = require('zigbee-herdsman-converters/lib/ota/zigbeeOTA'); - -const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride'); - -describe('OTA update', () => { - let controller; - - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'OTAUpdate'); - await controller.enableDisableExtension(true, 'OTAUpdate'); - }; - - const mockClear = (mapped) => { - mapped.ota.updateToLatest = jest.fn(); - mapped.ota.isUpdateAvailable = jest.fn(); - }; - - beforeAll(async () => { - data.writeDefaultConfiguration(); - settings.reRead(); - data.writeDefaultConfiguration(); - settings.set(['ota', 'ikea_ota_use_test_url'], true); - settings.reRead(); - jest.useFakeTimers(); - controller = new Controller(jest.fn(), jest.fn()); - sleep.mock(); - await controller.start(); - await jest.runOnlyPendingTimers(); - await flushPromises(); - }); - - afterEach(async () => { - settings.set(['ota', 'disable_automatic_update_check'], false); - jest.runOnlyPendingTimers(); - }); - - afterAll(async () => { - jest.useRealTimers(); - sleep.restore(); - }); - - beforeEach(async () => { - const extension = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate'); - extension.lastChecked = {}; - extension.inProgress = new Set(); - controller.state.state = {}; - MQTT.publish.mockClear(); - }); - - it('Should OTA update a device', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - let count = 0; - endpoint.read.mockImplementation(() => { - count++; - return {swBuildId: count, dateCode: '2019010' + count}; - }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - logger.info.mockClear(); - device.save.mockClear(); - mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { - onUpdate(0, null); - onUpdate(10, 3600.2123); - return 90; - }); - - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); - await flushPromises(); - expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, expect.any(Function)); - expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); - expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`); - expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); - expect(logger.info).toHaveBeenCalledWith( - `Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`, - ); - expect(device.save).toHaveBeenCalledTimes(2); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); - expect(endpoint.read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'updating', progress: 0}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'updating', progress: 10, remaining: 3600}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({ - data: {from: {date_code: '20190101', software_build_id: 1}, id: 'bulb', to: {date_code: '20190102', software_build_id: 2}}, - status: 'ok', - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); - }); - - it('Should handle when OTA update fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => { - return {swBuildId: 1, dateCode: '2019010'}; - }); - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - device.save.mockClear(); - mapped.ota.updateToLatest.mockImplementationOnce((a, onUpdate) => { - throw new Error('Update failed'); - }); - - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'})); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'available'}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({data: {id: 'bulb'}, status: 'error', error: "Update of 'bulb' failed (Update failed)"}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should be able to check if OTA update is available', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - - MQTT.publish.mockClear(); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should handle if OTA update check fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - throw new Error('RF signals disturbed because of dogs barking'); - }); - - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({ - data: {id: 'bulb'}, - status: 'error', - error: `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`, - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should fail when device does not exist', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'not_existing_deviceooo'}, status: 'error', error: `Device 'not_existing_deviceooo' does not exist`}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should not check for OTA when device does not support it', async () => { - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'dimmer_wall_switch'}, status: 'error', error: `Device 'dimmer_wall_switch' does not support OTA updates`}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should refuse to check/update when already in progress', async () => { - const device = zigbeeHerdsman.devices.bulb; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), 99999); - }); - }); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb'); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - jest.runOnlyPendingTimers(); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/check', - stringify({data: {id: 'bulb'}, status: 'error', error: `Update or check for update already in progress for 'bulb'`}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Shouldnt crash when read modelID before/after OTA update fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.endpoints[0]; - endpoint.read.mockImplementation(() => { - throw new Error('Failed!'); - }); - - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb'); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/ota_update/update', - stringify({data: {id: 'bulb', from: null, to: null}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should check for update when device requests it', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12}); - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - logger.info.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); - expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); - - // Should not request again when device asks again after a short time - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - - logger.info.mockClear(); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10}); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should respond with NO_IMAGE_AVAILABLE when update available request fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { - throw new Error('Nothing to find here'); - }); - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - logger.info.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'idle'}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should check for update when device requests it and it is not available', async () => { - const device = zigbeeHerdsman.devices.bulb; - device.endpoints[0].commandResponse.mockClear(); - const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: false, currentFileVersion: 13, otaFileVersion: 13}); - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - logger.info.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, {imageType: 12382}); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}), - {retain: true, qos: 0}, - expect.any(Function), - ); - }); - - it('Should not check for update when device requests it and disable_automatic_update_check is set to true', async () => { - settings.set(['ota', 'disable_automatic_update_check'], true); - const device = zigbeeHerdsman.devices.bulb; - const data = {imageType: 12382}; - const mapped = await zigbeeHerdsmanConverters.findByDevice(device); - mockClear(mapped); - mapped.ota.isUpdateAvailable.mockReturnValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 13}); - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - logger.info.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); - }); - - it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => { - const device = zigbeeHerdsman.devices.HGZB04D; - const data = {imageType: 12382}; - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); - }); - - it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA and device has no OTA endpoint to standard endpoint', async () => { - const device = zigbeeHerdsman.devices.SV01; - const data = {imageType: 12382}; - const payload = { - data, - cluster: 'genOta', - device, - endpoint: device.getEndpoint(1), - type: 'commandQueryNextImageRequest', - linkquality: 10, - meta: {zclTransactionSequenceNumber: 10}, - }; - logger.error.mockClear(); - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(1); - expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10); - }); - - it('Set zigbee_ota_override_index_location', async () => { - settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json'); - await resetExtension(); - expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json')); - spyUseIndexOverride.mockClear(); - - settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json'); - await resetExtension(); - expect(spyUseIndexOverride).toHaveBeenCalledWith('http://my.site/index.json'); - spyUseIndexOverride.mockClear(); - }); - - it('Clear update state on startup', async () => { - const device = controller.zigbee.resolveEntity(zigbeeHerdsman.devices.bulb_color.ieeeAddr); - controller.state.set(device, {update: {progress: 100, remaining: 10, state: 'updating'}}); - await resetExtension(); - expect(controller.state.get(device)).toStrictEqual({update: {state: 'available'}}); - }); -}); diff --git a/test/settings.test.js b/test/settings.test.ts similarity index 95% rename from test/settings.test.js rename to test/settings.test.ts index 1902648da5..016329e57f 100644 --- a/test/settings.test.js +++ b/test/settings.test.ts @@ -1,17 +1,19 @@ -require('./stub/logger'); -require('./stub/data'); -const data = require('../lib/util/data'); -const utils = require('../lib/util/utils').default; -const settings = require('../lib/util/settings.ts'); -const fs = require('fs'); -const configurationFile = data.joinPath('configuration.yaml'); -const devicesFile = data.joinPath('devices.yaml'); -const devicesFile2 = data.joinPath('devices2.yaml'); -const groupsFile = data.joinPath('groups.yaml'); -const secretFile = data.joinPath('secret.yaml'); -const yaml = require('js-yaml'); -const objectAssignDeep = require(`object-assign-deep`); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as data from './mocks/data'; +import fs from 'fs'; + +import yaml from 'js-yaml'; +import objectAssignDeep from 'object-assign-deep'; + +import mockedData from '../lib/util/data'; +import * as settings from '../lib/util/settings'; + +const configurationFile = mockedData.joinPath('configuration.yaml'); +const devicesFile = mockedData.joinPath('devices.yaml'); +const devicesFile2 = mockedData.joinPath('devices2.yaml'); +const groupsFile = mockedData.joinPath('groups.yaml'); +const secretFile = mockedData.joinPath('secret.yaml'); const minimalConfig = { external_converters: [], homeassistant: true, @@ -19,22 +21,28 @@ const minimalConfig = { }; describe('Settings', () => { - const write = (file, json, reread = true) => { + const write = (file: string, json: Record, reread: boolean = true): void => { fs.writeFileSync(file, yaml.dump(json)); + if (reread) { settings.reRead(); } }; - const read = (file) => yaml.load(fs.readFileSync(file, 'utf8')); - const remove = (file) => { - if (fs.existsSync(file)) fs.unlinkSync(file); + + const read = (file: string): unknown => yaml.load(fs.readFileSync(file, 'utf8')); + + const remove = (file: string): void => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } }; - const clearEnvironmentVariables = () => { - Object.keys(process.env).forEach((key) => { + + const clearEnvironmentVariables = (): void => { + for (const key in process.env) { if (key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) { delete process.env[key]; } - }); + } }; beforeEach(() => { @@ -48,6 +56,7 @@ describe('Settings', () => { it('Should return default settings', () => { write(configurationFile, {}); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; @@ -57,6 +66,7 @@ describe('Settings', () => { it('Should return settings', () => { write(configurationFile, {external_converters: ['abcd.js']}); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = {}; expected.groups = {}; @@ -66,7 +76,7 @@ describe('Settings', () => { it('Should apply environment variables', () => { process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true'; - process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = 15; + process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_CHANNEL'] = '15'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_OUTPUT'] = 'attribute_and_json'; process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_LOG_OUTPUT'] = '["console"]'; process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = @@ -88,6 +98,7 @@ describe('Settings', () => { expect(settings.validate()).toStrictEqual([]); const s = settings.get(); + // @ts-expect-error workaround const expected = objectAssignDeep.noMutate({}, settings.testing.defaults); expected.devices = { '0x00158d00018255df': { @@ -336,7 +347,7 @@ describe('Settings', () => { expect(read(devicesFile)).toStrictEqual(expected); }); - function extractFromMultipleDeviceConfigs(contentDevices2) { + function extractFromMultipleDeviceConfigs(contentDevices2: Record): void { const contentConfiguration = { devices: ['devices.yaml', 'devices2.yaml'], }; @@ -380,7 +391,7 @@ describe('Settings', () => { }); it('Should add devices for first file when using 2 separates file and the second file is empty', () => { - extractFromMultipleDeviceConfigs(null); + extractFromMultipleDeviceConfigs({}); }); it('Should add devices to a separate file if devices.yaml doesnt exist', () => { @@ -520,7 +531,7 @@ describe('Settings', () => { it('Should add groups', () => { write(configurationFile, {}); - const added = settings.addGroup('test123'); + settings.addGroup('test123'); const expected = { 1: { friendly_name: 'test123', @@ -533,7 +544,7 @@ describe('Settings', () => { it('Should add groups with specific ID', () => { write(configurationFile, {}); - const added = settings.addGroup('test123', 123); + settings.addGroup('test123', '123'); const expected = { 123: { friendly_name: 'test123', @@ -578,9 +589,9 @@ describe('Settings', () => { it('Should not add duplicate groups with specific ID', () => { write(configurationFile, {}); - settings.addGroup('test123', 123); + settings.addGroup('test123', '123'); expect(() => { - settings.addGroup('test_id_123', 123); + settings.addGroup('test_id_123', '123'); }).toThrow(new Error("Group ID '123' is already in use")); const expected = { 123: { @@ -676,7 +687,7 @@ describe('Settings', () => { }); expect(() => { - settings.removeDeviceFromGroup('test123', 'bulb'); + settings.removeDeviceFromGroup('test123', ['bulb']); }).toThrow(new Error("Group 'test123' does not exist")); }); diff --git a/test/stub/logger.js b/test/stub/logger.js deleted file mode 100644 index c03dd45c47..0000000000 --- a/test/stub/logger.js +++ /dev/null @@ -1,48 +0,0 @@ -let level = 'info'; -let debugNamespaceIgnore = ''; -let namespacedLevels = {}; -let transports = []; -let transportsEnabled = false; - -const getMessage = (messageOrLambda) => (messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda); -const mock = { - log: jest.fn().mockImplementation((level, message, namespace = 'z2m') => { - if (transportsEnabled) { - for (const transport of transports) { - transport.log({level, message, namespace}, () => {}); - } - } - }), - init: jest.fn(), - info: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('info', getMessage(messageOrLambda), namespace)), - warning: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('warning', getMessage(messageOrLambda), namespace)), - error: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('error', getMessage(messageOrLambda), namespace)), - debug: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mock.log('debug', getMessage(messageOrLambda), namespace)), - cleanup: jest.fn(), - logOutput: jest.fn(), - add: (transport) => transports.push(transport), - addTransport: (transport) => transports.push(transport), - removeTransport: (transport) => { - transports = transports.filter((t) => t !== transport); - }, - setLevel: (newLevel) => { - level = newLevel; - }, - getLevel: () => level, - setNamespacedLevels: (nsLevels) => { - namespacedLevels = nsLevels; - }, - getNamespacedLevels: () => namespacedLevels, - setDebugNamespaceIgnore: (newIgnore) => { - debugNamespaceIgnore = newIgnore; - }, - getDebugNamespaceIgnore: () => debugNamespaceIgnore, - setTransportsEnabled: (value) => { - transportsEnabled = value; - }, - end: jest.fn(), -}; - -jest.mock('../../lib/util/logger', () => mock); - -module.exports = {...mock}; diff --git a/test/stub/sleep.js b/test/stub/sleep.js deleted file mode 100644 index 0a74c3aae7..0000000000 --- a/test/stub/sleep.js +++ /dev/null @@ -1,10 +0,0 @@ -const utils = require('../../lib/util/utils'); -const spy = jest.spyOn(utils.default, 'sleep'); - -export function mock() { - spy.mockImplementation(() => {}); -} - -export function restore() { - spy.mockRestore(); -} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000000..df7373f526 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig", + "include": ["./**/*"], + "compilerOptions": { + "types": ["jest"], + "rootDir": "..", + "noEmit": true + }, + "references": [{"path": ".."}] +} diff --git a/test/utils.test.js b/test/utils.test.ts similarity index 68% rename from test/utils.test.js rename to test/utils.test.ts index 76e5bd5697..94cd8c33d3 100644 --- a/test/utils.test.js +++ b/test/utils.test.ts @@ -1,7 +1,7 @@ -const utils = require('../lib/util/utils').default; -const version = require('../package.json').version; -const versionHerdsman = require('../node_modules/zigbee-herdsman/package.json').version; -const versionHerdsmanConverters = require('../node_modules/zigbee-herdsman-converters/package.json').version; +import fs from 'fs'; +import path from 'path'; + +import utils from '../lib/util/utils'; describe('Utils', () => { it('Object is empty', () => { @@ -15,12 +15,12 @@ describe('Utils', () => { }); it('git last commit', async () => { - let mockReturnValue = []; + const version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; + let mockReturnValue: [identical: boolean, result: {shortHash: string} | null] = [false, {shortHash: '123'}]; jest.mock('git-last-commit', () => ({ - getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]), + getLastCommit: (cb: (identical: boolean, result: {shortHash: string} | null) => void): void => cb(mockReturnValue[0], mockReturnValue[1]), })); - mockReturnValue = [false, {shortHash: '123'}]; expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: '123', version: version}); mockReturnValue = [true, null]; @@ -28,19 +28,25 @@ describe('Utils', () => { }); it('Check dependency version', async () => { + const versionHerdsman = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman', 'package.json'), 'utf8'), + ).version; + const versionHerdsmanConverters = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'zigbee-herdsman-converters', 'package.json'), 'utf8'), + ).version; expect(await utils.getDependencyVersion('zigbee-herdsman')).toStrictEqual({version: versionHerdsman}); expect(await utils.getDependencyVersion('zigbee-herdsman-converters')).toStrictEqual({version: versionHerdsmanConverters}); }); it('To local iso string', async () => { - var date = new Date('August 19, 1975 23:15:30 UTC+00:00'); - var getTimezoneOffset = Date.prototype.getTimezoneOffset; - Date.prototype.getTimezoneOffset = () => 60; - expect(utils.formatDate(date, 'ISO_8601_local').endsWith('-01:00')).toBeTruthy(); - Date.prototype.getTimezoneOffset = () => -60; - expect(utils.formatDate(date, 'ISO_8601_local').endsWith('+01:00')).toBeTruthy(); - Date.prototype.getTimezoneOffset = getTimezoneOffset; + const date = new Date('August 19, 1975 23:15:30 UTC+00:00').getTime(); + const getTzOffsetSpy = jest.spyOn(Date.prototype, 'getTimezoneOffset'); + getTzOffsetSpy.mockReturnValueOnce(60); + expect(utils.formatDate(date, 'ISO_8601_local').toString().endsWith('-01:00')).toBeTruthy(); + getTzOffsetSpy.mockReturnValueOnce(-60); + expect(utils.formatDate(date, 'ISO_8601_local').toString().endsWith('+01:00')).toBeTruthy(); }); + it('Removes null properties from object', () => { const obj1 = { ab: 0, diff --git a/tsconfig.json b/tsconfig.json index f7dc5307fd..27f73c8f73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,12 +13,11 @@ "declarationMap": true, "outDir": "dist", "baseUrl": ".", - "allowJs": true, "rootDir": "lib", "inlineSourceMap": true, "resolveJsonModule": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "composite": true }, - "include": ["lib/**/*", "lib/util/settings.schema.json"], - "exclude": ["node_modules"] + "include": ["./lib/**/*", "./lib/util/settings.schema.json"] } From 3f76586a506a3674333d17cff93d32810c76687e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 21 Oct 2024 21:47:40 +0200 Subject: [PATCH 08/70] fix!: Remove configuring group members through `configuration.yaml` (#24338) * fix!: Remove configuring group members through `configuration.yaml` * Updates * Updates * fix * Updates * Feedback --- lib/extension/groups.ts | 95 --------------- lib/types/types.d.ts | 3 +- lib/util/settings.schema.json | 6 - lib/util/settings.ts | 62 +--------- test/extensions/bridge.test.ts | 74 ++++++------ test/extensions/groups.test.ts | 206 ++++----------------------------- test/mocks/data.ts | 6 - test/mocks/zigbeeHerdsman.ts | 11 +- test/settings.test.ts | 108 +++-------------- 9 files changed, 85 insertions(+), 486 deletions(-) diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index 114774c35a..eb5b0e6ed7 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -45,79 +45,6 @@ export default class Groups extends Extension { override async start(): Promise { this.eventBus.onStateChange(this, this.onStateChange); this.eventBus.onMQTTMessage(this, this.onMQTTMessage); - await this.syncGroupsWithSettings(); - } - - private async syncGroupsWithSettings(): Promise { - const settingsGroups = settings.getGroups(); - - const addRemoveFromGroup = async ( - action: 'add' | 'remove', - deviceName: string, - groupName: string | number, - endpoint: zh.Endpoint, - group: Group, - ): Promise => { - try { - logger.info(`${action === 'add' ? 'Adding' : 'Removing'} '${deviceName}' to group '${groupName}'`); - - if (action === 'remove') { - await endpoint.removeFromGroup(group.zh); - } else { - await endpoint.addToGroup(group.zh); - } - } catch (error) { - logger.error(`Failed to ${action} '${deviceName}' from '${groupName}'`); - logger.debug((error as Error).stack!); - } - }; - - for (const settingGroup of settingsGroups) { - const groupID = settingGroup.ID; - const zigbeeGroup = this.zigbee.groupsIterator((g) => g.groupID === groupID).next().value || this.zigbee.createGroup(groupID); - const settingsEndpoints: zh.Endpoint[] = []; - - for (const d of settingGroup.devices) { - const parsed = this.zigbee.resolveEntityAndEndpoint(d); - const device = parsed.entity as Device; - - if (!device) { - logger.error(`Cannot find '${d}' of group '${settingGroup.friendly_name}'`); - } - - if (!parsed.endpoint) { - if (parsed.endpointID) { - logger.error(`Cannot find endpoint '${parsed.endpointID}' of device '${parsed.ID}'`); - } - - continue; - } - - // In settings but not in zigbee - if (!zigbeeGroup.zh.hasMember(parsed.endpoint)) { - await addRemoveFromGroup('add', device?.name, settingGroup.friendly_name, parsed.endpoint, zigbeeGroup); - } - - settingsEndpoints.push(parsed.endpoint); - } - - // In zigbee but not in settings - for (const endpoint of zigbeeGroup.zh.members) { - if (!settingsEndpoints.includes(endpoint)) { - const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr)!.friendly_name; - - await addRemoveFromGroup('remove', deviceName, settingGroup.friendly_name, endpoint, zigbeeGroup); - } - } - } - - for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) { - for (const endpoint of zigbeeGroup.zh.members) { - const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr)!.friendly_name; - - await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup); - } - } } @bind async onStateChange(data: eventdata.StateChange): Promise { @@ -320,33 +247,15 @@ export default class Groups extends Extension { if (!error) { assert(resolvedEntityEndpoint, '`resolvedEntityEndpoint` is missing'); try { - const keys = [ - `${resolvedEntityDevice.ieeeAddr}/${resolvedEntityEndpoint.ID}`, - `${resolvedEntityDevice.name}/${resolvedEntityEndpoint.ID}`, - ]; - const endpointNameLocal = resolvedEntityDevice.endpointName(resolvedEntityEndpoint); - - if (endpointNameLocal) { - keys.push(`${resolvedEntityDevice.ieeeAddr}/${endpointNameLocal}`); - keys.push(`${resolvedEntityDevice.name}/${endpointNameLocal}`); - } - - if (!endpointNameLocal) { - keys.push(resolvedEntityDevice.name); - keys.push(resolvedEntityDevice.ieeeAddr); - } - if (type === 'add') { assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing'); logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`); await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh); - settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys); changedGroups.push(resolvedEntityGroup); } else if (type === 'remove') { assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing'); logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`); await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh); - settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys); changedGroups.push(resolvedEntityGroup); } else { // remove_all @@ -357,10 +266,6 @@ export default class Groups extends Extension { } await resolvedEntityEndpoint.removeFromAllGroups(); - - for (const settingsGroup of settings.getGroups()) { - settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys); - } } } catch (e) { error = `Failed to ${type} from group (${(e as Error).message})`; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 312a442892..23859a66ea 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -185,7 +185,7 @@ declare global { ssl_key?: string; }; devices: {[s: string]: DeviceOptions}; - groups: {[s: string]: OptionalProps, 'devices'>}; + groups: {[s: string]: Omit}; device_options: KeyValue; advanced: { log_rotation: boolean; @@ -239,7 +239,6 @@ declare global { } interface GroupOptions { - devices: string[]; ID: number; optimistic?: boolean; off_state?: 'all_members_off' | 'last_member_state'; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index 8043099cd6..f30239472f 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -911,12 +911,6 @@ "retain": { "type": "boolean" }, - "devices": { - "type": "array", - "items": { - "type": "string" - } - }, "optimistic": { "type": "boolean" }, diff --git a/lib/util/settings.ts b/lib/util/settings.ts index eabd8be7fe..1fd12fbcc2 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -533,26 +533,18 @@ export function getGroup(IDorName: string | number): GroupOptions | undefined { const byID = settings.groups[IDorName]; if (byID) { - return {devices: [], ...byID, ID: Number(IDorName)}; + return {...byID, ID: Number(IDorName)}; } for (const [ID, group] of Object.entries(settings.groups)) { if (group.friendly_name === IDorName) { - return {devices: [], ...group, ID: Number(ID)}; + return {...group, ID: Number(ID)}; } } return undefined; } -export function getGroups(): GroupOptions[] { - const settings = get(); - - return Object.entries(settings.groups).map(([ID, group]) => { - return {devices: [], ...group, ID: Number(ID)}; - }); -} - function getGroupThrowIfNotExists(IDorName: string): GroupOptions { const group = getGroup(IDorName); @@ -620,16 +612,6 @@ export function removeDevice(IDorName: string): void { const device = getDeviceThrowIfNotExists(IDorName); const settings = getInternalSettings(); delete settings.devices?.[device.ID]; - - // Remove device from groups - if (settings.groups) { - const regex = new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`); - - for (const group of Object.values(settings.groups).filter((g) => g.devices)) { - group.devices = group.devices?.filter((device) => !device.match(regex)); - } - } - write(); } @@ -667,46 +649,6 @@ export function addGroup(name: string, ID?: string): GroupOptions { return getGroup(ID)!; // valid from creation above } -function groupGetDevice(group: {devices?: string[]}, keys: string[]): string | undefined { - for (const device of group.devices ?? []) { - if (keys.includes(device)) { - return device; - } - } - - return undefined; -} - -export function addDeviceToGroup(IDorName: string, keys: string[]): void { - const groupID = getGroupThrowIfNotExists(IDorName).ID!; - const settings = getInternalSettings(); - - const group = settings.groups![groupID]; - - if (!groupGetDevice(group, keys)) { - if (!group.devices) group.devices = []; - group.devices.push(keys[0]); - write(); - } -} - -export function removeDeviceFromGroup(IDorName: string, keys: string[]): void { - const groupID = getGroupThrowIfNotExists(IDorName).ID!; - const settings = getInternalSettings(); - const group = settings.groups![groupID]; - - if (!group.devices) { - return; - } - - const key = groupGetDevice(group, keys); - - if (key) { - group.devices = group.devices.filter((d) => d != key); - write(); - } -} - export function removeGroup(IDorName: string | number): void { const groupID = getGroupThrowIfNotExists(IDorName.toString()).ID!; const settings = getInternalSettings(); diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 88a20ad4f9..79fb2e69d6 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -184,13 +184,13 @@ describe('Extension: Bridge', () => { external_converters: [], groups: { 1: {friendly_name: 'group_1', retain: false}, - 11: {devices: ['bulb_2'], friendly_name: 'group_with_tradfri', retain: false}, - 12: {devices: ['TS0601_thermostat'], friendly_name: 'thermostat_group', retain: false}, - 14: {devices: ['power_plug', 'bulb_2'], friendly_name: 'switch_group', retain: false}, - 15071: {devices: ['bulb_color_2', 'bulb_2'], friendly_name: 'group_tradfri_remote', retain: false}, + 11: {friendly_name: 'group_with_tradfri', retain: false}, + 12: {friendly_name: 'thermostat_group', retain: false}, + 14: {friendly_name: 'switch_group', retain: false}, + 15071: {friendly_name: 'group_tradfri_remote', retain: false}, 2: {friendly_name: 'group_2', retain: false}, - 21: {devices: ['GLEDOPTO_2ID/cct'], friendly_name: 'gledopto_group'}, - 9: {devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], friendly_name: 'ha_discovery_group'}, + 21: {friendly_name: 'gledopto_group'}, + 9: {friendly_name: 'ha_discovery_group'}, }, homeassistant: false, map_options: { @@ -2152,27 +2152,45 @@ describe('Extension: Bridge', () => { it('Should publish groups on startup', async () => { await resetExtension(); mockLogger.setTransportsEnabled(true); + // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/groups')); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', stringify([ {friendly_name: 'group_1', id: 1, members: [], scenes: []}, - {friendly_name: 'group_tradfri_remote', id: 15071, members: [{endpoint: 1, ieee_address: '0x000b57fffec6a5b4'}], scenes: []}, + {friendly_name: 'group_2', id: 2, members: [], scenes: []}, + { + friendly_name: 'group_tradfri_remote', + id: 15071, + members: [ + {endpoint: 1, ieee_address: '0x000b57fffec6a5b4'}, + {endpoint: 1, ieee_address: '0x000b57fffec6a5b7'}, + ], + scenes: [], + }, {friendly_name: '99', id: 99, members: [], scenes: []}, - {friendly_name: 'group_with_tradfri', id: 11, members: [], scenes: []}, - {friendly_name: 'thermostat_group', id: 12, members: [], scenes: []}, - {friendly_name: 'switch_group', id: 14, members: [{endpoint: 1, ieee_address: '0x0017880104e45524'}], scenes: []}, - {friendly_name: 'gledopto_group', id: 21, members: [], scenes: []}, + {friendly_name: 'group_with_tradfri', id: 11, members: [{endpoint: 1, ieee_address: '0x000b57fffec6a5b7'}], scenes: []}, + {friendly_name: 'thermostat_group', id: 12, members: [{endpoint: 1, ieee_address: '0x0017882104a44559'}], scenes: []}, + { + friendly_name: 'switch_group', + id: 14, + members: [ + {endpoint: 1, ieee_address: '0x0017880104e45524'}, + {endpoint: 1, ieee_address: '0x000b57fffec6a5b7'}, + ], + scenes: [], + }, + {friendly_name: 'gledopto_group', id: 21, members: [{endpoint: 15, ieee_address: '0x0017880104e45724'}], scenes: []}, {friendly_name: 'default_bind_group', id: 901, members: [], scenes: []}, { friendly_name: 'ha_discovery_group', id: 9, members: [ {endpoint: 1, ieee_address: '0x000b57fffec6a5b4'}, - {endpoint: 2, ieee_address: '0x0017880104e45542'}, + {endpoint: 1, ieee_address: '0x000b57fffec6a5b7'}, + {endpoint: 3, ieee_address: '0x0017880104e45542'}, ], scenes: [{id: 4, name: 'Scene 4'}], }, - {friendly_name: 'group_2', id: 2, members: [], scenes: []}, ]), {retain: true, qos: 0}, expect.any(Function), @@ -2766,22 +2784,6 @@ describe('Extension: Bridge', () => { it('Should allow to remove device by string', async () => { const device = devices.bulb; - settings.set(['groups'], { - 1: { - friendly_name: 'group_1', - retain: false, - devices: [ - '0x999b57fffec6a5b9/1', - '0x000b57fffec6a5b2/1', - 'bulb', - 'bulb/right', - 'other_bulb', - 'bulb_1', - '0x000b57fffec6a5b2', - 'bulb/room/2', - ], - }, - }); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); await flushPromises(); @@ -2799,7 +2801,6 @@ describe('Extension: Bridge', () => { expect.any(Function), ); expect(settings.get().blocklist).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual(['0x999b57fffec6a5b9/1', 'other_bulb', 'bulb_1', 'bulb/room/2']); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); }); @@ -2970,7 +2971,7 @@ describe('Extension: Bridge', () => { mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/rename', stringify({from: 'group_1', to: 'group_new_name'})); await flushPromises(); expect(settings.getGroup('group_1')).toBeUndefined(); - expect(settings.getGroup('group_new_name')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_new_name', retain: false}); + expect(settings.getGroup('group_new_name')).toStrictEqual({ID: 1, friendly_name: 'group_new_name', retain: false}); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/rename', @@ -3307,10 +3308,10 @@ describe('Extension: Bridge', () => { it('Should allow change group options', async () => { mockMQTT.publish.mockClear(); - expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, friendly_name: 'group_1', retain: false}); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {retain: true, transition: 1}, id: 'group_1'})); await flushPromises(); - expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: true, transition: 1}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, friendly_name: 'group_1', retain: true, transition: 1}); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/options', stringify({data: {from: {retain: false}, to: {retain: true, transition: 1}, restart_required: false, id: 'group_1'}, status: 'ok'}), @@ -3321,12 +3322,11 @@ describe('Extension: Bridge', () => { it('Should allow change group options with restart required', async () => { mockMQTT.publish.mockClear(); - expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, devices: [], friendly_name: 'group_1', retain: false}); + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, friendly_name: 'group_1', retain: false}); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/options', stringify({options: {off_state: 'all_members_off'}, id: 'group_1'})); await flushPromises(); expect(settings.getGroup('group_1')).toStrictEqual({ ID: 1, - devices: [], friendly_name: 'group_1', retain: false, off_state: 'all_members_off', @@ -3358,7 +3358,7 @@ describe('Extension: Bridge', () => { mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); await flushPromises(); - expect(settings.getGroup('group_193')).toStrictEqual({ID: 3, devices: [], friendly_name: 'group_193'}); + expect(settings.getGroup('group_193')).toStrictEqual({ID: 3, friendly_name: 'group_193'}); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', @@ -3372,7 +3372,7 @@ describe('Extension: Bridge', () => { mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: 'group_193', id: 92})); await flushPromises(); - expect(settings.getGroup('group_193')).toStrictEqual({ID: 92, devices: [], friendly_name: 'group_193'}); + expect(settings.getGroup('group_193')).toStrictEqual({ID: 92, friendly_name: 'group_193'}); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', diff --git a/test/extensions/groups.test.ts b/test/extensions/groups.test.ts index 559f059b7a..9c1f638983 100644 --- a/test/extensions/groups.test.ts +++ b/test/extensions/groups.test.ts @@ -2,7 +2,7 @@ import * as data from '../mocks/data'; import {mockLogger} from '../mocks/logger'; import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt'; import {flushPromises} from '../mocks/utils'; -import {devices, groups, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman'; +import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from '../mocks/zigbeeHerdsman'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -24,11 +24,6 @@ returnDevices.push( describe('Extension: Groups', () => { let controller: Controller; - const resetExtension = async (): Promise => { - await controller.enableDisableExtension(false, 'Groups'); - await controller.enableDisableExtension(true, 'Groups'); - }; - beforeAll(async () => { jest.useFakeTimers(); controller = new Controller(jest.fn(), jest.fn()); @@ -41,7 +36,7 @@ describe('Extension: Groups', () => { }); beforeEach(() => { - Object.values(groups).forEach((g) => (g.members = [])); + resetGroupMembers(); data.writeDefaultConfiguration(); settings.reRead(); mockMQTT.publish.mockClear(); @@ -51,82 +46,11 @@ describe('Extension: Groups', () => { controller.state.state = {}; }); - it('Apply group updates add', async () => { - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb', 'bulb_color']}}); - groups.group_1.members.push(devices.bulb.getEndpoint(1)!); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); - }); - - it('Apply group updates remove', async () => { - const endpoint = devices.bulb_color.getEndpoint(1)!; - const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([]); - }); - - it('Apply group updates remove handle fail', async () => { - const endpoint = devices.bulb_color.getEndpoint(1)!; - endpoint.removeFromGroup.mockImplementationOnce(() => { - throw new Error('failed!'); - }); - const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false}}); - mockLogger.error.mockClear(); - await resetExtension(); - expect(mockLogger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`); - expect(groups.group_1.members).toStrictEqual([endpoint]); - }); - - it('Move to non existing group', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; - const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {3: {friendly_name: 'group_3', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([]); - }); - - it('Add non standard endpoint to group with name', async () => { - const QBKG03LM = devices.QBKG03LM; - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['0x0017880104e45542/right']}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); - }); - - it('Add non standard endpoint to group with number', async () => { - const QBKG03LM = devices.QBKG03LM; - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['wall_switch_double/2']}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); - }); - - it('Shouldnt crash on non-existing devices', async () => { - mockLogger.error.mockClear(); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['not_existing_bla']}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([]); - expect(mockLogger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); - }); - - it('Should resolve device friendly names', async () => { - settings.set(['devices', devices.bulb.ieeeAddr, 'friendly_name'], 'bulb_friendly_name'); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['bulb_friendly_name', 'bulb_color']}}); - await resetExtension(); - expect(groups.group_1.members).toStrictEqual([devices.bulb.getEndpoint(1), devices.bulb_color.getEndpoint(1)]); - }); - it('Should publish group state change when a device in it changes state', async () => { const device = devices.bulb_color; const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; @@ -146,7 +70,6 @@ describe('Extension: Groups', () => { it('Should not republish identical optimistic group states', async () => { const device1 = devices.bulb_2; const device2 = devices.bulb_color_2; - await resetExtension(); mockMQTT.publish.mockClear(); await mockZHEvents.message({ @@ -205,8 +128,6 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); @@ -227,8 +148,6 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(endpoint); settings.set(['devices', device.ieeeAddr, 'disabled'], true); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); @@ -243,8 +162,6 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); @@ -290,7 +207,6 @@ describe('Extension: Groups', () => { it('Should publish state of device with endpoint name', async () => { const group = groups.gledopto_group; - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); @@ -314,7 +230,6 @@ describe('Extension: Groups', () => { it('Should publish state of group when specific state of specific endpoint is changed', async () => { const group = groups.gledopto_group; - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); @@ -340,8 +255,7 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness'], devices: [device.ieeeAddr]}}); - await resetExtension(); + settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness']}}); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); @@ -361,8 +275,7 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', devices: [device.ieeeAddr], optimistic: false, retain: false}}); - await resetExtension(); + settings.set(['groups'], {1: {friendly_name: 'group_1', optimistic: false, retain: false}}); mockMQTT.publish.mockClear(); const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; @@ -381,14 +294,8 @@ describe('Extension: Groups', () => { it('Should publish state change of another group with shared device when a group changes its state', async () => { const device = devices.bulb_color; const endpoint = device.getEndpoint(1)!; - const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], { - 1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}, - 2: {friendly_name: 'group_2', retain: false, devices: [device.ieeeAddr]}, - 3: {friendly_name: 'group_3', retain: false, devices: []}, - }); - await resetExtension(); + groups.group_1.members.push(endpoint); + groups.group_2.members.push(endpoint); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); @@ -412,10 +319,6 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(endpoint_1); group.members.push(endpoint_2); - settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, - }); - await resetExtension(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); @@ -441,9 +344,8 @@ describe('Extension: Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false, off_state: 'last_member_state'}, + 1: {friendly_name: 'group_1', retain: false, off_state: 'last_member_state'}, }); - await resetExtension(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); @@ -473,14 +375,9 @@ describe('Extension: Groups', () => { const device_2 = devices.bulb; const endpoint_1 = device_1.getEndpoint(1)!; const endpoint_2 = device_2.getEndpoint(1)!; - const group = groups.group_1; - group.members.push(endpoint_1); - group.members.push(endpoint_2); - settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, - 2: {friendly_name: 'group_2', retain: false, devices: [device_1.ieeeAddr]}, - }); - await resetExtension(); + groups.group_1.members.push(endpoint_1); + groups.group_1.members.push(endpoint_2); + groups.group_2.members.push(endpoint_1); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); @@ -512,9 +409,8 @@ describe('Extension: Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, + 1: {friendly_name: 'group_1', retain: false}, }); - await resetExtension(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); @@ -548,9 +444,8 @@ describe('Extension: Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, + 1: {friendly_name: 'group_1', retain: false}, }); - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200})); @@ -590,9 +485,8 @@ describe('Extension: Groups', () => { group.members.push(endpoint_1); group.members.push(endpoint_2); settings.set(['groups'], { - 1: {friendly_name: 'group_1', devices: [device_1.ieeeAddr, device_2.ieeeAddr], retain: false}, + 1: {friendly_name: 'group_1', retain: false}, }); - await resetExtension(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); await flushPromises(); @@ -622,9 +516,7 @@ describe('Extension: Groups', () => { const device = devices.bulb_color; const endpoint = device.getEndpoint(1); const group = groups.group_1; - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', @@ -632,7 +524,6 @@ describe('Extension: Groups', () => { ); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', @@ -646,9 +537,7 @@ describe('Extension: Groups', () => { const device = devices.bulb_color; const endpoint = device.getEndpoint(1)!; const group = groups.group_1; - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: []}}); expect(group.members.length).toBe(0); - await resetExtension(); endpoint.addToGroup.mockImplementationOnce(() => { throw new Error('timeout'); }); @@ -657,7 +546,6 @@ describe('Extension: Groups', () => { mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', @@ -671,14 +559,12 @@ describe('Extension: Groups', () => { const device = devices.bulb_color; const endpoint = device.getEndpoint(1); const group = groups['group/with/slashes']; - settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false, devices: []}}); + settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false}}); expect(group.members.length).toBe(0); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group/with/slashes').devices).toStrictEqual([`${device.ieeeAddr}/1`]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', @@ -693,12 +579,10 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(3)!; const group = groups.group_1; expect(group.members.length).toBe(0); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', @@ -713,14 +597,12 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(3)!; const group = groups.group_1; expect(group.members.length).toBe(0); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); await flushPromises(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); - expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', @@ -735,13 +617,10 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', @@ -756,8 +635,6 @@ describe('Extension: Groups', () => { const endpoint = device.getEndpoint(1)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [device.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/remove', @@ -765,28 +642,6 @@ describe('Extension: Groups', () => { ); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); - expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); - expect(mockMQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Remove from group via MQTT when in zigbee but not in settings', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; - const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: ['dummy']}}); - await resetExtension(); - mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); - await flushPromises(); - expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', @@ -797,17 +652,14 @@ describe('Extension: Groups', () => { }); it('Remove from group via MQTT with postfix variant 1', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/right`]}}); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', @@ -818,17 +670,14 @@ describe('Extension: Groups', () => { }); it('Remove from group via MQTT with postfix variant 2', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`0x0017880104e45542/right`]}}); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', @@ -839,17 +688,14 @@ describe('Extension: Groups', () => { }); it('Remove from group via MQTT with postfix variant 3', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; const group = groups.group_1; group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); - await resetExtension(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', @@ -860,17 +706,12 @@ describe('Extension: Groups', () => { }); it('Remove from group all', async () => { - const device = devices.bulb_color; - const endpoint = device.getEndpoint(1)!; const group = groups.group_1; - group.members.push(endpoint); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [`wall_switch_double/3`]}}); - await resetExtension(); + groups.group_1.members.push(devices.QBKG03LM.endpoints[2]); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); await flushPromises(); expect(group.members).toStrictEqual([]); - expect(settings.getGroup('group_1').devices).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove_all', @@ -881,7 +722,6 @@ describe('Extension: Groups', () => { }); it('Error when adding to non-existing group', async () => { - await resetExtension(); mockLogger.error.mockClear(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); @@ -900,7 +740,6 @@ describe('Extension: Groups', () => { }); it('Error when adding a non-existing device', async () => { - await resetExtension(); mockLogger.error.mockClear(); mockMQTT.publish.mockClear(); mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'})); @@ -919,7 +758,6 @@ describe('Extension: Groups', () => { }); it('Error when adding a non-existing endpoint', async () => { - await resetExtension(); mockLogger.error.mockClear(); mockMQTT.publish.mockClear(); mockMQTTEvents.message( @@ -946,8 +784,6 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(bulbColor.getEndpoint(1)!); group.members.push(bulbColorTemp.getEndpoint(1)!); - settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, devices: [bulbColor.ieeeAddr, bulbColorTemp.ieeeAddr]}}); - await resetExtension(); mockMQTT.publish.mockClear(); await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); diff --git a/test/mocks/data.ts b/test/mocks/data.ts index 2fc10cc4db..fee037b83f 100644 --- a/test/mocks/data.ts +++ b/test/mocks/data.ts @@ -214,30 +214,24 @@ export function writeDefaultConfiguration(): void { 15071: { friendly_name: 'group_tradfri_remote', retain: false, - devices: ['bulb_color_2', 'bulb_2'], }, 11: { friendly_name: 'group_with_tradfri', retain: false, - devices: ['bulb_2'], }, 12: { friendly_name: 'thermostat_group', retain: false, - devices: ['TS0601_thermostat'], }, 14: { friendly_name: 'switch_group', retain: false, - devices: ['power_plug', 'bulb_2'], }, 21: { friendly_name: 'gledopto_group', - devices: ['GLEDOPTO_2ID/cct'], }, 9: { friendly_name: 'ha_discovery_group', - devices: ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], }, }, external_converters: [], diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 377dcf920f..19e60b0f10 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -451,6 +451,7 @@ const zigfred_plus = new Device( export const groups = { group_1: new Group(1, []), + group_2: new Group(2, []), group_tradfri_remote: new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]), 'group/with/slashes': new Group(99, []), group_with_tradfri: new Group(11, [bulb_2.endpoints[0]]), @@ -458,9 +459,17 @@ export const groups = { group_with_switch: new Group(14, [ZNCZ02LM.endpoints[0], bulb_2.endpoints[0]]), gledopto_group: new Group(21, [GLEDOPTO_2ID.endpoints[3]]), default_bind_group: new Group(901, []), - ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[1]]), + ha_discovery_group: new Group(9, [bulb_color_2.endpoints[0], bulb_2.endpoints[0], QBKG03LM.endpoints[2]]), }; +const groupMembersBackup = Object.fromEntries(Object.entries(groups).map((v) => [v[0], [...v[1].members]])); + +export function resetGroupMembers(): void { + for (const key in groupMembersBackup) { + groups[key].members = [...groupMembersBackup[key]]; + } +} + export const devices = { coordinator: new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [], '0x00124b00120144ae')], false), bulb: new Device( diff --git a/test/settings.test.ts b/test/settings.test.ts index 016329e57f..4c54734ae0 100644 --- a/test/settings.test.ts +++ b/test/settings.test.ts @@ -1,6 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import * as data from './mocks/data'; - import fs from 'fs'; import yaml from 'js-yaml'; @@ -445,12 +442,25 @@ describe('Settings', () => { const expected = { ID: 1, friendly_name: '123', - devices: [], }; expect(group).toStrictEqual(expected); }); + it('Throw if removing non-existing group', () => { + const content = { + groups: { + 1: { + friendly_name: '123', + }, + }, + }; + + write(configurationFile, content); + + expect(() => settings.removeGroup('2')).toThrow(`Group '2' does not exist`); + }); + it('Should read groups from a separate file', () => { const contentConfiguration = { groups: 'groups.yaml', @@ -469,7 +479,6 @@ describe('Settings', () => { const expected = { ID: 1, friendly_name: '123', - devices: [], }; expect(group).toStrictEqual(expected); @@ -602,95 +611,6 @@ describe('Settings', () => { expect(settings.get().groups).toStrictEqual(expected); }); - it('Should add devices to groups', () => { - write(configurationFile, { - devices: { - '0x123': { - friendly_name: 'bulb', - retain: true, - }, - }, - }); - - settings.addGroup('test123'); - settings.addDeviceToGroup('test123', ['0x123']); - const expected = { - 1: { - friendly_name: 'test123', - devices: ['0x123'], - }, - }; - - expect(settings.get().groups).toStrictEqual(expected); - }); - - it('Should remove devices from groups', () => { - write(configurationFile, { - devices: { - '0x123': { - friendly_name: 'bulb', - retain: true, - }, - }, - groups: { - 1: { - friendly_name: 'test123', - devices: ['0x123'], - }, - }, - }); - - settings.removeDeviceFromGroup('test123', ['0x123']); - const expected = { - 1: { - friendly_name: 'test123', - devices: [], - }, - }; - - expect(settings.get().groups).toStrictEqual(expected); - }); - - it('Shouldnt crash when removing device from group when group has no devices', () => { - write(configurationFile, { - devices: { - '0x123': { - friendly_name: 'bulb', - retain: true, - }, - }, - groups: { - 1: { - friendly_name: 'test123', - }, - }, - }); - - settings.removeDeviceFromGroup('test123', ['0x123']); - const expected = { - 1: { - friendly_name: 'test123', - }, - }; - - expect(settings.get().groups).toStrictEqual(expected); - }); - - it('Should throw when adding device to non-existing group', () => { - write(configurationFile, { - devices: { - '0x123': { - friendly_name: 'bulb', - retain: true, - }, - }, - }); - - expect(() => { - settings.removeDeviceFromGroup('test123', ['bulb']); - }).toThrow(new Error("Group 'test123' does not exist")); - }); - it('Should throw when adding device which already exists', () => { write(configurationFile, { devices: { From 9537ef05d563de815b24e45176a1bec68eb811b8 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:58:24 +0200 Subject: [PATCH 09/70] fix!: Improve bind/bridge/groups parsing and resolving consistency (#24432) * Improve bind/bridge/groups parsing and resolving consistency * Fix mqtt payload casing. Improve typing. * Cleanup tests * Add test for bind endpoint by ID. * Match behavior for group response 'default' endpoint. --- lib/extension/bind.ts | 269 +++++++++++++++++--------- lib/extension/bridge.ts | 23 +-- lib/extension/groups.ts | 181 +++++++++-------- test/extensions/bind.test.ts | 159 +++++++++++---- test/extensions/bridge.test.ts | 63 +++++- test/extensions/groups.test.ts | 102 ++++++---- test/extensions/homeassistant.test.ts | 2 +- 7 files changed, 530 insertions(+), 269 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index c917f5270c..6e324f13d8 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -195,15 +195,23 @@ const POLL_ON_MESSAGE: Readonly = [ interface ParsedMQTTMessage { type: 'bind' | 'unbind'; - sourceKey: string; - targetKey: string; + sourceKey?: string; + sourceEndpointKey?: string | number; + targetKey?: string; + targetEndpointKey?: string | number; clusters?: string[]; skipDisableReporting: boolean; + resolvedSource?: Device; + resolvedTarget?: Device | Group | typeof DEFAULT_BIND_GROUP; + resolvedSourceEndpoint?: zh.Endpoint; + resolvedBindTarget?: number | zh.Endpoint | zh.Group; } interface DataMessage { from: ParsedMQTTMessage['sourceKey']; + from_endpoint?: ParsedMQTTMessage['sourceEndpointKey']; to: ParsedMQTTMessage['targetKey']; + to_endpoint: ParsedMQTTMessage['targetEndpointKey']; clusters: ParsedMQTTMessage['clusters']; skip_disable_reporting?: ParsedMQTTMessage['skipDisableReporting']; } @@ -217,128 +225,199 @@ export default class Bind extends Extension { this.eventBus.onGroupMembersChanged(this, this.onGroupMembersChanged); } - private parseMQTTMessage(data: eventdata.MQTTMessage): ParsedMQTTMessage | undefined { - let type: ParsedMQTTMessage['type'] | undefined; - let sourceKey: ParsedMQTTMessage['sourceKey'] | undefined; - let targetKey: ParsedMQTTMessage['targetKey'] | undefined; - let clusters: ParsedMQTTMessage['clusters'] | undefined; - let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false; - + private parseMQTTMessage( + data: eventdata.MQTTMessage, + ): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] { if (data.topic.match(TOPIC_REGEX)) { - type = data.topic.endsWith('unbind') ? 'unbind' : 'bind'; + const type = data.topic.endsWith('unbind') ? 'unbind' : 'bind'; + let skipDisableReporting = false; const message: DataMessage = JSON.parse(data.message); - sourceKey = message.from; - targetKey = message.to; - clusters = message.clusters; + + if (typeof message !== 'object' || message.from == undefined || message.to == undefined) { + return [message, {type, skipDisableReporting}, `Invalid payload`]; + } + + const sourceKey = message.from; + const sourceEndpointKey = message.from_endpoint ?? 'default'; + const targetKey = message.to; + const targetEndpointKey = message.to_endpoint; + const clusters = message.clusters; skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false; - } else { - return undefined; - } + const resolvedSource = this.zigbee.resolveEntity(message.from) as Device; - return {type, sourceKey, targetKey, clusters, skipDisableReporting}; - } + if (!resolvedSource || !(resolvedSource instanceof Device)) { + return [message, {type, skipDisableReporting}, `Source device '${message.from}' does not exist`]; + } - @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const parsed = this.parseMQTTMessage(data); + const resolvedTarget = message.to === DEFAULT_BIND_GROUP.name ? DEFAULT_BIND_GROUP : this.zigbee.resolveEntity(message.to); - if (!parsed || !parsed.type) { - return; - } + if (!resolvedTarget) { + return [message, {type, skipDisableReporting}, `Target device or group '${message.to}' does not exist`]; + } - const {type, sourceKey, targetKey, clusters, skipDisableReporting} = parsed; - const message = utils.parseJSON(data.message, data.message); - - let error: string | undefined; - const parsedSource = this.zigbee.resolveEntityAndEndpoint(sourceKey); - const parsedTarget = this.zigbee.resolveEntityAndEndpoint(targetKey); - const source = parsedSource.entity; - const target = targetKey === DEFAULT_BIND_GROUP.name ? DEFAULT_BIND_GROUP : parsedTarget.entity; - const responseData: KeyValue = {from: sourceKey, to: targetKey}; - - if (!source || !(source instanceof Device)) { - error = `Source device '${sourceKey}' does not exist`; - } else if (parsedSource.endpointID && !parsedSource.endpoint) { - error = `Source device '${parsedSource.ID}' does not have endpoint '${parsedSource.endpointID}'`; - } else if (!target) { - error = `Target device or group '${targetKey}' does not exist`; - } else if (target instanceof Device && parsedTarget.endpointID && !parsedTarget.endpoint) { - error = `Target device '${parsedTarget.ID}' does not have endpoint '${parsedTarget.endpointID}'`; - } else { - const successfulClusters: string[] = []; - const failedClusters = []; - const attemptedClusters = []; + const resolvedSourceEndpoint = resolvedSource.endpoint(sourceEndpointKey); - const bindSource = parsedSource.endpoint; - const bindTarget = target instanceof Device ? parsedTarget.endpoint : target instanceof Group ? target.zh : Number(target.ID); + if (!resolvedSourceEndpoint) { + return [ + message, + {type, skipDisableReporting}, + `Source device '${resolvedSource.name}' does not have endpoint '${sourceEndpointKey}'`, + ]; + } - assert(bindSource != undefined && bindTarget != undefined); + // resolves to 'default' endpoint if targetEndpointKey is invalid (used by frontend for 'Coordinator') + const resolvedBindTarget = + resolvedTarget instanceof Device + ? resolvedTarget.endpoint(targetEndpointKey) + : resolvedTarget instanceof Group + ? resolvedTarget.zh + : Number(resolvedTarget.ID); + + if (resolvedTarget instanceof Device && !resolvedBindTarget) { + return [ + message, + {type, skipDisableReporting}, + `Target device '${resolvedTarget.name}' does not have endpoint '${targetEndpointKey}'`, + ]; + } - // Find which clusters are supported by both the source and target. - // Groups are assumed to support all clusters. - const clusterCandidates = clusters ?? ALL_CLUSTER_CANDIDATES; + return [ + message, + { + type, + sourceKey, + sourceEndpointKey, + targetKey, + targetEndpointKey, + clusters, + skipDisableReporting, + resolvedSource, + resolvedTarget, + resolvedSourceEndpoint, + resolvedBindTarget, + }, + undefined, + ]; + } else { + return [undefined, undefined, undefined]; + } + } - for (const cluster of clusterCandidates) { - let matchingClusters = false; + @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { + const [raw, parsed, error] = this.parseMQTTMessage(data); - const anyClusterValid = utils.isZHGroup(bindTarget) || typeof bindTarget === 'number' || (target as Device).zh.type === 'Coordinator'; + if (!raw || !parsed) { + return; + } - if (!anyClusterValid && utils.isZHEndpoint(bindTarget)) { - matchingClusters = - (bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) || - (bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster)); - } + if (error) { + await this.publishResponse(parsed.type, raw, {}, error); + return; + } - const sourceValid = bindSource.supportsInputCluster(cluster) || bindSource.supportsOutputCluster(cluster); + const { + type, + sourceKey, + sourceEndpointKey, + targetKey, + targetEndpointKey, + clusters, + skipDisableReporting, + resolvedSource, + resolvedTarget, + resolvedSourceEndpoint, + resolvedBindTarget, + } = parsed; + + assert(resolvedSource, '`resolvedSource` is missing'); + assert(resolvedTarget, '`resolvedTarget` is missing'); + assert(resolvedSourceEndpoint, '`resolvedSourceEndpoint` is missing'); + assert(resolvedBindTarget != undefined, '`resolvedBindTarget` is missing'); + + const successfulClusters: string[] = []; + const failedClusters = []; + const attemptedClusters = []; + // Find which clusters are supported by both the source and target. + // Groups are assumed to support all clusters. + const clusterCandidates = clusters ?? ALL_CLUSTER_CANDIDATES; + + for (const cluster of clusterCandidates) { + let matchingClusters = false; + + const anyClusterValid = + utils.isZHGroup(resolvedBindTarget) || + typeof resolvedBindTarget === 'number' || + (resolvedTarget instanceof Device && resolvedTarget.zh.type === 'Coordinator'); + + if (!anyClusterValid && utils.isZHEndpoint(resolvedBindTarget)) { + matchingClusters = + (resolvedBindTarget.supportsInputCluster(cluster) && resolvedSourceEndpoint.supportsOutputCluster(cluster)) || + (resolvedSourceEndpoint.supportsInputCluster(cluster) && resolvedBindTarget.supportsOutputCluster(cluster)); + } - if (sourceValid && (anyClusterValid || matchingClusters)) { - logger.debug(`${type}ing cluster '${cluster}' from '${source.name}' to '${target.name}'`); - attemptedClusters.push(cluster); + const sourceValid = resolvedSourceEndpoint.supportsInputCluster(cluster) || resolvedSourceEndpoint.supportsOutputCluster(cluster); - try { - if (type === 'bind') { - await bindSource.bind(cluster, bindTarget); - } else { - await bindSource.unbind(cluster, bindTarget); - } + if (sourceValid && (anyClusterValid || matchingClusters)) { + logger.debug(`${type}ing cluster '${cluster}' from '${resolvedSource.name}' to '${resolvedTarget.name}'`); + attemptedClusters.push(cluster); - successfulClusters.push(cluster); - logger.info( - `Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from '${source.name}' to '${target.name}'`, - ); - } catch (error) { - failedClusters.push(cluster); - logger.error(`Failed to ${type} cluster '${cluster}' from '${source.name}' to '${target.name}' (${error})`); + try { + if (type === 'bind') { + await resolvedSourceEndpoint.bind(cluster, resolvedBindTarget); + } else { + await resolvedSourceEndpoint.unbind(cluster, resolvedBindTarget); } - } - } - if (attemptedClusters.length === 0) { - logger.error(`Nothing to ${type} from '${source.name}' to '${target.name}'`); - error = `Nothing to ${type}`; - } else if (failedClusters.length === attemptedClusters.length) { - error = `Failed to ${type}`; + successfulClusters.push(cluster); + logger.info( + `Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from '${resolvedSource.name}' to '${resolvedTarget.name}'`, + ); + } catch (error) { + failedClusters.push(cluster); + logger.error(`Failed to ${type} cluster '${cluster}' from '${resolvedSource.name}' to '${resolvedTarget.name}' (${error})`); + } } + } - responseData[`clusters`] = successfulClusters; - responseData[`failed`] = failedClusters; + if (attemptedClusters.length === 0) { + logger.error(`Nothing to ${type} from '${resolvedSource.name}' to '${resolvedTarget.name}'`); + await this.publishResponse(parsed.type, raw, {}, `Nothing to ${type}`); + return; + } else if (failedClusters.length === attemptedClusters.length) { + await this.publishResponse(parsed.type, raw, {}, `Failed to ${type}`); + return; + } - if (successfulClusters.length !== 0) { - if (type === 'bind') { - await this.setupReporting(bindSource.binds.filter((b) => successfulClusters.includes(b.cluster.name) && b.target === bindTarget)); - } else if (typeof bindTarget !== 'number' && !skipDisableReporting) { - await this.disableUnnecessaryReportings(bindTarget); - } + const responseData: KeyValue = { + from: sourceKey, + from_endpoint: sourceEndpointKey, + to: targetKey, + to_endpoint: targetEndpointKey, + clusters: successfulClusters, + failed: failedClusters, + }; + + /* istanbul ignore else */ + if (successfulClusters.length !== 0) { + if (type === 'bind') { + await this.setupReporting( + resolvedSourceEndpoint.binds.filter((b) => successfulClusters.includes(b.cluster.name) && b.target === resolvedBindTarget), + ); + } else if (typeof resolvedBindTarget !== 'number' && !skipDisableReporting) { + await this.disableUnnecessaryReportings(resolvedBindTarget); } } - const response = utils.getResponse(message, responseData, error); + await this.publishResponse(parsed.type, raw, responseData); + this.eventBus.emitDevicesChanged(); + } - await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response)); + private async publishResponse(type: ParsedMQTTMessage['type'], request: KeyValue, data: KeyValue, error?: string): Promise { + const response = stringify(utils.getResponse(request, data, error)); + await this.mqtt.publish(`bridge/response/device/${type}`, response); if (error) { logger.error(error); - } else { - this.eventBus.emitDevicesChanged(); } } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 69ff55963f..b652c5d4d3 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -431,6 +431,7 @@ export default class Bridge extends Extension { if ( typeof message !== 'object' || message.id === undefined || + message.endpoint === undefined || message.cluster === undefined || message.maximum_report_interval === undefined || message.minimum_report_interval === undefined || @@ -440,14 +441,11 @@ export default class Bridge extends Extension { throw new Error(`Invalid payload`); } - const device = this.zigbee.resolveEntityAndEndpoint(message.id); - if (!device.entity) { - throw new Error(`Device '${message.id}' does not exist`); - } + const device = this.getEntity('device', message.id); + const endpoint = device.endpoint(message.endpoint); - const endpoint = device.endpoint; if (!endpoint) { - throw new Error(`Device '${device.ID}' does not have endpoint '${device.endpointID}'`); + throw new Error(`Device '${device.ID}' does not have endpoint '${message.endpoint}'`); } const coordinatorEndpoint = this.zigbee.firstCoordinatorEndpoint(); @@ -472,6 +470,7 @@ export default class Bridge extends Extension { return utils.getResponse(message, { id: message.id, + endpoint: message.endpoint, cluster: message.cluster, maximum_report_interval: message.maximum_report_interval, minimum_report_interval: message.minimum_report_interval, @@ -485,7 +484,7 @@ export default class Bridge extends Extension { throw new Error(`Invalid payload`); } - const device = this.getEntity('device', message.id) as Device; + const device = this.getEntity('device', message.id); logger.info(`Interviewing '${device.name}'`); try { @@ -508,12 +507,7 @@ export default class Bridge extends Extension { throw new Error(`Invalid payload`); } - const device = this.zigbee.resolveEntityAndEndpoint(message.id).entity as Device; - - if (!device) { - throw new Error(`Device '${message.id}' does not exist`); - } - + const device = this.getEntity('device', message.id); const source = await zhc.generateExternalDefinitionSource(device.zh); return utils.getResponse(message, {id: message.id, source}); @@ -634,6 +628,9 @@ export default class Bridge extends Extension { } } + getEntity(type: 'group', ID: string): Group; + getEntity(type: 'device', ID: string): Device; + getEntity(type: 'group' | 'device', ID: string): Device | Group; getEntity(type: 'group' | 'device', ID: string): Device | Group { const entity = this.zigbee.resolveEntity(ID); if (!entity || entity.constructor.name.toLowerCase() !== type) { diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index eb5b0e6ed7..939160cdab 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -30,13 +30,20 @@ const STATE_PROPERTIES: Readonly { - let type: ParsedMQTTMessage['type'] | undefined; - let resolvedEntityGroup: ParsedMQTTMessage['resolvedEntityGroup'] | undefined; - let resolvedEntityDevice: ParsedMQTTMessage['resolvedEntityDevice'] | undefined; - let resolvedEntityEndpoint: ParsedMQTTMessage['resolvedEntityEndpoint'] | undefined; - let error: ParsedMQTTMessage['error'] | undefined; - let groupKey: ParsedMQTTMessage['groupKey'] | undefined; - let deviceKey: ParsedMQTTMessage['deviceKey'] | undefined; - let skipDisableReporting: ParsedMQTTMessage['skipDisableReporting'] = false; - - /* istanbul ignore else */ + private parseMQTTMessage( + data: eventdata.MQTTMessage, + ): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] { const topicRegexMatch = data.topic.match(TOPIC_REGEX); if (topicRegexMatch) { - type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all'; - const message = JSON.parse(data.message); - deviceKey = message.device; - skipDisableReporting = 'skip_disable_reporting' in message ? message.skip_disable_reporting : false; + const type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all'; + let resolvedGroup; + let groupKey; + let skipDisableReporting = false; + const message: DataMessage = JSON.parse(data.message); + + if (typeof message !== 'object' || message.device == undefined) { + return [message, {type, skipDisableReporting}, 'Invalid payload']; + } + + const deviceKey = message.device; + skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false; if (type !== 'remove_all') { groupKey = message.group; - resolvedEntityGroup = this.zigbee.resolveEntity(message.group) as Group; - if (!resolvedEntityGroup || !(resolvedEntityGroup instanceof Group)) { - error = `Group '${message.group}' does not exist`; + if (message.group == undefined) { + return [message, {type, skipDisableReporting}, `Invalid payload`]; + } + + resolvedGroup = this.zigbee.resolveEntity(message.group); + + if (!resolvedGroup || !(resolvedGroup instanceof Group)) { + return [message, {type, skipDisableReporting}, `Group '${message.group}' does not exist`]; } } - const parsed = this.zigbee.resolveEntityAndEndpoint(message.device); - resolvedEntityDevice = parsed?.entity as Device; + const resolvedDevice = this.zigbee.resolveEntity(message.device); - if (!error && (!resolvedEntityDevice || !(resolvedEntityDevice instanceof Device))) { - error = `Device '${message.device}' does not exist`; + if (!resolvedDevice || !(resolvedDevice instanceof Device)) { + return [message, {type, skipDisableReporting}, `Device '${message.device}' does not exist`]; } - if (!error) { - resolvedEntityEndpoint = parsed.endpoint; + const endpointKey = message.endpoint ?? 'default'; + const resolvedEndpoint = resolvedDevice.endpoint(message.endpoint); - if (parsed.endpointID && !resolvedEntityEndpoint) { - error = `Device '${parsed.ID}' does not have endpoint '${parsed.endpointID}'`; - } + if (!resolvedEndpoint) { + return [message, {type, skipDisableReporting}, `Device '${resolvedDevice.name}' does not have endpoint '${endpointKey}'`]; } + + return [ + message, + { + resolvedGroup, + resolvedDevice, + resolvedEndpoint, + type, + groupKey, + deviceKey, + endpointKey, + skipDisableReporting, + }, + undefined, + ]; } else { - return undefined; + return [undefined, undefined, undefined]; } - - return { - resolvedEntityGroup, - resolvedEntityDevice, - type, - error, - groupKey, - deviceKey, - skipDisableReporting, - resolvedEntityEndpoint, - }; } @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const parsed = await this.parseMQTTMessage(data); + const [raw, parsed, error] = this.parseMQTTMessage(data); + + if (!raw || !parsed) { + return; + } - if (!parsed || !parsed.type) { + if (error) { + await this.publishResponse(parsed.type, raw, {}, error); return; } - const {resolvedEntityGroup, resolvedEntityDevice, type, groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint} = parsed; - let error = parsed.error; + const {resolvedGroup, resolvedDevice, resolvedEndpoint, type, groupKey, deviceKey, endpointKey, skipDisableReporting} = parsed; const changedGroups: Group[] = []; - if (!error) { - assert(resolvedEntityEndpoint, '`resolvedEntityEndpoint` is missing'); - try { - if (type === 'add') { - assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing'); - logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`); - await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh); - changedGroups.push(resolvedEntityGroup); - } else if (type === 'remove') { - assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing'); - logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`); - await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh); - changedGroups.push(resolvedEntityGroup); - } else { - // remove_all - logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`); - - for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) { - changedGroups.push(group); - } + assert(resolvedDevice, '`resolvedDevice` is missing'); + assert(resolvedEndpoint, '`resolvedEndpoint` is missing'); + + try { + if (type === 'add') { + assert(resolvedGroup, '`resolvedGroup` is missing'); + logger.info(`Adding '${resolvedDevice.name}' to '${resolvedGroup.name}'`); + await resolvedEndpoint.addToGroup(resolvedGroup.zh); + changedGroups.push(resolvedGroup); + } else if (type === 'remove') { + assert(resolvedGroup, '`resolvedGroup` is missing'); + logger.info(`Removing '${resolvedDevice.name}' from '${resolvedGroup.name}'`); + await resolvedEndpoint.removeFromGroup(resolvedGroup.zh); + changedGroups.push(resolvedGroup); + } else { + // remove_all + logger.info(`Removing '${resolvedDevice.name}' from all groups`); - await resolvedEntityEndpoint.removeFromAllGroups(); + for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEndpoint))) { + changedGroups.push(group); } - } catch (e) { - error = `Failed to ${type} from group (${(e as Error).message})`; - logger.debug((e as Error).stack!); + + await resolvedEndpoint.removeFromAllGroups(); } + } catch (e) { + const errorMsg = `Failed to ${type} from group (${(e as Error).message})`; + await this.publishResponse(parsed.type, raw, {}, errorMsg); + logger.debug((e as Error).stack!); + return; } - const message = utils.parseJSON(data.message, data.message); - const responseData: KeyValue = {device: deviceKey}; + const responseData: KeyValue = {device: deviceKey, endpoint: endpointKey}; if (groupKey) { responseData.group = groupKey; } - await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(utils.getResponse(message, responseData, error))); + await this.publishResponse(parsed.type, raw, responseData); + + for (const group of changedGroups) { + this.eventBus.emitGroupMembersChanged({group, action: type, endpoint: resolvedEndpoint, skipDisableReporting}); + } + } + + private async publishResponse(type: ParsedMQTTMessage['type'], request: KeyValue, data: KeyValue, error?: string): Promise { + const response = stringify(utils.getResponse(request, data, error)); + await this.mqtt.publish(`bridge/response/group/members/${type}`, response); if (error) { logger.error(error); - } else { - assert(resolvedEntityEndpoint, '`resolvedEntityEndpoint` is missing'); - for (const group of changedGroups) { - this.eventBus.emitGroupMembersChanged({group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting}); - } } } } diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index b29bb33096..e8ec68c6b1 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -96,7 +96,13 @@ describe('Extension: Bind', () => { 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, status: 'ok', }), {retain: false, qos: 0}, @@ -110,6 +116,18 @@ describe('Extension: Bind', () => { device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; }); + it('Should throw error on invalid payload', async () => { + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({fromz: 'remote', to: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + it('Filters out unsupported clusters for reporting setup', async () => { const device = devices.remote; const target = devices.bulb_color.getEndpoint(1)!; @@ -156,7 +174,13 @@ describe('Extension: Bind', () => { 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, status: 'ok', }), {retain: false, qos: 0}, @@ -221,7 +245,13 @@ describe('Extension: Bind', () => { 'zigbee2mqtt/bridge/response/device/bind', stringify({ transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, status: 'ok', }), {retain: false, qos: 0}, @@ -247,7 +277,7 @@ describe('Extension: Bind', () => { expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + stringify({data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -263,7 +293,7 @@ describe('Extension: Bind', () => { expect(endpoint.bind).toHaveBeenCalledTimes(0); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: 'button', clusters: [], failed: []}, status: 'error', error: 'Nothing to bind'}), + stringify({data: {}, status: 'error', error: 'Nothing to bind'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -313,7 +343,10 @@ describe('Extension: Bind', () => { expect(devices.bulb_color.meta.configured).toBe(332242049); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -337,7 +370,10 @@ describe('Extension: Bind', () => { expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -366,7 +402,10 @@ describe('Extension: Bind', () => { ]); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -400,7 +439,10 @@ describe('Extension: Bind', () => { expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -475,7 +517,10 @@ describe('Extension: Bind', () => { expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -492,30 +537,55 @@ describe('Extension: Bind', () => { mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: 'Failed to bind'}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Should bind from non default endpoint names', async () => { + const device = devices.remote; + const target = devices.QBKG03LM.getEndpoint(3)!; + const endpoint = device.getEndpoint(2)!; + mockClear(device); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/device/bind', + stringify({from: 'remote', from_endpoint: 'ep2', to: 'wall_switch_double', to_endpoint: 'right'}), + ); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', stringify({ - data: {from: 'remote', to: 'bulb_color', clusters: [], failed: ['genScenes', 'genOnOff', 'genLevelCtrl']}, - status: 'error', - error: 'Failed to bind', + data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch_double', to_endpoint: 'right', clusters: ['genOnOff'], failed: []}, + status: 'ok', }), {retain: false, qos: 0}, expect.any(Function), ); }); - it('Should bind from non default endpoints', async () => { + it('Should bind from non default endpoint IDs', async () => { const device = devices.remote; const target = devices.QBKG03LM.getEndpoint(3)!; const endpoint = device.getEndpoint(2)!; mockClear(device); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch_double/right'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/device/bind', + stringify({from: 'remote', from_endpoint: 2, to: 'wall_switch_double', to_endpoint: 3}), + ); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote/ep2', to: 'wall_switch_double/right', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + stringify({ + data: {from: 'remote', from_endpoint: 2, to: 'wall_switch_double', to_endpoint: 3, clusters: ['genOnOff'], failed: []}, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -532,7 +602,16 @@ describe('Extension: Bind', () => { expect(endpoint.bind).toHaveBeenCalledWith('msTemperatureMeasurement', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}), + stringify({ + data: { + from: 'temperature_sensor', + from_endpoint: 'default', + to: 'heating_actuator', + clusters: ['msTemperatureMeasurement'], + failed: [], + }, + status: 'ok', + }), {retain: false, qos: 0}, expect.any(Function), ); @@ -543,13 +622,13 @@ describe('Extension: Bind', () => { const target = devices.QBKG04LM.getEndpoint(2)!; const endpoint = device.getEndpoint(2)!; mockClear(device); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/ep2', to: 'wall_switch'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', from_endpoint: 'ep2', to: 'wall_switch'})); await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + stringify({data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -569,7 +648,13 @@ describe('Extension: Bind', () => { expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/unbind', stringify({ - data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + data: { + from: 'remote', + from_endpoint: 'default', + to: 'default_bind_group', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], + failed: [], + }, status: 'ok', }), {retain: false, qos: 0}, @@ -584,11 +669,7 @@ describe('Extension: Bind', () => { await flushPromises(); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - data: {from: 'remote_not_existing', to: 'bulb_color'}, - status: 'error', - error: "Source device 'remote_not_existing' does not exist", - }), + stringify({data: {}, status: 'error', error: "Source device 'remote_not_existing' does not exist"}), {retain: false, qos: 0}, expect.any(Function), ); @@ -597,15 +678,14 @@ describe('Extension: Bind', () => { it("Error bind fails when source device's endpoint does not exist", async () => { const device = devices.remote; mockClear(device); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/device/bind', + stringify({from: 'remote', from_endpoint: 'not_existing_endpoint', to: 'bulb_color'}), + ); await flushPromises(); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - data: {from: 'remote/not_existing_endpoint', to: 'bulb_color'}, - status: 'error', - error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'", - }), + stringify({data: {}, status: 'error', error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'"}), {retain: false, qos: 0}, expect.any(Function), ); @@ -618,11 +698,7 @@ describe('Extension: Bind', () => { await flushPromises(); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - data: {from: 'remote', to: 'bulb_color_not_existing'}, - status: 'error', - error: "Target device or group 'bulb_color_not_existing' does not exist", - }), + stringify({data: {}, status: 'error', error: "Target device or group 'bulb_color_not_existing' does not exist"}), {retain: false, qos: 0}, expect.any(Function), ); @@ -631,15 +707,14 @@ describe('Extension: Bind', () => { it("Error bind fails when target device's endpoint does not exist", async () => { const device = devices.remote; mockClear(device); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/device/bind', + stringify({from: 'remote', to: 'bulb_color', to_endpoint: 'not_existing_endpoint'}), + ); await flushPromises(); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - data: {from: 'remote', to: 'bulb_color/not_existing_endpoint'}, - status: 'error', - error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'", - }), + stringify({data: {}, status: 'error', error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}), {retain: false, qos: 0}, expect.any(Function), ); diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 79fb2e69d6..094cea86bd 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -3551,15 +3551,17 @@ describe('Extension: Bridge', () => { ); }); - it('Should allow to configure reporting', async () => { + it('Should allow to configure reporting with endpoint as number', async () => { const device = devices.bulb; const endpoint = device.getEndpoint(1)!; + endpoint.bind.mockClear(); endpoint.configureReporting.mockClear(); mockMQTT.publish.mockClear(); mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ - id: '0x000b57fffec6a5b2/1', + id: '0x000b57fffec6a5b2', + endpoint: 1, cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, @@ -3580,7 +3582,55 @@ describe('Extension: Bridge', () => { 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({ data: { - id: '0x000b57fffec6a5b2/1', + id: '0x000b57fffec6a5b2', + endpoint: 1, + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }, + status: 'ok', + }), + {retain: false, qos: 0}, + expect.any(Function), + ); + expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + }); + + it('Should allow to configure reporting with endpoint as string', async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.bind.mockClear(); + endpoint.configureReporting.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/device/configure_reporting', + stringify({ + id: '0x000b57fffec6a5b2', + endpoint: '1', + cluster: 'genLevelCtrl', + attribute: 'currentLevel', + maximum_report_interval: 10, + minimum_report_interval: 1, + reportable_change: 1, + }), + ); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', devices.coordinator.endpoints[0]); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(1); + expect(endpoint.configureReporting).toHaveBeenCalledWith( + 'genLevelCtrl', + [{attribute: 'currentLevel', maximumReportInterval: 10, minimumReportInterval: 1, reportableChange: 1}], + undefined, + ); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure_reporting', + stringify({ + data: { + id: '0x000b57fffec6a5b2', + endpoint: '1', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, @@ -3604,8 +3654,9 @@ describe('Extension: Bridge', () => { 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'bulb', + // endpoint: '1', cluster: 'genLevelCtrl', - attribute_lala: 'currentLevel', + attribute: 'currentLevel', maximum_report_interval: 10, minimum_report_interval: 1, reportable_change: 1, @@ -3630,6 +3681,7 @@ describe('Extension: Bridge', () => { 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'non_existing_device', + endpoint: '1', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, @@ -3655,7 +3707,8 @@ describe('Extension: Bridge', () => { mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ - id: '0x000b57fffec6a5b2/non_existing_endpoint', + id: '0x000b57fffec6a5b2', + endpoint: 'non_existing_endpoint', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, diff --git a/test/extensions/groups.test.ts b/test/extensions/groups.test.ts index 9c1f638983..2dffc71b6f 100644 --- a/test/extensions/groups.test.ts +++ b/test/extensions/groups.test.ts @@ -527,7 +527,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({data: {device: 'bulb_color', group: 'group_1'}, transaction: '123', status: 'ok'}), + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, transaction: '123', status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -549,7 +549,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'error', error: 'Failed to add from group (timeout)'}), + stringify({data: {}, status: 'error', error: 'Failed to add from group (timeout)'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -568,7 +568,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({data: {device: 'bulb_color', group: 'group/with/slashes'}, status: 'ok'}), + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group/with/slashes'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -580,13 +580,16 @@ describe('Extension: Groups', () => { const group = groups.group_1; expect(group.members.length).toBe(0); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'group_1', device: 'wall_switch_double', endpoint: 'right'}), + ); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -598,15 +601,21 @@ describe('Extension: Groups', () => { const group = groups.group_1; expect(group.members.length).toBe(0); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'wall_switch_double/right'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'group_1', device: 'wall_switch_double', endpoint: 'right'}), + ); await flushPromises(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: '3'}), + ); await flushPromises(); expect(group.members).toStrictEqual([endpoint]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({data: {device: 'wall_switch_double/right', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -624,7 +633,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -645,7 +654,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: 'bulb_color', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -657,13 +666,16 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(endpoint); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/3'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: '3'}), + ); await flushPromises(); expect(group.members).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: '0x0017880104e45542/3', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: '0x0017880104e45542', endpoint: '3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -675,13 +687,16 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(endpoint); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'wall_switch_double/3'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: 'wall_switch_double', endpoint: '3'}), + ); await flushPromises(); expect(group.members).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: 'wall_switch_double/3', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: 'wall_switch_double', endpoint: '3', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -693,13 +708,16 @@ describe('Extension: Groups', () => { const group = groups.group_1; group.members.push(endpoint); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: '0x0017880104e45542/right'})); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: 'right'}), + ); await flushPromises(); expect(group.members).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({data: {device: '0x0017880104e45542/right', group: 'group_1'}, status: 'ok'}), + stringify({data: {device: '0x0017880104e45542', endpoint: 'right', group: 'group_1'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -709,13 +727,13 @@ describe('Extension: Groups', () => { const group = groups.group_1; groups.group_1.members.push(devices.QBKG03LM.endpoints[2]); mockMQTT.publish.mockClear(); - mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542/right'})); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542', endpoint: 'right'})); await flushPromises(); expect(group.members).toStrictEqual([]); expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove_all', - stringify({data: {device: '0x0017880104e45542/right'}, status: 'ok'}), + stringify({data: {device: '0x0017880104e45542', endpoint: 'right'}, status: 'ok'}), {retain: false, qos: 0}, expect.any(Function), ); @@ -729,11 +747,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/remove', - stringify({ - data: {device: 'bulb_color', group: 'group_1_not_existing'}, - status: 'error', - error: "Group 'group_1_not_existing' does not exist", - }), + stringify({data: {}, status: 'error', error: "Group 'group_1_not_existing' does not exist"}), {retain: false, qos: 0}, expect.any(Function), ); @@ -747,11 +761,7 @@ describe('Extension: Groups', () => { expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({ - data: {device: 'bulb_color_not_existing', group: 'group_1'}, - status: 'error', - error: "Device 'bulb_color_not_existing' does not exist", - }), + stringify({data: {}, status: 'error', error: "Device 'bulb_color_not_existing' does not exist"}), {retain: false, qos: 0}, expect.any(Function), ); @@ -762,17 +772,41 @@ describe('Extension: Groups', () => { mockMQTT.publish.mockClear(); mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', - stringify({group: 'group_1', device: 'bulb_color/not_existing_endpoint'}), + stringify({group: 'group_1', device: 'bulb_color', endpoint: 'not_existing_endpoint'}), + ); + await flushPromises(); + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}), + {retain: false, qos: 0}, + expect.any(Function), + ); + }); + + it('Error when invalid payload', async () => { + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', devicez: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); + expect(mockMQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + expect.any(Function), ); + }); + + it('Error when add/remove with invalid payload', async () => { + mockLogger.error.mockClear(); + mockMQTT.publish.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({groupz: 'group_1', device: 'bulb_color'})); await flushPromises(); expect(mockMQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object), expect.any(Function)); expect(mockMQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/members/add', - stringify({ - data: {device: 'bulb_color/not_existing_endpoint', group: 'group_1'}, - status: 'error', - error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'", - }), + stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, expect.any(Function), ); diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index 98be1a7097..a57c8ded26 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -2268,7 +2268,7 @@ describe('Extension: HomeAssistant', () => { mockMQTT.publish.mockClear(); mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/group/members/add', - stringify({group: 'ha_discovery_group', device: 'wall_switch_double/left'}), + stringify({group: 'ha_discovery_group', device: 'wall_switch_double', endpoint: 'left'}), ); await flushPromises(); From 61cf5094c7f0bf112374b900a13a3a5986519004 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 3 Nov 2024 22:16:26 +0100 Subject: [PATCH 10/70] fix 236b0843968961a0f6e6209624d05d9d9bf6af8d --- lib/extension/homeassistant.ts | 253 +--- pnpm-lock.yaml | 2008 +++++++++++++------------ test/extensions/homeassistant.test.ts | 12 +- 3 files changed, 1125 insertions(+), 1148 deletions(-) diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index f641a129d5..543b63254e 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -43,18 +43,6 @@ const ACTION_PATTERNS: string[] = [ '^(?dial_rotate)_(?left|right)_(?step|slow|fast)$', '^(?brightness_step)(?:_(?up|down))?$', ]; - -const SENSOR_CLICK: Readonly = { - type: 'sensor', - object_id: 'click', - mockProperties: [{property: 'click', value: null}], - discovery_payload: { - name: 'Click', - icon: 'mdi:toggle-switch', - value_template: '{{ value_json.click }}', - }, -}; - const ACCESS_STATE = 0b001; const ACCESS_SET = 0b010; const GROUP_SUPPORTED_TYPES: ReadonlyArray = ['light', 'switch', 'lock', 'cover']; @@ -63,55 +51,6 @@ const COVER_OPENING_LOOKUP: ReadonlyArray = ['opening', 'open', 'forward const COVER_CLOSING_LOOKUP: ReadonlyArray = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining']; const COVER_STOPPED_LOOKUP: ReadonlyArray = ['stopped', 'stop', 'pause', 'paused']; const SWITCH_DIFFERENT: ReadonlyArray = ['valve_detection', 'window_detection', 'auto_lock', 'away_mode']; -const LEGACY_MAPPING: ReadonlyArray<{models: string[]; discovery: DiscoveryEntry}> = [ - { - models: [ - 'WXKG01LM', - 'HS1EB/HS1EB-E', - 'ICZB-KPD14S', - 'TERNCY-SD01', - 'TERNCY-PP01', - 'ICZB-KPD18S', - 'E1766', - 'ZWallRemote0', - 'ptvo.switch', - '2AJZ4KPKEY', - 'ZGRC-KEY-013', - 'HGZB-02S', - 'HGZB-045', - 'HGZB-1S', - 'AV2010/34', - 'IM6001-BTP01', - 'WXKG11LM', - 'WXKG03LM', - 'WXKG02LM_rev1', - 'WXKG02LM_rev2', - 'QBKG04LM', - 'QBKG03LM', - 'QBKG11LM', - 'QBKG21LM', - 'QBKG22LM', - 'WXKG12LM', - 'QBKG12LM', - 'E1743', - ], - discovery: SENSOR_CLICK, - }, - { - models: ['ICTC-G-1'], - discovery: { - type: 'sensor', - mockProperties: [{property: 'brightness', value: null}], - object_id: 'brightness', - discovery_payload: { - name: 'Brightness', - unit_of_measurement: 'brightness', - icon: 'mdi:brightness-5', - value_template: '{{ value_json.brightness }}', - }, - }, - }, -]; const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = { activity_led_indicator: {icon: 'mdi:led-on'}, auto_off: {icon: 'mdi:flash-auto'}, @@ -139,7 +78,6 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = { led_disabled_night: {entity_category: 'config', icon: 'mdi:led-off'}, led_indication: {entity_category: 'config', icon: 'mdi:led-on'}, led_enable: {entity_category: 'config', icon: 'mdi:led-on'}, - legacy: {entity_category: 'config', icon: 'mdi:cog'}, motor_reversal: {entity_category: 'config', icon: 'mdi:arrow-left-right'}, moving: {device_class: 'moving'}, no_position_support: {entity_category: 'config', icon: 'mdi:minus-circle-outline'}, @@ -242,8 +180,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = { humidity_max: {entity_category: 'config', icon: 'mdi:water-percent'}, humidity_min: {entity_category: 'config', icon: 'mdi:water-percent'}, illuminance_calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'}, - illuminance_lux: {device_class: 'illuminance', state_class: 'measurement'}, - illuminance: {device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement'}, + illuminance: {device_class: 'illuminance', state_class: 'measurement'}, linkquality: { enabled_by_default: false, entity_category: 'diagnostic', @@ -448,8 +385,6 @@ export default class HomeAssistant extends Extension { private discoveryRegex: RegExp; private discoveryRegexWoTopic = new RegExp(`(.*)/(.*)/(.*)/config`); private statusTopic: string; - private entityAttributes: boolean; - private legacyTrigger: boolean; private experimentalEventEntities: boolean; // @ts-expect-error initialized in `start` private zigbee2MQTTVersion: string; @@ -481,8 +416,6 @@ export default class HomeAssistant extends Extension { this.discoveryTopic = haSettings.discovery_topic; this.discoveryRegex = new RegExp(`${haSettings.discovery_topic}/(.*)/(.*)/(.*)/config`); this.statusTopic = haSettings.status_topic; - this.entityAttributes = haSettings.legacy_entity_attributes; - this.legacyTrigger = haSettings.legacy_triggers; this.experimentalEventEntities = haSettings.experimental_event_entities; if (haSettings.discovery_topic === settings.get().mqtt.base_topic) { throw new Error(`'homeassistant.discovery_topic' cannot not be equal to the 'mqtt.base_topic' (got '${settings.get().mqtt.base_topic}')`); @@ -513,8 +446,8 @@ export default class HomeAssistant extends Extension { this.eventBus.onEntityOptionsChanged(this, async (data) => await this.discover(data.entity)); this.eventBus.onExposesChanged(this, async (data) => await this.discover(data.device)); - this.mqtt.subscribe(this.statusTopic); - this.mqtt.subscribe(DEFAULT_STATUS_TOPIC); + await this.mqtt.subscribe(this.statusTopic); + await this.mqtt.subscribe(DEFAULT_STATUS_TOPIC); /** * Prevent unnecessary re-discovery of entities by waiting 5 seconds for retained discovery messages to come in. @@ -531,9 +464,9 @@ export default class HomeAssistant extends Extension { } logger.debug(`Discovering entities to Home Assistant in ${discoverWait}s`); - this.mqtt.subscribe(`${this.discoveryTopic}/#`); + await this.mqtt.subscribe(`${this.discoveryTopic}/#`); setTimeout(async () => { - this.mqtt.unsubscribe(`${this.discoveryTopic}/#`); + await this.mqtt.unsubscribe(`${this.discoveryTopic}/#`); logger.debug(`Discovering entities to Home Assistant`); await this.discover(this.bridge); @@ -815,7 +748,7 @@ export default class HomeAssistant extends Extension { case 'lock': { assert(!endpoint, `Endpoint not supported for lock type`); const state = (firstExpose as zhc.Lock).features.filter(isBinaryExpose).find((f) => f.name === 'state'); - assert(state, `Lock expose must have a 'state'`); + assert(state?.property === 'state', "Lock property must be 'state'"); const discoveryEntry: DiscoveryEntry = { type: 'lock', object_id: 'lock', @@ -824,35 +757,11 @@ export default class HomeAssistant extends Extension { name: null, command_topic: true, value_template: `{{ value_json.${state.property} }}`, + state_locked: state.value_on, + state_unlocked: state.value_off, }, }; - // istanbul ignore if - if (state.property === 'keypad_lockout') { - // deprecated: keypad_lockout is messy, but changing is breaking - discoveryEntry.discovery_payload.name = firstExpose.label; - discoveryEntry.discovery_payload.payload_lock = state.value_on; - discoveryEntry.discovery_payload.payload_unlock = state.value_off; - discoveryEntry.discovery_payload.state_topic = true; - discoveryEntry.object_id = 'keypad_lock'; - } else if (state.property === 'child_lock') { - // deprecated: child_lock is messy, but changing is breaking - discoveryEntry.discovery_payload.name = firstExpose.label; - discoveryEntry.discovery_payload.payload_lock = state.value_on; - discoveryEntry.discovery_payload.payload_unlock = state.value_off; - discoveryEntry.discovery_payload.state_locked = 'LOCK'; - discoveryEntry.discovery_payload.state_unlocked = 'UNLOCK'; - discoveryEntry.discovery_payload.state_topic = true; - discoveryEntry.object_id = 'child_lock'; - } else { - discoveryEntry.discovery_payload.state_locked = state.value_on; - discoveryEntry.discovery_payload.state_unlocked = state.value_off; - } - - if (state.property !== 'state') { - discoveryEntry.discovery_payload.command_topic_postfix = state.property; - } - discoveryEntries.push(discoveryEntry); break; } @@ -1147,8 +1056,35 @@ export default class HomeAssistant extends Extension { } case 'enum': { assertEnumExpose(firstExpose); - const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; + /** + * If enum attribute does not have SET access and is named 'action', then expose + * as EVENT entity. Wildcard actions like `recall_*` are currently not supported. + */ + if (firstExpose.property === 'action') { + if ( + this.experimentalEventEntities && + firstExpose.access & ACCESS_STATE && + !(firstExpose.access & ACCESS_SET) && + firstExpose.property == 'action' + ) { + discoveryEntries.push({ + type: 'event', + object_id: firstExpose.property, + mockProperties: [{property: firstExpose.property, value: null}], + discovery_payload: { + name: endpoint ? /* istanbul ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label, + state_topic: true, + event_types: this.prepareActionEventTypes(firstExpose.values), + value_template: this.actionValueTemplate, + ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], + }, + }); + } + // Don't expose action sensor, use MQTT device trigger instead + break; + } + const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; if (firstExpose.access & ACCESS_STATE) { discoveryEntries.push({ type: 'sensor', @@ -1163,30 +1099,6 @@ export default class HomeAssistant extends Extension { }); } - /** - * If enum attribute does not have SET access and is named 'action', then expose - * as EVENT entity. Wildcard actions like `recall_*` are currently not supported. - */ - if ( - this.experimentalEventEntities && - firstExpose.access & ACCESS_STATE && - !(firstExpose.access & ACCESS_SET) && - firstExpose.property == 'action' - ) { - discoveryEntries.push({ - type: 'event', - object_id: firstExpose.property, - mockProperties: [{property: firstExpose.property, value: null}], - discovery_payload: { - name: endpoint ? /* istanbul ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label, - state_topic: true, - event_types: this.prepareActionEventTypes(firstExpose.values), - value_template: this.actionValueTemplate, - ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], - }, - }); - } - /** * If enum attribute has SET access then expose as SELECT entity too. * Note: currently both sensor and select are discovered, this is to avoid @@ -1237,7 +1149,7 @@ export default class HomeAssistant extends Extension { case 'text': case 'composite': case 'list': { - // Deprecated: remove text sensor + // legacy: remove text sensor const firstExposeTyped = firstExpose as zhc.Text | zhc.Composite | zhc.List; const settableText = firstExposeTyped.type === 'text' && firstExposeTyped.access & ACCESS_SET; if (firstExposeTyped.access & ACCESS_STATE) { @@ -1260,7 +1172,7 @@ export default class HomeAssistant extends Extension { discoveryEntries.push({ type: 'text', object_id: firstExposeTyped.property, - mockProperties: [], // Already mocked above in case access STATE is supported + mockProperties: firstExposeTyped.access & ACCESS_STATE ? [{property: firstExposeTyped.property, value: null}] : [], discovery_payload: { name: endpoint ? `${firstExposeTyped.label} ${endpoint}` : firstExposeTyped.label, state_topic: firstExposeTyped.access & ACCESS_STATE, @@ -1365,31 +1277,16 @@ export default class HomeAssistant extends Extension { } } - /** - * Publish an empty value for click and action payload, in this way Home Assistant - * can use Home Assistant entities in automations. - * https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347 - */ - if (this.legacyTrigger) { - const keys = ['action', 'click'].filter((k) => data.message[k]); - for (const key of keys) { - await this.publishEntityState(data.entity, {[key]: ''}); - } - } - /** * Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/) * The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device * Whenever a device publish an {action: *} we discover an MQTT device trigger sensor * and republish it to zigbee2mqtt/my_device/action */ - if (entity.isDevice() && entity.definition) { - const keys = ['action', 'click'].filter((k) => data.message[k]); - for (const key of keys) { - const value = data.message[key].toString(); - await this.publishDeviceTriggerDiscover(entity, key, value); - await this.mqtt.publish(`${data.entity.name}/${key}`, value, {}); - } + if (entity.isDevice() && entity.definition && 'action' in data.message) { + const value = data.message['action'].toString(); + await this.publishDeviceTriggerDiscover(entity, 'action', value); + await this.mqtt.publish(`${data.entity.name}/action`, value, {}); } } @@ -1433,20 +1330,6 @@ export default class HomeAssistant extends Extension { for (const expose of exposes) { configs.push(...this.exposeToConfig([expose], 'device', exposes, entity.definition)); } - - for (const mapping of LEGACY_MAPPING) { - if (mapping.models.includes(entity.definition!.model)) { - configs.push(mapping.discovery); - } - } - - // @ts-expect-error deprecated in favour of exposes - const haConfig = entity.definition?.homeassistant; - - /* istanbul ignore if */ - if (haConfig != undefined) { - configs.push(haConfig); - } } else if (isGroup) { // group const exposesByType: {[s: string]: zhc.Expose[]} = {}; @@ -1504,35 +1387,6 @@ export default class HomeAssistant extends Extension { } if (isDevice && entity.definition?.ota) { - const updateStateSensor: DiscoveryEntry = { - type: 'sensor', - object_id: 'update_state', - mockProperties: [], // update is mocked below with updateSensor - discovery_payload: { - name: 'Update state', - icon: 'mdi:update', - value_template: `{{ value_json['update']['state'] }}`, - enabled_by_default: false, - entity_category: 'diagnostic', - }, - }; - - configs.push(updateStateSensor); - const updateAvailableSensor: DiscoveryEntry = { - type: 'binary_sensor', - object_id: 'update_available', - mockProperties: [{property: 'update_available', value: null}], - discovery_payload: { - name: null, - payload_on: true, - payload_off: false, - value_template: `{{ value_json['update']['state'] == "available" }}`, - enabled_by_default: false, - device_class: 'update', - entity_category: 'diagnostic', - }, - }; - configs.push(updateAvailableSensor); const updateSensor: DiscoveryEntry = { type: 'update', object_id: 'update', @@ -1576,14 +1430,6 @@ export default class HomeAssistant extends Extension { }); }); - if (isDevice && entity.options.legacy !== undefined && !entity.options.legacy) { - configs = configs.filter((c) => c !== SENSOR_CLICK); - } - - if (!this.legacyTrigger) { - configs = configs.filter((c) => (c.object_id !== 'action' && c.object_id !== 'click') || c.type == 'event'); - } - // deep clone of the config objects configs = JSON.parse(JSON.stringify(configs)); @@ -1647,10 +1493,6 @@ export default class HomeAssistant extends Extension { payload.tilt_status_topic = stateTopic; } - if (this.entityAttributes && (isDevice || isGroup)) { - payload.json_attributes_topic = stateTopic; - } - const devicePayload = this.getDevicePayload(entity); // Suggest object_id (entity_id) for entity @@ -2213,7 +2055,7 @@ export default class HomeAssistant extends Extension { return bridge; } - private parseActionValue(action: string): ActionData { + parseActionValue(action: string): ActionData { // Handle standard actions. for (const p of ACTION_PATTERNS) { const m = action.match(p); @@ -2268,20 +2110,19 @@ export default class HomeAssistant extends Extension { const value_template = `{% set patterns = [\n${patterns}\n] %}\n` + - `{% set action_value = value_json.action|default('') %}\n` + - `{% set ns = namespace(r=[('action', action_value)]) %}\n` + + `{% set ns = namespace(r=[('event_type', value_json.action)]) %}\n` + `{% for p in patterns %}\n` + - ` {% set m = action_value|regex_findall(p.pattern) %}\n` + + ` {% set m = value_json.action|regex_findall(p.pattern) %}\n` + ` {% if m[0] is undefined %}{% continue %}{% endif %}\n` + ` {% for key, value in zip(p.groups, m[0]) %}\n` + - ` {% set ns.r = ns.r|rejectattr(0, 'eq', key)|list + [(key, value)] %}\n` + + ` {% set ns.r = ns.r + [(key, value)] %}\n` + ` {% endfor %}\n` + `{% endfor %}\n` + `{% if ns.r|selectattr(0, 'eq', 'actionPrefix')|first is defined %}\n` + ` {% set ns.r = ns.r|rejectattr(0, 'eq', 'action')|list + [('action', ns.r|selectattr(0, 'eq', 'actionPrefix')|map(attribute=1)|first + ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}\n` + `{% endif %}\n` + `{% set ns.r = ns.r + [('event_type', ns.r|selectattr(0, 'eq', 'action')|map(attribute=1)|first)] %}\n` + - `{{dict.from_keys(ns.r|rejectattr(0, 'in', 'action, actionPrefix')|reject('eq', ('event_type', None))|reject('eq', ('event_type', '')))|to_json}}`; + `{{dict.from_keys(ns.r|rejectattr(0, 'in', 'action, actionPrefix'))|to_json}}`; return value_template; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7378bcb44..4f63b48dc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,12 @@ importers: bind-decorator: specifier: ^1.0.11 version: 1.0.11 - connect-gzip-static: - specifier: 3.0.1 - version: 3.0.1 debounce: - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^2.2.0 + version: 2.2.0 + express-static-gzip: + specifier: ^2.2.0 + version: 2.2.0 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -51,8 +51,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 mqtt: - specifier: ^5.10.1 - version: 5.10.1 + specifier: ^5.10.2 + version: 5.10.3 object-assign-deep: specifier: ^0.4.0 version: 0.4.0 @@ -72,14 +72,14 @@ importers: specifier: ^4.4.1 version: 4.4.1 winston: - specifier: ^3.15.0 - version: 3.15.0 + specifier: ^3.17.0 + version: 3.17.0 winston-syslog: specifier: ^2.7.1 - version: 2.7.1(winston@3.15.0) + version: 2.7.1(winston@3.17.0) winston-transport: - specifier: ^4.8.0 - version: 4.8.0 + specifier: ^4.9.0 + version: 4.9.0 ws: specifier: ^8.18.0 version: 8.18.0 @@ -98,26 +98,26 @@ importers: version: 2.8.0 devDependencies: '@babel/core': - specifier: ^7.25.8 - version: 7.25.8 + specifier: ^7.26.0 + version: 7.26.0 '@babel/plugin-proposal-decorators': - specifier: ^7.25.7 - version: 7.25.7(@babel/core@7.25.8) + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) '@babel/preset-env': - specifier: ^7.25.8 - version: 7.25.8(@babel/core@7.25.8) + specifier: ^7.26.0 + version: 7.26.0(@babel/core@7.26.0) '@babel/preset-typescript': - specifier: ^7.25.7 - version: 7.25.7(@babel/core@7.25.8) + specifier: ^7.26.0 + version: 7.26.0(@babel/core@7.26.0) '@eslint/core': - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^0.9.0 + version: 0.9.0 '@eslint/js': - specifier: ^9.12.0 - version: 9.12.0 + specifier: ^9.15.0 + version: 9.16.0 '@ianvs/prettier-plugin-sort-imports': - specifier: ^4.3.1 - version: 4.3.1(prettier@3.3.3) + specifier: ^4.4.0 + version: 4.4.0(prettier@3.3.3) '@types/eslint__js': specifier: ^8.42.3 version: 8.42.3 @@ -128,38 +128,41 @@ importers: specifier: ^3.27.4 version: 3.27.4 '@types/jest': - specifier: ^29.5.13 - version: 29.5.13 + specifier: ^29.5.14 + version: 29.5.14 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^22.7.5 - version: 22.7.5 + specifier: ^22.9.3 + version: 22.10.1 '@types/object-assign-deep': specifier: ^0.4.3 version: 0.4.3 '@types/readable-stream': - specifier: 4.0.15 - version: 4.0.15 + specifier: 4.0.18 + version: 4.0.18 '@types/sd-notify': specifier: ^2.8.2 version: 2.8.2 + '@types/serve-static': + specifier: ^1.15.7 + version: 1.15.7 '@types/ws': - specifier: 8.5.12 - version: 8.5.12 + specifier: 8.5.13 + version: 8.5.13 babel-jest: specifier: ^29.7.0 - version: 29.7.0(@babel/core@7.25.8) + version: 29.7.0(@babel/core@7.26.0) eslint: - specifier: ^9.12.0 - version: 9.12.0 + specifier: ^9.15.0 + version: 9.16.0 eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.12.0) + version: 9.1.0(eslint@9.16.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.7.5) + version: 29.7.0(@types/node@22.10.1) prettier: specifier: ^3.3.3 version: 3.3.3 @@ -167,11 +170,11 @@ importers: specifier: ^0.2.3 version: 0.2.3 typescript: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.7.2 + version: 5.7.2 typescript-eslint: - specifier: ^8.8.1 - version: 8.8.1(eslint@9.12.0)(typescript@5.6.3) + specifier: ^8.15.0 + version: 8.16.0(eslint@9.16.0)(typescript@5.7.2) packages: @@ -183,32 +186,40 @@ packages: resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.8': - resolution: {integrity: sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/core@7.25.8': - resolution: {integrity: sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==} + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.25.7': - resolution: {integrity: sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.25.7': resolution: {integrity: sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==} engines: {node: '>=6.9.0'} - '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': - resolution: {integrity: sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==} + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.7': - resolution: {integrity: sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.25.7': - resolution: {integrity: sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==} + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -219,71 +230,89 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.25.9': + resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-define-polyfill-provider@0.6.2': resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-member-expression-to-functions@7.25.7': - resolution: {integrity: sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==} + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.25.7': - resolution: {integrity: sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.25.7': - resolution: {integrity: sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.25.7': - resolution: {integrity: sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==} + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} engines: {node: '>=6.9.0'} '@babel/helper-plugin-utils@7.25.7': resolution: {integrity: sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==} engines: {node: '>=6.9.0'} - '@babel/helper-remap-async-to-generator@7.25.7': - resolution: {integrity: sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.25.7': - resolution: {integrity: sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==} + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-simple-access@7.25.7': - resolution: {integrity: sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==} + '@babel/helper-simple-access@7.25.9': + resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==} engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.25.7': - resolution: {integrity: sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==} + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.25.7': resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.7': resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.7': - resolution: {integrity: sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.25.7': - resolution: {integrity: sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.7': - resolution: {integrity: sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.25.7': @@ -295,38 +324,43 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7': - resolution: {integrity: sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==} + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7': - resolution: {integrity: sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==} + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7': - resolution: {integrity: sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7': - resolution: {integrity: sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7': - resolution: {integrity: sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-decorators@7.25.7': - resolution: {integrity: sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==} + '@babel/plugin-proposal-decorators@7.25.9': + resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -358,14 +392,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-decorators@7.25.7': - resolution: {integrity: sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==} + '@babel/plugin-syntax-decorators@7.25.9': + resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.25.7': - resolution: {integrity: sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==} + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -376,6 +410,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -392,6 +432,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -440,314 +486,326 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.25.7': - resolution: {integrity: sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==} + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.25.8': - resolution: {integrity: sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==} + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.25.7': - resolution: {integrity: sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==} + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.25.7': - resolution: {integrity: sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==} + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.25.7': - resolution: {integrity: sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==} + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.25.7': - resolution: {integrity: sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==} + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.25.8': - resolution: {integrity: sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==} + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.25.7': - resolution: {integrity: sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==} + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.25.7': - resolution: {integrity: sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==} + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.25.7': - resolution: {integrity: sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==} + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.25.7': - resolution: {integrity: sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==} + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-keys@7.25.7': - resolution: {integrity: sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==} + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7': - resolution: {integrity: sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-dynamic-import@7.25.8': - resolution: {integrity: sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==} + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.25.7': - resolution: {integrity: sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==} + '@babel/plugin-transform-exponentiation-operator@7.25.9': + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.25.8': - resolution: {integrity: sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==} + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.25.7': - resolution: {integrity: sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==} + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.25.7': - resolution: {integrity: sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==} + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.25.8': - resolution: {integrity: sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==} + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.25.7': - resolution: {integrity: sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==} + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.25.8': - resolution: {integrity: sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==} + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.25.7': - resolution: {integrity: sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==} + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-amd@7.25.7': - resolution: {integrity: sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==} + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.25.7': - resolution: {integrity: sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==} + '@babel/plugin-transform-modules-commonjs@7.25.9': + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.25.7': - resolution: {integrity: sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==} + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-umd@7.25.7': - resolution: {integrity: sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==} + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.25.7': - resolution: {integrity: sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==} + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-new-target@7.25.7': - resolution: {integrity: sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==} + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.25.8': - resolution: {integrity: sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==} + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.25.8': - resolution: {integrity: sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==} + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.25.8': - resolution: {integrity: sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==} + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.25.7': - resolution: {integrity: sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==} + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.25.8': - resolution: {integrity: sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==} + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.25.8': - resolution: {integrity: sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==} + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.25.7': - resolution: {integrity: sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==} + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.25.7': - resolution: {integrity: sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==} + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.25.8': - resolution: {integrity: sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==} + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.25.7': - resolution: {integrity: sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==} + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.25.7': - resolution: {integrity: sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==} + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-reserved-words@7.25.7': - resolution: {integrity: sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==} + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.25.7': - resolution: {integrity: sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==} + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.25.7': - resolution: {integrity: sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==} + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-sticky-regex@7.25.7': - resolution: {integrity: sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==} + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.25.7': - resolution: {integrity: sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==} + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typeof-symbol@7.25.7': - resolution: {integrity: sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==} + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.25.7': - resolution: {integrity: sha512-VKlgy2vBzj8AmEzunocMun2fF06bsSWV+FvVXohtL6FGve/+L217qhHxRTVGHEDO/YR8IANcjzgJsd04J8ge5Q==} + '@babel/plugin-transform-typescript@7.25.9': + resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-escapes@7.25.7': - resolution: {integrity: sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==} + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-property-regex@7.25.7': - resolution: {integrity: sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==} + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-regex@7.25.7': - resolution: {integrity: sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==} + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-sets-regex@7.25.7': - resolution: {integrity: sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==} + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.25.8': - resolution: {integrity: sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==} + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -757,8 +815,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/preset-typescript@7.25.7': - resolution: {integrity: sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==} + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -771,14 +829,22 @@ packages: resolution: {integrity: sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.7': - resolution: {integrity: sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} '@babel/types@7.25.8': resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -795,44 +861,40 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.18.0': - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.6.0': - resolution: {integrity: sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==} + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.12.0': - resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} + '@eslint/js@9.16.0': + resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.0': - resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@folder/readdir@3.1.0': - resolution: {integrity: sha512-mqkVdQ77BcCOWur/dxoPxjJS2eJg8Eqqx1Dgdc/qbTeGI5UMPDLmT0peGEjxLdlnD9SvhMnpJuiyaYl2btdT6A==} - engines: {node: '>=10'} - - '@humanfs/core@0.19.0': - resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.5': - resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -843,8 +905,12 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@ianvs/prettier-plugin-sort-imports@4.3.1': - resolution: {integrity: sha512-ZHwbyjkANZOjaBm3ZosADD2OUYGFzQGxfy67HmGZU94mHqe7g1LCMA7YYKB1Cq+UTPCBqlAYapY0KXAjKEw8Sg==} + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@ianvs/prettier-plugin-sort-imports@4.4.0': + resolution: {integrity: sha512-f4/e+/ANGk3tHuwRW0uh2YuBR50I4h1ZjGQ+5uD8sWfinHTivQsnieR5cz24t8M6Vx4rYvZ5v/IEKZhYpzQm9Q==} peerDependencies: '@vue/compiler-sfc': 2.7.x || 3.x prettier: 2 || 3 @@ -1023,6 +1089,9 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + '@types/humanize-duration@3.27.4': resolution: {integrity: sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==} @@ -1035,8 +1104,8 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.13': - resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1044,26 +1113,35 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.10.1': + resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} '@types/object-assign-deep@0.4.3': resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==} - '@types/readable-stream@4.0.15': - resolution: {integrity: sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==} + '@types/readable-stream@4.0.18': + resolution: {integrity: sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==} '@types/sd-notify@2.8.2': resolution: {integrity: sha512-LVWtuGvzso9z3N89NISzseq8RVHkEeg2h275370yQYx8/CoNaV2NnG17TTjDavy2FrmcUBFaR6OymlPQjqfb2g==} + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1071,8 +1149,8 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@8.8.1': - resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} + '@typescript-eslint/eslint-plugin@8.16.0': + resolution: {integrity: sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -1082,8 +1160,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.8.1': - resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} + '@typescript-eslint/parser@8.16.0': + resolution: {integrity: sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1092,25 +1170,26 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.8.1': - resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} + '@typescript-eslint/scope-manager@8.16.0': + resolution: {integrity: sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.8.1': - resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} + '@typescript-eslint/type-utils@8.16.0': + resolution: {integrity: sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true - '@typescript-eslint/types@8.8.1': - resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} + '@typescript-eslint/types@8.16.0': + resolution: {integrity: sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.8.1': - resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} + '@typescript-eslint/typescript-estree@8.16.0': + resolution: {integrity: sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1118,14 +1197,18 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.8.1': - resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} + '@typescript-eslint/utils@8.16.0': + resolution: {integrity: sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true - '@typescript-eslint/visitor-keys@8.8.1': - resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} + '@typescript-eslint/visitor-keys@8.16.0': + resolution: {integrity: sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} abort-controller@3.0.0: @@ -1137,8 +1220,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -1372,9 +1455,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - connect-gzip-static@3.0.1: - resolution: {integrity: sha512-VVqi1fJ4uZK8aIrb1BN9n7tsRAbc3BhkIo/mz1VuNhYC2KDuP1ZgCDapjYIfT3mcGgWzyUr04kv7nmnGOnCvWg==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1393,8 +1473,12 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - debounce@2.1.1: - resolution: {integrity: sha512-+xRWxgel9LgTC4PwKlm7TJUK6B6qsEK77NaiNvXmeQ7Y3e6OVVsBC4a9BSptS/mAYceyAz37Oa8JTTuPRft7uQ==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} debug@2.6.9: @@ -1520,20 +1604,20 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-scope@8.1.0: - resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.1.0: - resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.12.0: - resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} + eslint@9.16.0: + resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1542,8 +1626,8 @@ packages: jiti: optional: true - espree@10.2.0: - resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: @@ -1591,6 +1675,9 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + express-static-gzip@2.2.0: + resolution: {integrity: sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2086,8 +2173,8 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - logform@2.6.1: - resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} lru-cache@10.4.3: @@ -2164,11 +2251,11 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - mqtt-packet@9.0.0: - resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==} + mqtt-packet@9.0.1: + resolution: {integrity: sha512-koZF1V/X2RZUI6uD9wN5OK1JxxcG1ofAR4H3LjCw1FkeKzruZQ26aAA6v2m1lZyWONZIR5wMMJFrZJDRNzbiQw==} - mqtt@5.10.1: - resolution: {integrity: sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==} + mqtt@5.10.3: + resolution: {integrity: sha512-hA/6YrUS4fywhBGCjH/XXUuLeueJiPqruVVWjK2A24Ma4KcWfZ/x8x07aoesBV+HXDWBC08tbT4IWfSXNW0Jtw==} engines: {node: '>=16.0.0'} hasBin: true @@ -2468,10 +2555,6 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} - send@0.19.1: - resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} - engines: {node: '>= 0.8.0'} - serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} @@ -2608,9 +2691,6 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -2665,22 +2745,23 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.8.1: - resolution: {integrity: sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==} + typescript-eslint@8.16.0: + resolution: {integrity: sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} @@ -2736,12 +2817,12 @@ packages: peerDependencies: winston: ^3.8.2 - winston-transport@4.8.0: - resolution: {integrity: sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==} + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} - winston@3.15.0: - resolution: {integrity: sha512-RhruH2Cj0bV0WgNL+lOfoUBI4DVfdUNjVnJGVovWZmrcKtrFTTRzgXYK2O9cymSGjrERCtaAeHwMNnUWXlwZow==} + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} engines: {node: '>= 12.0.0'} word-wrap@1.2.5: @@ -2825,20 +2906,26 @@ snapshots: '@babel/highlight': 7.25.7 picocolors: 1.1.0 - '@babel/compat-data@7.25.8': {} + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.0 + + '@babel/compat-data@7.26.2': {} - '@babel/core@7.25.8': + '@babel/core@7.26.0': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helpers': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 convert-source-map: 2.0.0 debug: 4.3.7 gensync: 1.0.0-beta.2 @@ -2847,9 +2934,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.25.7': + '@babel/generator@7.26.2': dependencies: - '@babel/types': 7.25.8 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.0.2 @@ -2858,132 +2946,148 @@ snapshots: dependencies: '@babel/types': 7.25.8 - '@babel/helper-builder-binary-assignment-operator-visitor@7.25.7': + '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 + + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-compilation-targets@7.25.7': + '@babel/helper-compilation-targets@7.25.9': dependencies: - '@babel/compat-data': 7.25.8 - '@babel/helper-validator-option': 7.25.7 + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 browserslist: 4.24.0 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.25.7(@babel/core@7.25.8)': + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-member-expression-to-functions': 7.25.7 - '@babel/helper-optimise-call-expression': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.25.9 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.25.7(@babel/core@7.25.8)': + '@babel/helper-create-regexp-features-plugin@7.25.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-annotate-as-pure': 7.25.7 regexpu-core: 6.1.1 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.25.8)': + '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.1.1 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 debug: 4.3.7 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: - supports-color - '@babel/helper-member-expression-to-functions@7.25.7': + '@babel/helper-member-expression-to-functions@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.25.7': + '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.25.7(@babel/core@7.25.8)': + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-simple-access': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.25.7': + '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.25.8 + '@babel/types': 7.26.0 '@babel/helper-plugin-utils@7.25.7': {} - '@babel/helper-remap-async-to-generator@7.25.7(@babel/core@7.25.8)': + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-wrap-function': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.25.7(@babel/core@7.25.8)': + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-member-expression-to-functions': 7.25.7 - '@babel/helper-optimise-call-expression': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/helper-simple-access@7.25.7': + '@babel/helper-simple-access@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.25.7': + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-validator-identifier@7.25.7': {} - '@babel/helper-validator-option@7.25.7': {} + '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-wrap-function@7.25.7': + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': dependencies: - '@babel/template': 7.25.7 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color - '@babel/helpers@7.25.7': + '@babel/helpers@7.26.0': dependencies: - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 '@babel/highlight@7.25.7': dependencies: @@ -2996,572 +3100,598 @@ snapshots: dependencies: '@babel/types': 7.25.8 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.7(@babel/core@7.25.8)': + '@babel/parser@7.26.2': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/types': 7.26.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-decorators': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.8)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-decorators@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-assertions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-attributes@7.25.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-jsx@7.25.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.8)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.8)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.8)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.8)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-transform-arrow-functions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-syntax-typescript@7.25.7(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-transform-async-generator-functions@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-imports': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-remap-async-to-generator': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-block-scoping@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-class-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/template': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 - '@babel/plugin-transform-destructuring@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dotall-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-keys@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-dynamic-import@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-exponentiation-operator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-for-of@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-logical-assignment-operators@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-member-expression-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-modules-amd@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-simple-access': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - '@babel/traverse': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-module-transforms': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-new-target@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-nullish-coalescing-operator@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-numeric-separator@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-object-rest-spread@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-super@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-replace-supers': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-optional-chaining@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-private-methods@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.25.8(@babel/core@7.25.8)': + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-regenerator@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 regenerator-transform: 0.15.2 - '@babel/plugin-transform-reserved-words@7.25.7(@babel/core@7.25.8)': - dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - - '@babel/plugin-transform-shorthand-properties@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-spread@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-sticky-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - - '@babel/plugin-transform-template-literals@7.25.7(@babel/core@7.25.8)': - dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - - '@babel/plugin-transform-typeof-symbol@7.25.7(@babel/core@7.25.8)': - dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-annotate-as-pure': 7.25.7 - '@babel/helper-create-class-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.7 - '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-property-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-unicode-sets-regex@7.25.7(@babel/core@7.25.8)': + '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-create-regexp-features-plugin': 7.25.7(@babel/core@7.25.8) - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color - '@babel/preset-env@7.25.8(@babel/core@7.25.8)': - dependencies: - '@babel/compat-data': 7.25.8 - '@babel/core': 7.25.8 - '@babel/helper-compilation-targets': 7.25.7 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-option': 7.25.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.8) - '@babel/plugin-syntax-import-assertions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.8) - '@babel/plugin-transform-arrow-functions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-async-generator-functions': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-async-to-generator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-block-scoped-functions': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-block-scoping': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-class-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-class-static-block': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-classes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-computed-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-destructuring': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-dotall-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-duplicate-keys': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-dynamic-import': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-exponentiation-operator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-export-namespace-from': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-for-of': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-function-name': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-json-strings': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-logical-assignment-operators': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-member-expression-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-amd': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-systemjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-umd': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-named-capturing-groups-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-new-target': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-nullish-coalescing-operator': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-numeric-separator': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-object-rest-spread': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-object-super': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-optional-catch-binding': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-optional-chaining': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-parameters': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-private-methods': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-private-property-in-object': 7.25.8(@babel/core@7.25.8) - '@babel/plugin-transform-property-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-regenerator': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-reserved-words': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-shorthand-properties': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-spread': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-sticky-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-template-literals': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-typeof-symbol': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-escapes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-property-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-regex': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-unicode-sets-regex': 7.25.7(@babel/core@7.25.8) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.8) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.8) - babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.8) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.8) + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.26.0) core-js-compat: 3.38.1 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.8)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 '@babel/types': 7.25.8 esutils: 2.0.3 - '@babel/preset-typescript@7.25.7(@babel/core@7.25.8)': + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': dependencies: - '@babel/core': 7.25.8 - '@babel/helper-plugin-utils': 7.25.7 - '@babel/helper-validator-option': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-modules-commonjs': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color @@ -3575,13 +3705,19 @@ snapshots: '@babel/parser': 7.25.8 '@babel/types': 7.25.8 - '@babel/traverse@7.25.7': + '@babel/template@7.25.9': dependencies: - '@babel/code-frame': 7.25.7 - '@babel/generator': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/template': 7.25.7 - '@babel/types': 7.25.8 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: @@ -3593,6 +3729,11 @@ snapshots: '@babel/helper-validator-identifier': 7.25.7 to-fast-properties: 2.0.0 + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@bcoe/v8-coverage@0.2.3': {} '@colors/colors@1.6.0': {} @@ -3603,14 +3744,14 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.16.0)': dependencies: - eslint: 9.12.0 + eslint: 9.16.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.1': {} + '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.18.0': + '@eslint/config-array@0.19.0': dependencies: '@eslint/object-schema': 2.1.4 debug: 4.3.7 @@ -3618,13 +3759,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.6.0': {} + '@eslint/core@0.9.0': {} - '@eslint/eslintrc@3.1.0': + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 debug: 4.3.7 - espree: 10.2.0 + espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -3634,34 +3775,33 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.12.0': {} + '@eslint/js@9.16.0': {} '@eslint/object-schema@2.1.4': {} - '@eslint/plugin-kit@0.2.0': + '@eslint/plugin-kit@0.2.3': dependencies: levn: 0.4.1 - '@folder/readdir@3.1.0': {} - - '@humanfs/core@0.19.0': {} + '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.5': + '@humanfs/node@0.16.6': dependencies: - '@humanfs/core': 0.19.0 + '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.1': {} - '@ianvs/prettier-plugin-sort-imports@4.3.1(prettier@3.3.3)': + '@humanwhocodes/retry@0.4.1': {} + + '@ianvs/prettier-plugin-sort-imports@4.4.0(prettier@3.3.3)': dependencies: - '@babel/core': 7.25.8 - '@babel/generator': 7.25.7 - '@babel/parser': 7.25.8 - '@babel/traverse': 7.25.7 - '@babel/types': 7.25.8 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 prettier: 3.3.3 semver: 7.6.3 transitivePeerDependencies: @@ -3689,7 +3829,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -3702,14 +3842,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.7.5) + jest-config: 29.7.0(@types/node@22.10.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3734,7 +3874,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -3752,7 +3892,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.7.5 + '@types/node': 22.10.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -3774,7 +3914,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -3821,7 +3961,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -3844,7 +3984,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.7.5 + '@types/node': 22.10.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -3950,11 +4090,13 @@ snapshots: '@types/finalhandler@1.2.3': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 + + '@types/http-errors@2.0.4': {} '@types/humanize-duration@3.27.4': {} @@ -3968,7 +4110,7 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.13': + '@types/jest@29.5.14': dependencies: expect: 29.7.0 pretty-format: 29.7.0 @@ -3977,26 +4119,39 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.7.5': + '@types/mime@1.3.5': {} + + '@types/node@22.10.1': dependencies: - undici-types: 6.19.8 + undici-types: 6.20.0 '@types/object-assign-deep@0.4.3': {} - '@types/readable-stream@4.0.15': + '@types/readable-stream@4.0.18': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 safe-buffer: 5.1.2 '@types/sd-notify@2.8.2': {} + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.10.1 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.10.1 + '@types/send': 0.17.4 + '@types/stack-utils@2.0.3': {} '@types/triple-beam@1.3.5': {} - '@types/ws@8.5.12': + '@types/ws@8.5.13': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 '@types/yargs-parser@21.0.3': {} @@ -4004,96 +4159,97 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.8.1 - eslint: 9.12.0 + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.16.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/type-utils': 8.16.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 + eslint: 9.16.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.3.0(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + '@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.16.0 debug: 4.3.7 - eslint: 9.12.0 + eslint: 9.16.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.8.1': + '@typescript-eslint/scope-manager@8.16.0': dependencies: - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 - '@typescript-eslint/type-utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.16.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.16.0)(typescript@5.7.2) debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.3) + eslint: 9.16.0 + ts-api-utils: 1.3.0(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.8.1': {} + '@typescript-eslint/types@8.16.0': {} - '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.16.0(typescript@5.7.2)': dependencies: - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/visitor-keys': 8.8.1 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/visitor-keys': 8.16.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.3) + ts-api-utils: 1.3.0(typescript@5.7.2) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': + '@typescript-eslint/utils@8.16.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@typescript-eslint/scope-manager': 8.8.1 - '@typescript-eslint/types': 8.8.1 - '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) - eslint: 9.12.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.16.0) + '@typescript-eslint/scope-manager': 8.16.0 + '@typescript-eslint/types': 8.16.0 + '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2) + eslint: 9.16.0 + optionalDependencies: + typescript: 5.7.2 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.8.1': + '@typescript-eslint/visitor-keys@8.16.0': dependencies: - '@typescript-eslint/types': 8.8.1 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.16.0 + eslint-visitor-keys: 4.2.0 abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 + acorn: 8.14.0 - acorn@8.12.1: {} + acorn@8.14.0: {} agent-base@7.1.1: dependencies: @@ -4160,13 +4316,13 @@ snapshots: b4a@1.6.7: {} - babel-jest@29.7.0(@babel/core@7.25.8): + babel-jest@29.7.0(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.25.8) + babel-preset-jest: 29.6.3(@babel/core@7.26.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -4190,54 +4346,54 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.25.8): + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.26.0): dependencies: - '@babel/compat-data': 7.25.8 - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.25.8): + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) core-js-compat: 3.38.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.25.8): + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.26.0): dependencies: - '@babel/core': 7.25.8 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.8) + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - babel-preset-current-node-syntax@1.1.0(@babel/core@7.25.8): - dependencies: - '@babel/core': 7.25.8 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.8) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.8) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.8) - '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.8) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.8) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.8) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.8) - - babel-preset-jest@29.6.3(@babel/core@7.25.8): - dependencies: - '@babel/core': 7.25.8 + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) balanced-match@1.0.2: {} @@ -4255,7 +4411,7 @@ snapshots: bl@6.0.16: dependencies: - '@types/readable-stream': 4.0.15 + '@types/readable-stream': 4.0.18 buffer: 6.0.3 inherits: 2.0.4 readable-stream: 4.5.2 @@ -4375,16 +4531,6 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - connect-gzip-static@3.0.1: - dependencies: - '@folder/readdir': 3.1.0 - debug: 4.3.7 - parseurl: 1.3.3 - send: 0.19.1 - serve-static: 1.16.2 - transitivePeerDependencies: - - supports-color - convert-source-map@2.0.0: {} core-js-compat@3.38.1: @@ -4393,13 +4539,13 @@ snapshots: core-util-is@1.0.3: {} - create-jest@29.7.0(@types/node@22.7.5): + create-jest@29.7.0(@types/node@22.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.7.5) + jest-config: 29.7.0(@types/node@22.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -4414,7 +4560,13 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debounce@2.1.1: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debounce@2.2.0: {} debug@2.6.9: dependencies: @@ -4480,41 +4632,41 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@9.1.0(eslint@9.12.0): + eslint-config-prettier@9.1.0(eslint@9.16.0): dependencies: - eslint: 9.12.0 + eslint: 9.16.0 - eslint-scope@8.1.0: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.1.0: {} + eslint-visitor-keys@4.2.0: {} - eslint@9.12.0: + eslint@9.16.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@eslint-community/regexpp': 4.11.1 - '@eslint/config-array': 0.18.0 - '@eslint/core': 0.6.0 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.12.0 - '@eslint/plugin-kit': 0.2.0 - '@humanfs/node': 0.16.5 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.16.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.16.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/retry': 0.4.1 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.1.0 - eslint-visitor-keys: 4.1.0 - espree: 10.2.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -4529,15 +4681,14 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.2.0: + espree@10.3.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.1.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esprima@4.0.1: {} @@ -4581,6 +4732,13 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + express-static-gzip@2.2.0: + dependencies: + parseurl: 1.3.3 + serve-static: 1.16.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} @@ -4814,7 +4972,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@babel/parser': 7.25.8 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -4824,8 +4982,8 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.25.8 - '@babel/parser': 7.25.8 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.3 @@ -4867,7 +5025,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -4887,16 +5045,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.7.5): + jest-cli@29.7.0(@types/node@22.10.1): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.7.5) + create-jest: 29.7.0(@types/node@22.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.7.5) + jest-config: 29.7.0(@types/node@22.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -4906,12 +5064,12 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.7.5): + jest-config@29.7.0(@types/node@22.10.1): dependencies: - '@babel/core': 7.25.8 + '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.25.8) + babel-jest: 29.7.0(@babel/core@7.26.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -4931,7 +5089,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -4960,7 +5118,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4970,7 +5128,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.7.5 + '@types/node': 22.10.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -5009,7 +5167,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -5044,7 +5202,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -5072,7 +5230,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 cjs-module-lexer: 1.4.1 collect-v8-coverage: 1.0.2 @@ -5092,15 +5250,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.25.8 - '@babel/generator': 7.25.7 - '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.25.8) - '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.25.8) - '@babel/types': 7.25.8 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.7(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.7(@babel/core@7.26.0) + '@babel/types': 7.26.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.25.8) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -5118,7 +5276,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -5137,7 +5295,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.10.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -5146,17 +5304,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.7.5 + '@types/node': 22.10.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.7.5): + jest@29.7.0(@types/node@22.10.1): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.7.5) + jest-cli: 29.7.0(@types/node@22.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -5230,7 +5388,7 @@ snapshots: lodash.merge@4.6.2: {} - logform@2.6.1: + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 '@types/triple-beam': 1.3.5 @@ -5296,7 +5454,7 @@ snapshots: moment@2.30.1: {} - mqtt-packet@9.0.0: + mqtt-packet@9.0.1: dependencies: bl: 6.0.16 debug: 4.3.7 @@ -5304,17 +5462,17 @@ snapshots: transitivePeerDependencies: - supports-color - mqtt@5.10.1: + mqtt@5.10.3: dependencies: - '@types/readable-stream': 4.0.15 - '@types/ws': 8.5.12 + '@types/readable-stream': 4.0.18 + '@types/ws': 8.5.13 commist: 3.2.0 concat-stream: 2.0.0 debug: 4.3.7 help-me: 5.0.0 lru-cache: 10.4.3 minimist: 1.2.8 - mqtt-packet: 9.0.0 + mqtt-packet: 9.0.1 number-allocator: 1.0.14 readable-stream: 4.5.2 reinterval: 1.1.0 @@ -5603,24 +5761,6 @@ snapshots: transitivePeerDependencies: - supports-color - send@0.19.1: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -5757,8 +5897,6 @@ snapshots: text-hex@1.0.0: {} - text-table@0.2.0: {} - throttleit@2.1.0: {} thunky@1.1.0: {} @@ -5777,9 +5915,9 @@ snapshots: triple-beam@1.4.1: {} - ts-api-utils@1.3.0(typescript@5.6.3): + ts-api-utils@1.3.0(typescript@5.7.2): dependencies: - typescript: 5.6.3 + typescript: 5.7.2 tslib@2.7.0: {} @@ -5793,20 +5931,20 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.8.1(eslint@9.12.0)(typescript@5.6.3): + typescript-eslint@8.16.0(eslint@9.16.0)(typescript@5.7.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 8.16.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.16.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.16.0(eslint@9.16.0)(typescript@5.7.2) + eslint: 9.16.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - typescript@5.6.3: {} + typescript@5.7.2: {} - undici-types@6.19.8: {} + undici-types@6.20.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -5853,34 +5991,34 @@ snapshots: dependencies: isexe: 2.0.0 - winston-syslog@2.7.1(winston@3.15.0): + winston-syslog@2.7.1(winston@3.17.0): dependencies: glossy: 0.1.7 triple-beam: 1.4.1 - winston: 3.15.0 - winston-transport: 4.8.0 + winston: 3.17.0 + winston-transport: 4.9.0 optionalDependencies: unix-dgram: 2.0.6 - winston-transport@4.8.0: + winston-transport@4.9.0: dependencies: - logform: 2.6.1 - readable-stream: 4.5.2 + logform: 2.7.0 + readable-stream: 3.6.2 triple-beam: 1.4.1 - winston@3.15.0: + winston@3.17.0: dependencies: '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.3 async: 3.2.6 is-stream: 2.0.1 - logform: 2.6.1 + logform: 2.7.0 one-time: 1.0.0 readable-stream: 3.6.2 safe-stable-stringify: 2.5.0 stack-trace: 0.0.10 triple-beam: 1.4.1 - winston-transport: 4.8.0 + winston-transport: 4.9.0 word-wrap@1.2.5: {} @@ -5962,7 +6100,7 @@ snapshots: '@serialport/parser-delimiter': 12.0.0 '@serialport/stream': 12.0.0 bonjour-service: 1.2.1 - debounce: 2.1.1 + debounce: 2.2.0 fast-deep-equal: 3.1.3 mixin-deep: 2.0.1 slip: 1.0.2 diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index a57c8ded26..d0dbeda603 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -422,13 +422,12 @@ describe('Extension: HomeAssistant', () => { ); payload = { - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], device: { identifiers: ['zigbee2mqtt_0x0017880104e45520'], manufacturer: 'Aqara', model: 'Wireless mini switch (WXKG11LM)', name: 'button', - sw_version: null, via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, event_types: ['single', 'double', 'triple', 'quadruple', 'hold', 'release'], @@ -436,7 +435,7 @@ describe('Extension: HomeAssistant', () => { json_attributes_topic: 'zigbee2mqtt/button', name: 'Action', object_id: 'button_action', - origin: origin, + origin, state_topic: 'zigbee2mqtt/button', unique_id: '0x0017880104e45520_action_zigbee2mqtt', // Needs to be updated whenever one of the ACTION_*_PATTERN constants changes. @@ -444,7 +443,7 @@ describe('Extension: HomeAssistant', () => { "{% set patterns = [\n{\"pattern\": '^(?P