diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f9f9c3611..2f1e8296f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,69 +14,98 @@ jobs: if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' with: # Required for `release: merge dev -> master and promote dev` - token: ${{secrets.GH_TOKEN}} + 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 - - name: Docker login + run: pnpm run test-with-coverage + + - name: Log in to the Docker container registry 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 - - name: Docker login ghcr.io + uses: docker/login-action@v3 + with: + username: koenkk + password: ${{ secrets.DOCKER_KEY }} + + - name: Log in to the GitHub container registry if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' - run: echo ${{ secrets.GH_TOKEN }} | docker login ghcr.io -u koenkk --password-stdin + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: koenkk + password: ${{ secrets.GH_TOKEN }} + - name: Docker setup - QEMU if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' uses: docker/setup-qemu-action@v3 with: platforms: all + - name: Docker setup - Buildx if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push' id: buildx uses: docker/setup-buildx-action@v3 with: version: latest - - name: 'dev: Docker build' + + - name: dev - Docker build and push if: github.ref == 'refs/heads/dev' && github.event_name == 'push' - run: | - docker buildx build \ - --build-arg COMMIT=$(git rev-parse --short HEAD) \ - --platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \ - -f docker/Dockerfile \ - --provenance=false \ - --push \ - -t koenkk/zigbee2mqtt:latest-dev -t ghcr.io/koenkk/zigbee2mqtt:latest-dev \ - . - - name: 'release: Docker build' + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + platforms: linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/riscv64,linux/386 + tags: koenkk/zigbee2mqtt:latest-dev,ghcr.io/koenkk/zigbee2mqtt:latest-dev + push: true + build-args: | + COMMIT=${{ github.sha }} + VERSION=dev + DATE=${{ github.event.repository.updated_at }} + + - name: release - Docker build and push if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' - run: | - TAG="$(git describe --tags)" - docker buildx build \ - --build-arg COMMIT=$(git rev-parse --short HEAD) \ - --platform linux/arm64/v8,linux/386,linux/amd64,linux/arm/v6,linux/arm/v7 \ - -f docker/Dockerfile \ - --provenance=false \ - --push \ - -t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" -t ghcr.io/koenkk/zigbee2mqtt:latest -t "ghcr.io/koenkk/zigbee2mqtt:$TAG" \ - . + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + provenance: false + platforms: linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/riscv64,linux/386 + tags: koenkk/zigbee2mqtt:latest,ghcr.io/koenkk/zigbee2mqtt:latest,koenkk/zigbee2mqtt:${{ github.ref_name }},ghcr.io/koenkk/zigbee2mqtt:${{ github.ref_name }} + push: true + build-args: | + COMMIT=${{ github.sha }} + VERSION=${{ github.ref_name }} + DATE=${{ github.event.repository.updated_at }} + - 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' if: github.ref == 'refs/heads/dev' && github.event_name == 'push' run: | @@ -86,6 +115,7 @@ jobs: -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/actions/workflows/ci.yml/dispatches \ -d '{"ref":"master","inputs":{}}' + - name: 'release: Trigger zigbee2mqtt/hassio-zigbee2mqtt build' if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' run: | @@ -98,6 +128,7 @@ jobs: -H "Content-Type: application/json" \ https://api.github.com/repos/zigbee2mqtt/hassio-zigbee2mqtt/dispatches \ --data "{\"event_type\": \"release\", \"client_payload\": { \"version\": \"$TAG-1\"}}" + - name: 'release: Trigger zigbee2mqtt-chart image update' if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' run: | @@ -110,6 +141,7 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/Koenkk/zigbee2mqtt-chart/actions/workflows/on_zigbee2mqtt_release.yaml/dispatches \ --data "{\"ref\": \"main\", \"inputs\": { \"zigbee2mqtt_version\": \"$TAG\"}}" + - name: 'release: merge dev -> master and promote dev' if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' run: | @@ -139,15 +171,21 @@ 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' + 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..327ee14d8a 100644 --- a/.github/workflows/release_please.yml +++ b/.github/workflows/release_please.yml @@ -16,6 +16,10 @@ jobs: release_created: ${{ steps.release.outputs.release_created }} version: '${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}' steps: + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 with: node-version: 20 @@ -52,7 +56,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/.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/.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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 007402a64c..a99c5313ac 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,24 +17,24 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community Examples of unacceptable behavior include: -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Enforcement Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f2e422786..02a5179308 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is written in JavaScript and is based upon [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters). Zigbee-herdsman-converters contains all device definitions, zigbee-herdsman is responsible for handling all communication with the adapter. -- 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`. -- 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). +- 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 `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..f1e5de1353 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because ### [Home Assistant](https://www.home-assistant.io/) -- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt) -- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html) +- [Home Assistant OS](https://www.home-assistant.io/installation/): Using [the official addon](https://github.com/zigbee2mqtt/hassio-zigbee2mqtt) +- Other installation: using instructions [here](https://www.zigbee2mqtt.io/guide/usage/integrations/home_assistant.html)
@@ -65,8 +65,8 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because ### [Homey](https://homey.app/) -- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/) -- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214) +- Integration implemented in the [Homey App](https://homey.app/nl-nl/app/com.gruijter.zigbee2mqtt/) +- Documentation and support in the [Homey Forum](https://community.homey.app/t/83214)
@@ -74,7 +74,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because ### [Domoticz](https://www.domoticz.com/) -- Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)). +- Integration implemented in Domoticz ([documentation](https://www.domoticz.com/wiki/Zigbee2MQTT)).
@@ -82,7 +82,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because ### [Gladys Assistant](https://gladysassistant.com/) -- Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)). +- Integration implemented natively in Gladys Assistant ([documentation](https://gladysassistant.com/docs/integrations/zigbee2mqtt/)).
@@ -90,7 +90,7 @@ Zigbee2MQTT integrates well with (almost) every home automation solution because ### [IoBroker](https://www.iobroker.net/) -- Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)). +- Integration implemented in IoBroker ([documentation](https://github.com/o0shojo0o/ioBroker.zigbee2mqtt)).
@@ -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.example.yaml b/data/configuration.example.yaml index 4af9322448..e442c66f75 100644 --- a/data/configuration.example.yaml +++ b/data/configuration.example.yaml @@ -1,8 +1,14 @@ +# Indicates the configuration version (used by configuration migrations) +version: 2 + # Home Assistant integration (MQTT discovery) -homeassistant: false +homeassistant: + enabled: false # Enable the frontend, runs on port 8080 by default -frontend: true +frontend: + enabled: true + # port: 8080 # MQTT settings mqtt: @@ -14,13 +20,24 @@ 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 + +# Periodically check whether devices are online/offline +# availability: +# enabled: false # Advanced settings advanced: + # channel: 11 # Let Zigbee2MQTT generate a network key on first start network_key: GENERATE # Let Zigbee2MQTT generate a pan_id on first start 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..9d805a5c10 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,33 +1,49 @@ -FROM alpine:3.18.4 as base +ARG TARGETPLATFORM +# Need to use Alpine 3.18.4 which uses Node 18 for arm/v6 and arm/v7, otherwise the build hangs. +# See https://github.com/nodejs/docker-node/issues/2077 +FROM alpine:3.18.4 AS linux-arm-alpine +FROM alpine:3.21 AS linux-arm64-alpine +FROM alpine:3.21 AS linux-amd64-alpine +FROM alpine:3.21 AS linux-riscv64-alpine +FROM alpine:3.21 AS linux-386-alpine + +FROM linux-${TARGETARCH}-alpine AS base + +ENV NODE_ENV=production WORKDIR /app RUN apk add --no-cache tzdata eudev tini nodejs # Dependencies and build -FROM base as dependencies_and_build +FROM base AS deps -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 && \ - # 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 +COPY package.json pnpm-lock.yaml ./ +# Make and such are needed to compile serialport for riscv64 +RUN apk add make gcc g++ python3 linux-headers npm && \ + npm install -g pnpm && \ + pnpm install --frozen-lockfile --no-optional && \ + # serialport has outdated prebuilds that appear to fail on some archs, force build on target platform + rm -rf `find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d` && \ + pnpm rebuild @serialport/bindings-cpp # Release -FROM base as release +FROM base AS release +ARG DATE +ARG VERSION LABEL org.opencontainers.image.authors="Koen Kanters" -LABEL org.opencontainers.image.title="zigbee2mqtt" +LABEL org.opencontainers.image.title="Zigbee2MQTT" LABEL org.opencontainers.image.description="Zigbee to MQTT bridge using Zigbee-herdsman" LABEL org.opencontainers.image.url="https://github.com/Koenkk/zigbee2mqtt" LABEL org.opencontainers.image.documentation="https://www.zigbee2mqtt.io/" LABEL org.opencontainers.image.source="https://github.com/Koenkk/zigbee2mqtt" +LABEL org.opencontainers.image.licenses="GPL-3.0" +LABEL org.opencontainers.image.created=${DATE} +LABEL org.opencontainers.image.version=${VERSION} -COPY --from=dependencies_and_build /app/node_modules ./node_modules +COPY --from=deps /app/node_modules ./node_modules COPY dist ./dist -COPY package.json LICENSE index.js data/configuration.yaml data/configuration.example.yaml ./ +COPY package.json LICENSE index.js data/configuration.example.yaml ./ COPY docker/docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh @@ -37,7 +53,5 @@ RUN mkdir /app/data ARG COMMIT RUN echo "$COMMIT" > dist/.hash -ENV NODE_ENV production - ENTRYPOINT ["docker-entrypoint.sh"] CMD [ "/sbin/tini", "--", "node", "index.js"] diff --git a/index.js b/index.js index c728bb8f60..244b7b9628 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'); @@ -129,6 +129,13 @@ async function start() { settings.reRead(); + // gc + { + const settingsMigration = require('./dist/util/settingsMigration'); + + settingsMigration.migrateIfNecessary(); + } + const errors = settings.validate(); if (errors.length > 0) { @@ -148,7 +155,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 cd2a2f3f09..afeebe4fe8 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -1,6 +1,9 @@ +import type {IClientPublishOptions} from 'mqtt'; import type * as SdNotify from 'sd-notify'; -import assert from 'assert'; +import type {Zigbee2MQTTAPI} from './types/api'; + +import assert from 'node:assert'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -14,15 +17,11 @@ import ExtensionBind from './extension/bind'; import ExtensionBridge from './extension/bridge'; import ExtensionConfigure from './extension/configure'; import ExtensionExternalConverters from './extension/externalConverters'; -import ExtensionExternalExtension from './extension/externalExtension'; +import ExtensionExternalExtensions from './extension/externalExtensions'; // Extensions 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,20 +40,16 @@ const AllExtensions = [ ExtensionPublish, ExtensionReceive, ExtensionNetworkMap, - ExtensionSoftReset, ExtensionHomeAssistant, ExtensionConfigure, - ExtensionDeviceGroupMembership, - ExtensionBridgeLegacy, ExtensionBridge, ExtensionGroups, ExtensionBind, - ExtensionReport, ExtensionOnEvent, ExtensionOTAUpdate, ExtensionExternalConverters, ExtensionFrontend, - ExtensionExternalExtension, + ExtensionExternalExtensions, ExtensionAvailability, ]; @@ -104,41 +99,27 @@ export class Controller { ]; this.extensions = [ + new ExtensionExternalConverters(...this.extensionArgs), new ExtensionOnEvent(...this.extensionArgs), 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 ExtensionExternalExtensions(...this.extensionArgs), new ExtensionAvailability(...this.extensionArgs), ]; - if (settings.get().frontend) { + if (settings.get().frontend.enabled) { 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)); - } - - if (settings.get().homeassistant) { + if (settings.get().homeassistant.enabled) { 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,27 +137,21 @@ 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'); + 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/24364'); + } 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; @@ -192,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(); @@ -293,7 +255,7 @@ export class Controller { } @bind async publishEntityState(entity: Group | Device, payload: KeyValue, stateChangeReason?: StateChangeReason): Promise { - let message = {...payload}; + let message: Zigbee2MQTTAPI['{friendlyName}'] = {...payload}; // Update state cache with new state. const newState = this.state.set(entity, payload, stateChangeReason); @@ -303,14 +265,14 @@ export class Controller { message = newState; } - const options: MQTTOptions = { - retain: utils.getObjectProperty(entity.options, 'retain', false) as boolean, - qos: utils.getObjectProperty(entity.options, 'qos', 0) as 0 | 1 | 2, + const options: IClientPublishOptions = { + retain: utils.getObjectProperty(entity.options, 'retain', false), + qos: utils.getObjectProperty(entity.options, 'qos', 0), }; + const retention = utils.getObjectProperty(entity.options, 'retention', false); - const retention = utils.getObjectProperty(entity.options, 'retention', false); if (retention !== false) { - options.properties = {messageExpiryInterval: retention as number}; + options.properties = {messageExpiryInterval: retention}; } if (entity.isDevice() && settings.get().mqtt.include_device_information) { @@ -352,7 +314,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); @@ -366,7 +328,7 @@ export class Controller { this.eventBus.emitPublishEntityState({entity, message, stateChangeReason, payload}); } - async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: MQTTOptions): Promise { + async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: IClientPublishOptions): Promise { for (const [key, value] of Object.entries(payload)) { let subPayload = value; let message = null; @@ -404,5 +366,3 @@ export class Controller { } } } - -module.exports = Controller; diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 13110547c9..33c695947c 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -1,4 +1,4 @@ -import events from 'events'; +import events from 'node:events'; import logger from './util/logger'; @@ -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..82e33a99d7 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -1,4 +1,6 @@ -import assert from 'assert'; +import type {Zigbee2MQTTAPI} from 'lib/types/api'; + +import assert from 'node:assert'; import bind from 'bind-decorator'; import debounce from 'debounce'; @@ -29,14 +31,9 @@ export default class Availability extends Extension { return utils.minutes(device.options.availability.timeout); } - const key = this.isActiveDevice(device) ? 'active' : 'passive'; - let value = settings.get().availability?.[key]?.timeout; - - if (value == null) { - value = key == 'active' ? 10 : 1500; - } + const type = this.isActiveDevice(device) ? 'active' : 'passive'; - return utils.minutes(value); + return utils.minutes(settings.get().availability[type].timeout); } private isActiveDevice(device: Device): boolean { @@ -140,7 +137,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,9 +185,9 @@ export default class Availability extends Extension { } const topic = `${entity.name}/availability`; - const payload = utils.availabilityPayload(available ? 'online' : 'offline', settings.get()); + const payload: Zigbee2MQTTAPI['{friendlyName}/availability'] = {state: available ? 'online' : 'offline'}; this.availabilityCache[entity.ID] = available; - await this.mqtt.publish(topic, payload, {retain: true, qos: 1}); + await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 1}); if (!skipGroups && entity.isDevice()) { for (const group of this.zigbee.groupsIterator()) { diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 1ddbaeefc3..8a0808235a 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -1,4 +1,6 @@ -import assert from 'assert'; +import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from 'lib/types/api'; + +import assert from 'node:assert'; import bind from 'bind-decorator'; import debounce from 'debounce'; @@ -14,8 +16,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', @@ -197,17 +197,16 @@ 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; -} - -interface DataMessage { - from: ParsedMQTTMessage['sourceKey']; - to: ParsedMQTTMessage['targetKey']; - clusters: ParsedMQTTMessage['clusters']; - skip_disable_reporting?: ParsedMQTTMessage['skipDisableReporting']; + resolvedSource?: Device; + resolvedTarget?: Device | Group | typeof DEFAULT_BIND_GROUP; + resolvedSourceEndpoint?: zh.Endpoint; + resolvedBindTarget?: number | zh.Endpoint | zh.Group; } export default class Bind extends Extension { @@ -219,158 +218,204 @@ 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; - - 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)) { - type = data.topic.endsWith('unbind') ? 'unbind' : 'bind'; - const message: DataMessage = JSON.parse(data.message); - sourceKey = message.from; - targetKey = message.to; - clusters = message.clusters; - skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false; - } else { - return undefined; - } + private parseMQTTMessage( + data: eventdata.MQTTMessage, + ): [raw: KeyValue | undefined, parsed: ParsedMQTTMessage | undefined, error: string | undefined] { + if (data.topic.match(TOPIC_REGEX)) { + const type = data.topic.endsWith('unbind') ? 'unbind' : 'bind'; + let skipDisableReporting = false; + const message = JSON.parse(data.message) as Zigbee2MQTTAPI['bridge/request/device/bind']; - return {type, sourceKey, targetKey, clusters, skipDisableReporting}; - } + if (typeof message !== 'object' || message.from == undefined || message.to == undefined) { + return [message, {type, skipDisableReporting}, `Invalid payload`]; + } - @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const parsed = this.parseMQTTMessage(data); + 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; + const resolvedSource = this.zigbee.resolveEntity(message.from) as Device; - if (!parsed || !parsed.type) { - return; - } + if (!resolvedSource || !(resolvedSource instanceof Device)) { + return [message, {type, skipDisableReporting}, `Source device '${message.from}' 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 resolvedTarget = message.to === DEFAULT_BIND_GROUP.name ? DEFAULT_BIND_GROUP : this.zigbee.resolveEntity(message.to); - const bindSource = parsedSource.endpoint; - const bindTarget = target instanceof Device ? parsedTarget.endpoint : target instanceof Group ? target.zh : Number(target.ID); + if (!resolvedTarget) { + return [message, {type, skipDisableReporting}, `Target device or group '${message.to}' does not exist`]; + } - assert(bindSource != undefined && bindTarget != undefined); + const resolvedSourceEndpoint = resolvedSource.endpoint(sourceEndpointKey); - // Find which clusters are supported by both the source and target. - // Groups are assumed to support all clusters. - const clusterCandidates = clusters ?? ALL_CLUSTER_CANDIDATES; + if (!resolvedSourceEndpoint) { + return [ + message, + {type, skipDisableReporting}, + `Source device '${resolvedSource.name}' does not have endpoint '${sourceEndpointKey}'`, + ]; + } - for (const cluster of clusterCandidates) { - let matchingClusters = false; + // 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}'`, + ]; + } - const anyClusterValid = utils.isZHGroup(bindTarget) || typeof bindTarget === 'number' || (target as Device).zh.type === 'Coordinator'; + return [ + message, + { + type, + sourceKey, + sourceEndpointKey, + targetKey, + targetEndpointKey, + clusters, + skipDisableReporting, + resolvedSource, + resolvedTarget, + resolvedSourceEndpoint, + resolvedBindTarget, + }, + undefined, + ]; + } else { + return [undefined, undefined, undefined]; + } + } - if (!anyClusterValid && utils.isZHEndpoint(bindTarget)) { - matchingClusters = - (bindTarget.supportsInputCluster(cluster) && bindSource.supportsOutputCluster(cluster)) || - (bindSource.supportsInputCluster(cluster) && bindTarget.supportsOutputCluster(cluster)); - } + @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { + const [raw, parsed, error] = this.parseMQTTMessage(data); - const sourceValid = bindSource.supportsInputCluster(cluster) || bindSource.supportsOutputCluster(cluster); + if (!raw || !parsed) { + return; + } - if (sourceValid && (anyClusterValid || matchingClusters)) { - logger.debug(`${type}ing cluster '${cluster}' from '${source.name}' to '${target.name}'`); - attemptedClusters.push(cluster); + if (error) { + await this.publishResponse(parsed.type, raw, {}, error); + return; + } - try { - if (type === 'bind') { - await bindSource.bind(cluster, bindTarget); - } else { - await bindSource.unbind(cluster, bindTarget); - } + 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)); + } - successfulClusters.push(cluster); - logger.info( - `Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from '${source.name}' to '${target.name}'`, - ); + const sourceValid = resolvedSourceEndpoint.supportsInputCluster(cluster) || resolvedSourceEndpoint.supportsOutputCluster(cluster); - /* 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})`); + if (sourceValid && (anyClusterValid || matchingClusters)) { + logger.debug(`${type}ing cluster '${cluster}' from '${resolvedSource.name}' to '${resolvedTarget.name}'`); + attemptedClusters.push(cluster); - /* 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}}), - ); - } + 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}`; - /* 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}})); + 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})`); } - } else if (failedClusters.length === attemptedClusters.length) { - error = `Failed to ${type}`; } + } - 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: Zigbee2MQTTAPI['bridge/response/device/bind'] | Zigbee2MQTTAPI['bridge/response/device/unbind'] = { + from: sourceKey!, // valid with assert above on `resolvedSource` + from_endpoint: sourceEndpointKey!, // valid with assert above on `resolvedSourceEndpoint` + to: targetKey!, // valid with assert above on `resolvedTarget` + 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 triggeredViaLegacyApi = data.topic.match(LEGACY_TOPIC_REGEX); - - if (!triggeredViaLegacyApi) { - 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: Zigbee2MQTTAPI[T], + error?: string, + ): Promise { + const response = utils.getResponse(request, data, error); + await this.mqtt.publish(`bridge/response/device/${type}`, stringify(response)); if (error) { logger.error(error); - } else { - this.eventBus.emitDevicesChanged(); } } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 6187e53619..71fc6c9b68 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -1,4 +1,6 @@ -import fs from 'fs'; +import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from 'lib/types/api'; + +import fs from 'node:fs'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -9,7 +11,6 @@ import Transport from 'winston-transport'; import * as zhc from 'zigbee-herdsman-converters'; import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster'; -import {ClusterDefinition, ClusterName, CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; import Device from '../model/device'; import Group from '../model/group'; @@ -19,64 +20,41 @@ import * as settings from '../util/settings'; import utils from '../util/utils'; import Extension from './extension'; -const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); - -type DefinitionPayload = { - model: string; - vendor: string; - description: string; - exposes: zhc.Expose[]; - supports_ota: boolean; - icon: string; - options: zhc.Option[]; -}; +const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); export default class Bridge extends Extension { - // @ts-expect-error initialized in `start` - private zigbee2mqttVersion: {commitHash?: string; version: string}; - // @ts-expect-error initialized in `start` - private zigbeeHerdsmanVersion: {version: string}; - // @ts-expect-error initialized in `start` - private zigbeeHerdsmanConvertersVersion: {version: string}; - // @ts-expect-error initialized in `start` - private coordinatorVersion: zh.CoordinatorVersion; + private zigbee2mqttVersion!: {commitHash?: string; version: string}; + private zigbeeHerdsmanVersion!: {version: string}; + private zigbeeHerdsmanConvertersVersion!: {version: string}; + private coordinatorVersion!: zh.CoordinatorVersion; private restartRequired = false; private lastJoinedDeviceIeeeAddr?: string; private lastBridgeLoggingPayload?: string; - // @ts-expect-error initialized in `start` - private logTransport: winston.transport; - // @ts-expect-error initialized in `start` - private requestLookup: {[key: string]: (message: KeyValue | string) => Promise}; + private logTransport!: winston.transport; + private requestLookup: {[key: string]: (message: KeyValue | string) => Promise>} = { + 'device/options': this.deviceOptions, + 'device/configure_reporting': this.deviceConfigureReporting, + 'device/remove': this.deviceRemove, + 'device/interview': this.deviceInterview, + 'device/generate_external_definition': this.deviceGenerateExternalDefinition, + 'device/rename': this.deviceRename, + 'group/add': this.groupAdd, + 'group/options': this.groupOptions, + 'group/remove': this.groupRemove, + 'group/rename': this.groupRename, + permit_join: this.permitJoin, + restart: this.restart, + backup: this.backup, + 'touchlink/factory_reset': this.touchlinkFactoryReset, + 'touchlink/identify': this.touchlinkIdentify, + 'install_code/add': this.installCodeAdd, + 'touchlink/scan': this.touchlinkScan, + health_check: this.healthCheck, + coordinator_check: this.coordinatorCheck, + options: this.bridgeOptions, + }; override async start(): Promise { - this.requestLookup = { - 'device/options': this.deviceOptions, - 'device/configure_reporting': this.deviceConfigureReporting, - 'device/remove': this.deviceRemove, - 'device/interview': this.deviceInterview, - 'device/generate_external_definition': this.deviceGenerateExternalDefinition, - 'device/rename': this.deviceRename, - 'group/add': this.groupAdd, - 'group/options': this.groupOptions, - 'group/remove': this.groupRemove, - 'group/rename': this.groupRename, - permit_join: this.permitJoin, - restart: this.restart, - backup: this.backup, - 'touchlink/factory_reset': this.touchlinkFactoryReset, - 'touchlink/identify': this.touchlinkIdentify, - 'install_code/add': this.installCodeAdd, - 'touchlink/scan': this.touchlinkScan, - 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; const baseTopic = settings.get().mqtt.base_topic; @@ -91,7 +69,7 @@ export default class Bridge extends Extension { if (debugToMQTTFrontend) { class DebugEventTransport extends Transport { - log(info: {message: string; level: string; namespace: string}, next: () => void): void { + override log(info: {message: string; level: string; namespace: string}, next: () => void): void { bridgeLogging(info.message, info.level, info.namespace); next(); } @@ -100,7 +78,7 @@ export default class Bridge extends Extension { this.logTransport = new DebugEventTransport(); } else { class EventTransport extends Transport { - log(info: {message: string; level: string; namespace: string}, next: () => void): void { + override log(info: {message: string; level: string; namespace: string}, next: () => void): void { if (info.level !== 'debug') { bridgeLogging(info.message, info.level, info.namespace); } @@ -140,35 +118,62 @@ export default class Bridge extends Extension { }); // Zigbee events - const publishEvent = async (type: string, data: KeyValue): Promise => - await this.mqtt.publish('bridge/event', stringify({type, data}), {retain: false, qos: 0}); this.eventBus.onDeviceJoined(this, async (data) => { this.lastJoinedDeviceIeeeAddr = data.device.ieeeAddr; await this.publishDevices(); - await publishEvent('device_joined', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); + + const payload: Zigbee2MQTTAPI['bridge/event'] = { + type: 'device_joined', + data: {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}, + }; + + await this.mqtt.publish('bridge/event', stringify(payload), {retain: false, qos: 0}); }); this.eventBus.onDeviceLeave(this, async (data) => { await this.publishDevices(); await this.publishDefinitions(); - await publishEvent('device_leave', {ieee_address: data.ieeeAddr, friendly_name: data.name}); + + const payload: Zigbee2MQTTAPI['bridge/event'] = {type: 'device_leave', data: {ieee_address: data.ieeeAddr, friendly_name: data.name}}; + + await this.mqtt.publish('bridge/event', stringify(payload), {retain: false, qos: 0}); }); this.eventBus.onDeviceNetworkAddressChanged(this, async () => { await this.publishDevices(); }); this.eventBus.onDeviceInterview(this, async (data) => { await this.publishDevices(); - const payload: KeyValue = {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr}; + + let payload: Zigbee2MQTTAPI['bridge/event']; if (data.status === 'successful') { - payload.supported = data.device.isSupported; - payload.definition = this.getDefinitionPayload(data.device); + payload = { + type: 'device_interview', + data: { + friendly_name: data.device.name, + status: data.status, + ieee_address: data.device.ieeeAddr, + supported: data.device.isSupported, + definition: this.getDefinitionPayload(data.device), + }, + }; + } else { + payload = { + type: 'device_interview', + data: {friendly_name: data.device.name, status: data.status, ieee_address: data.device.ieeeAddr}, + }; } - await publishEvent('device_interview', payload); + await this.mqtt.publish('bridge/event', stringify(payload), {retain: false, qos: 0}); }); this.eventBus.onDeviceAnnounce(this, async (data) => { await this.publishDevices(); - await publishEvent('device_announce', {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}); + + const payload: Zigbee2MQTTAPI['bridge/event'] = { + type: 'device_announce', + data: {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr}, + }; + + await this.mqtt.publish('bridge/event', stringify(payload), {retain: false, qos: 0}); }); await this.publishInfo(); @@ -185,7 +190,7 @@ export default class Bridge extends Extension { } @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const match = data.topic.match(requestRegex); + const match = data.topic.match(REQUEST_REGEX); if (!match) { return; @@ -212,30 +217,25 @@ export default class Bridge extends Extension { * Requests */ - @bind async deviceOptions(message: KeyValue | string): Promise { + @bind async deviceOptions(message: KeyValue | string): Promise> { return await this.changeEntityOptions('device', message); } - @bind async groupOptions(message: KeyValue | string): Promise { + @bind async groupOptions(message: KeyValue | string): Promise> { return await this.changeEntityOptions('group', message); } - @bind async bridgeOptions(message: KeyValue | string): Promise { + @bind async bridgeOptions(message: KeyValue | string): Promise> { if (typeof message !== 'object' || typeof message.options !== 'object') { throw new Error(`Invalid payload`); } - const newSettings = message.options; - const restartRequired = settings.apply(newSettings); - if (restartRequired) this.restartRequired = true; + const newSettings = message.options as Partial; + this.restartRequired = settings.apply(newSettings); // 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'); + if (newSettings.homeassistant) { + await this.enableDisableExtension(settings.get().homeassistant.enabled, 'HomeAssistant'); } if (newSettings.advanced?.log_level != undefined) { @@ -255,19 +255,19 @@ export default class Bridge extends Extension { return utils.getResponse(message, {restart_required: this.restartRequired}); } - @bind async deviceRemove(message: string | KeyValue): Promise { + @bind async deviceRemove(message: string | KeyValue): Promise> { return await this.removeEntity('device', message); } - @bind async groupRemove(message: string | KeyValue): Promise { + @bind async groupRemove(message: string | KeyValue): Promise> { return await this.removeEntity('group', message); } - @bind async healthCheck(message: string | KeyValue): Promise { + @bind async healthCheck(message: string | KeyValue): Promise> { return utils.getResponse(message, {healthy: true}); } - @bind async coordinatorCheck(message: string | KeyValue): Promise { + @bind async coordinatorCheck(message: string | KeyValue): Promise> { const result = await this.zigbee.coordinatorCheck(); const missingRouters = result.missingRouters.map((d) => { return {ieee_address: d.ieeeAddr, friendly_name: d.name}; @@ -275,7 +275,7 @@ export default class Bridge extends Extension { return utils.getResponse(message, {missing_routers: missingRouters}); } - @bind async groupAdd(message: string | KeyValue): Promise { + @bind async groupAdd(message: string | KeyValue): Promise> { if (typeof message === 'object' && message.friendly_name === undefined) { throw new Error(`Invalid payload`); } @@ -288,22 +288,22 @@ export default class Bridge extends Extension { return utils.getResponse(message, {friendly_name: group.friendly_name, id: group.ID}); } - @bind async deviceRename(message: string | KeyValue): Promise { + @bind async deviceRename(message: string | KeyValue): Promise> { return await this.renameEntity('device', message); } - @bind async groupRename(message: string | KeyValue): Promise { + @bind async groupRename(message: string | KeyValue): Promise> { return await this.renameEntity('group', message); } - @bind async restart(message: string | KeyValue): Promise { + @bind async restart(message: string | KeyValue): Promise> { // Wait 500 ms before restarting so response can be send. setTimeout(this.restartCallback, 500); logger.info('Restarting Zigbee2MQTT'); return utils.getResponse(message, {}); } - @bind async backup(message: string | KeyValue): Promise { + @bind async backup(message: string | KeyValue): Promise> { await this.zigbee.backup(); const dataPath = data.getPath(); const files = utils @@ -316,7 +316,7 @@ export default class Bridge extends Extension { return utils.getResponse(message, {zip: base64Zip}); } - @bind async installCodeAdd(message: KeyValue | string): Promise { + @bind async installCodeAdd(message: KeyValue | string): Promise> { if (typeof message === 'object' && message.value === undefined) { throw new Error('Invalid payload'); } @@ -327,18 +327,16 @@ export default class Bridge extends Extension { return utils.getResponse(message, {value}); } - @bind async permitJoin(message: KeyValue | string): Promise { - if (typeof message === 'object' && message.value === undefined) { - throw new Error('Invalid payload'); - } - - let value: boolean | string; + @bind async permitJoin(message: KeyValue | string): Promise> { 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); @@ -350,83 +348,21 @@ 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); } - // 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 { + @bind async touchlinkIdentify(message: KeyValue | string): Promise> { if (typeof message !== 'object' || message.ieee_address === undefined || message.channel === undefined) { throw new Error('Invalid payload'); } @@ -436,14 +372,18 @@ export default class Bridge extends Extension { return utils.getResponse(message, {ieee_address: message.ieee_address, channel: message.channel}); } - @bind async touchlinkFactoryReset(message: KeyValue | string): Promise { + @bind async touchlinkFactoryReset(message: KeyValue | string): Promise> { let result = false; - const payload: {ieee_address?: string; channel?: number} = {}; + let payload: Zigbee2MQTTAPI['bridge/response/touchlink/factory_reset'] = {}; + if (typeof message === 'object' && message.ieee_address !== undefined && message.channel !== undefined) { logger.info(`Start Touchlink factory reset of '${message.ieee_address}' on channel ${message.channel}`); + result = await this.zigbee.touchlinkFactoryReset(message.ieee_address, message.channel); - payload.ieee_address = message.ieee_address; - payload.channel = message.channel; + payload = { + ieee_address: message.ieee_address, + channel: message.channel, + }; } else { logger.info('Start Touchlink factory reset of first found device'); result = await this.zigbee.touchlinkFactoryResetFirst(); @@ -458,7 +398,7 @@ export default class Bridge extends Extension { } } - @bind async touchlinkScan(message: KeyValue | string): Promise { + @bind async touchlinkScan(message: KeyValue | string): Promise> { logger.info('Start Touchlink scan'); const result = await this.zigbee.touchlinkScan(); const found = result.map((r) => { @@ -472,19 +412,10 @@ 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 { + async changeEntityOptions( + entityType: T, + message: KeyValue | string, + ): Promise> { if (typeof message !== 'object' || message.id === undefined || message.options === undefined) { throw new Error(`Invalid payload`); } @@ -512,10 +443,11 @@ export default class Bridge extends Extension { return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired}); } - @bind async deviceConfigureReporting(message: string | KeyValue): Promise { + @bind async deviceConfigureReporting(message: string | KeyValue): Promise> { if ( typeof message !== 'object' || message.id === undefined || + message.endpoint === undefined || message.cluster === undefined || message.maximum_report_interval === undefined || message.minimum_report_interval === undefined || @@ -525,14 +457,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(); @@ -557,6 +486,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, @@ -565,12 +495,12 @@ export default class Bridge extends Extension { }); } - @bind async deviceInterview(message: string | KeyValue): Promise { + @bind async deviceInterview(message: string | KeyValue): Promise> { if (typeof message !== 'object' || message.id === undefined) { 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 { @@ -588,23 +518,23 @@ export default class Bridge extends Extension { return utils.getResponse(message, {id: message.id}); } - @bind async deviceGenerateExternalDefinition(message: string | KeyValue): Promise { + @bind async deviceGenerateExternalDefinition( + message: string | KeyValue, + ): Promise> { if (typeof message !== 'object' || message.id === undefined) { 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}); } - async renameEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise { + async renameEntity( + entityType: T, + message: string | KeyValue, + ): Promise> { const deviceAndHasLast = entityType === 'device' && typeof message === 'object' && message.last === true; if (typeof message !== 'object' || (message.from === undefined && !deviceAndHasLast) || message.to === undefined) { @@ -641,7 +571,10 @@ export default class Bridge extends Extension { return utils.getResponse(message, {from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename}); } - async removeEntity(entityType: 'group' | 'device', message: string | KeyValue): Promise { + async removeEntity( + entityType: T, + message: string | KeyValue, + ): Promise> { const ID = typeof message === 'object' ? message.id : message.trim(); const entity = this.getEntity(entityType, ID); const friendlyName = entity.name; @@ -674,25 +607,17 @@ export default class Bridge extends Extension { } else { await entity.zh.removeFromNetwork(); } + + this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'}); + settings.removeDevice(entityID as string); } else { if (force) { entity.zh.removeFromDatabase(); } else { await entity.zh.removeFromNetwork(); } - } - // Fire event - if (entity instanceof Device) { - this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'}); - } else { this.eventBus.emitEntityRemoved({id: entityID, name, type: 'group'}); - } - - // Remove from configuration.yaml - if (entity instanceof Device) { - settings.removeDevice(entityID as string); - } else { settings.removeGroup(entityID); } @@ -709,16 +634,29 @@ export default class Bridge extends Extension { await this.publishDevices(); // Refresh Cluster definition await this.publishDefinitions(); - return utils.getResponse(message, {id: ID, block, force}); + + const responseData: Zigbee2MQTTAPI['bridge/response/device/remove'] = {id: ID, block, force}; + + return utils.getResponse(message, responseData); } else { await this.publishGroups(); - return utils.getResponse(message, {id: ID, force: force}); + + const responseData: Zigbee2MQTTAPI['bridge/response/group/remove'] = {id: ID, force}; + + return utils.getResponse( + message, + // @ts-expect-error typing infer does not work here + responseData, + ); } } catch (error) { throw new Error(`Failed to remove ${entityType} '${friendlyName}'${blockForceLog} (${error})`); } } + 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) { @@ -732,12 +670,10 @@ export default class Bridge extends Extension { // @ts-expect-error hidden from publish delete config.advanced.network_key; delete config.mqtt.password; + delete config.frontend.auth_token; - if (config.frontend) { - delete config.frontend.auth_token; - } - - const payload = { + const networkParams = await this.zigbee.getNetworkParameters(); + const payload: Zigbee2MQTTAPI['bridge/info'] = { version: this.zigbee2mqttVersion.version, commit: this.zigbee2mqttVersion.commitHash, zigbee_herdsman_converters: this.zigbeeHerdsmanConvertersVersion, @@ -746,40 +682,30 @@ export default class Bridge extends Extension { ieee_address: this.zigbee.firstCoordinatorEndpoint().getDevice().ieeeAddr, ...this.coordinatorVersion, }, - network: utils.toSnakeCaseObject(await this.zigbee.getNetworkParameters()), + network: { + pan_id: networkParams.panID, + extended_pan_id: networkParams.extendedPanID, + channel: networkParams.channel, + }, log_level: logger.getLevel(), permit_join: this.zigbee.getPermitJoin(), - permit_join_timeout: this.zigbee.getPermitJoinTimeout(), + permit_join_end: this.zigbee.getPermitJoinEnd(), restart_required: this.restartRequired, config, - config_schema: settings.schema, + config_schema: settings.schemaJson, }; await this.mqtt.publish('bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } async publishDevices(): Promise { - interface Data { - bindings: {cluster: string; target: {type: string; endpoint?: number; ieee_address?: string; id?: number}}[]; - configured_reportings: { - cluster: string; - attribute: string | number; - minimum_report_interval: number; - maximum_report_interval: number; - reportable_change: number; - }[]; - clusters: {input: string[]; output: string[]}; - scenes: Scene[]; - } - - // XXX: definition<>DefinitionPayload don't match to use `Device[]` type here - const devices: KeyValue[] = []; + const devices: Zigbee2MQTTAPI['bridge/devices'] = []; for (const device of this.zigbee.devicesIterator()) { - const endpoints: {[s: number]: Data} = {}; + const endpoints: (typeof devices)[number]['endpoints'] = {}; for (const endpoint of device.zh.endpoints) { - const data: Data = { + const data: (typeof endpoints)[keyof typeof endpoints] = { scenes: utils.getScenes(endpoint), bindings: [], configured_reportings: [], @@ -833,8 +759,7 @@ export default class Bridge extends Extension { } async publishGroups(): Promise { - // XXX: id<>ID can't use `Group[]` type - const groups: KeyValue[] = []; + const groups: Zigbee2MQTTAPI['bridge/groups'] = []; for (const group of this.zigbee.groupsIterator()) { const members = []; @@ -856,12 +781,7 @@ export default class Bridge extends Extension { } async publishDefinitions(): Promise { - interface ClusterDefinitionPayload { - clusters: Readonly>>; - custom_clusters: {[key: string]: CustomClusters}; - } - - const data: ClusterDefinitionPayload = { + const data: Zigbee2MQTTAPI['bridge/definition'] = { clusters: Clusters, custom_clusters: {}, }; @@ -873,7 +793,7 @@ export default class Bridge extends Extension { await this.mqtt.publish('bridge/definitions', stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } - getDefinitionPayload(device: Device): DefinitionPayload | undefined { + getDefinitionPayload(device: Device): Zigbee2MQTTDevice['definition'] | undefined { if (!device.definition) { return undefined; } @@ -889,7 +809,7 @@ export default class Bridge extends Extension { icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model)); } - const payload: DefinitionPayload = { + const payload: Zigbee2MQTTDevice['definition'] = { model: device.definition.model, vendor: device.definition.vendor, description: device.definition.description, diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 3068906340..00469f648e 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -1,3 +1,5 @@ +import type {Zigbee2MQTTAPI} from 'lib/types/api'; + import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -16,7 +18,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,38 +30,31 @@ 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) { - const message = utils.parseJSON(data.message, data.message); - const ID = typeof message === 'object' && message.id !== undefined ? message.id : message; + if (data.topic === this.topic) { + const message = utils.parseJSON(data.message, data.message) as Zigbee2MQTTAPI['bridge/request/device/configure']; + const ID = typeof message === 'object' ? message.id : message; let error: string | undefined; - const device = this.zigbee.resolveEntity(ID); - if (!device || !(device instanceof Device)) { - error = `Device '${ID}' does not exist`; - } else if (!device.definition || !device.definition.configure) { - error = `Device '${device.name}' cannot be configured`; + if (ID === undefined) { + error = `Invalid payload`; } else { - try { - await this.configure(device, 'mqtt_message', true, true); - } catch (e) { - error = `Failed to configure (${(e as Error).message})`; + const device = this.zigbee.resolveEntity(ID); + + if (!device || !(device instanceof Device)) { + error = `Device '${ID}' does not exist`; + } else if (!device.definition || !device.definition.configure) { + error = `Device '${device.name}' cannot be configured`; + } else { + try { + await this.configure(device, 'mqtt_message', true, true); + } catch (e) { + error = `Failed to configure (${(e as Error).message})`; + } } } - const response = utils.getResponse(message, {id: ID}, error); + const response = utils.getResponse<'bridge/response/device/configure'>(message, {id: ID}, error); + await this.mqtt.publish(`bridge/response/device/configure`, stringify(response)); } } diff --git a/lib/extension/externalConverters.ts b/lib/extension/externalConverters.ts index 598d59a370..5c54bbbae2 100644 --- a/lib/extension/externalConverters.ts +++ b/lib/extension/externalConverters.ts @@ -1,11 +1,13 @@ -import * as zhc from 'zigbee-herdsman-converters'; +import type * as zhc from 'zigbee-herdsman-converters'; + +import {addDefinition, removeExternalDefinitions} from 'zigbee-herdsman-converters'; import logger from '../util/logger'; -import * as settings from '../util/settings'; -import {loadExternalConverter} from '../util/utils'; -import Extension from './extension'; +import ExternalJSExtension from './externalJS'; + +type ModuleExports = zhc.Definition | zhc.Definition[]; -export default class ExternalConverters extends Extension { +export default class ExternalConverters extends ExternalJSExtension { constructor( zigbee: Zigbee, mqtt: MQTT, @@ -16,27 +18,51 @@ export default class ExternalConverters extends Extension { restartCallback: () => Promise, addExtension: (extension: Extension) => Promise, ) { - super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); - - for (const file of settings.get().external_converters) { - try { - for (const definition of loadExternalConverter(file)) { - const toAdd = {...definition}; - delete toAdd['homeassistant']; - zhc.addDefinition(toAdd); - } - logger.info(`Loaded external converter '${file}'`); - } catch (error) { - logger.error(`Failed to load external converter file '${file}' (${(error as Error).message})`); - logger.error( - `Probably there is a syntax error in the file or the external converter is not ` + - `compatible with the current Zigbee2MQTT version`, - ); - logger.error( - `Note that external converters are not meant for long term usage, it's meant for local ` + - `testing after which a pull request should be created to add out-of-the-box support for the device`, - ); + super( + zigbee, + mqtt, + state, + publishEntityState, + eventBus, + enableDisableExtension, + restartCallback, + addExtension, + 'converter', + 'external_converters', + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async removeJS(name: string, module: ModuleExports): Promise { + removeExternalDefinitions(name); + + await this.zigbee.resolveDevicesDefinitions(true); + } + + protected async loadJS(name: string, module: ModuleExports): Promise { + try { + removeExternalDefinitions(name); + + for (const definition of this.getDefinitions(module)) { + definition.externalConverterName = name; + + addDefinition(definition); + logger.info(`Loaded external converter '${name}'.`); } + + await this.zigbee.resolveDevicesDefinitions(true); + } catch (error) { + logger.error(`Failed to load external converter '${name}'`); + logger.error(`Check the code for syntax error and make sure it is up to date with the current Zigbee2MQTT version.`); + logger.error( + `External converters are not meant for long term usage, but for local testing after which a pull request should be created to add out-of-the-box support for the device`, + ); + + throw error; } } + + private getDefinitions(module: ModuleExports): zhc.Definition[] { + return Array.isArray(module) ? module : [module]; + } } diff --git a/lib/extension/externalExtension.ts b/lib/extension/externalExtension.ts deleted file mode 100644 index 6df6568554..0000000000 --- a/lib/extension/externalExtension.ts +++ /dev/null @@ -1,120 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -import bind from 'bind-decorator'; -import stringify from 'json-stable-stringify-without-jsonify'; - -import * as settings from '../util/settings'; -import utils from '../util/utils'; -import data from './../util/data'; -import logger from './../util/logger'; -import Extension from './extension'; - -const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`); - -export default class ExternalExtension extends Extension { - private requestLookup: {[s: string]: (message: KeyValue) => Promise} = { - save: this.saveExtension, - remove: this.removeExtension, - }; - - override async start(): Promise { - this.eventBus.onMQTTMessage(this, this.onMQTTMessage); - await this.loadUserDefinedExtensions(); - await this.publishExtensions(); - } - - private getExtensionsBasePath(): string { - return data.joinPath('extension'); - } - - private getListOfUserDefinedExtensions(): {name: string; code: string}[] { - const basePath = this.getExtensionsBasePath(); - if (fs.existsSync(basePath)) { - return fs - .readdirSync(basePath) - .filter((f) => f.endsWith('.js')) - .map((fileName) => { - const extensionFilePath = path.join(basePath, fileName); - return {name: fileName, code: fs.readFileSync(extensionFilePath, 'utf-8')}; - }); - } else { - return []; - } - } - - @bind private async removeExtension(message: KeyValue): Promise { - const {name} = message; - const extensions = this.getListOfUserDefinedExtensions(); - const extensionToBeRemoved = extensions.find((e) => e.name === name); - - if (extensionToBeRemoved) { - await this.enableDisableExtension(false, extensionToBeRemoved.name); - const basePath = this.getExtensionsBasePath(); - const extensionFilePath = path.join(basePath, path.basename(name)); - fs.unlinkSync(extensionFilePath); - await this.publishExtensions(); - logger.info(`Extension ${name} removed`); - return utils.getResponse(message, {}); - } else { - return utils.getResponse(message, {}, `Extension ${name} doesn't exists`); - } - } - - @bind private async saveExtension(message: KeyValue): Promise { - const {name, code} = message; - const ModuleConstructor = utils.loadModuleFromText(code, name) as typeof Extension; - await this.loadExtension(ModuleConstructor); - const basePath = this.getExtensionsBasePath(); - /* istanbul ignore else */ - if (!fs.existsSync(basePath)) { - fs.mkdirSync(basePath); - } - const extensionFilePath = path.join(basePath, path.basename(name)); - fs.writeFileSync(extensionFilePath, code); - await this.publishExtensions(); - logger.info(`Extension ${name} loaded`); - return utils.getResponse(message, {}); - } - - @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const match = data.topic.match(requestRegex); - if (match && this.requestLookup[match[1].toLowerCase()]) { - const message = utils.parseJSON(data.message, data.message) as KeyValue; - try { - const response = await this.requestLookup[match[1].toLowerCase()](message); - await this.mqtt.publish(`bridge/response/extension/${match[1]}`, stringify(response)); - } catch (error) { - logger.error(`Request '${data.topic}' failed with error: '${(error as Error).message}'`); - const response = utils.getResponse(message, {}, `${(error as Error).message}`); - await this.mqtt.publish(`bridge/response/extension/${match[1]}`, stringify(response)); - } - } - } - - @bind private async loadExtension(ConstructorClass: typeof Extension): Promise { - await this.enableDisableExtension(false, ConstructorClass.name); - // @ts-expect-error `ConstructorClass` is the interface, not the actual passed class - await this.addExtension(new ConstructorClass(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus, settings, logger)); - } - - private async loadUserDefinedExtensions(): Promise { - for (const extension of this.getListOfUserDefinedExtensions()) { - await this.loadExtension(utils.loadModuleFromText(extension.code, extension.name) as typeof Extension); - } - } - - private async publishExtensions(): Promise { - const extensions = this.getListOfUserDefinedExtensions(); - await this.mqtt.publish( - 'bridge/extensions', - stringify(extensions), - { - retain: true, - qos: 0, - }, - settings.get().mqtt.base_topic, - true, - ); - } -} diff --git a/lib/extension/externalExtensions.ts b/lib/extension/externalExtensions.ts new file mode 100644 index 0000000000..3537617b34 --- /dev/null +++ b/lib/extension/externalExtensions.ts @@ -0,0 +1,59 @@ +import type Extension from './extension'; + +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import ExternalJSExtension from './externalJS'; + +type ModuleExports = typeof Extension; + +export default class ExternalExtensions extends ExternalJSExtension { + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + ) { + super( + zigbee, + mqtt, + state, + publishEntityState, + eventBus, + enableDisableExtension, + restartCallback, + addExtension, + 'extension', + 'external_extensions', + ); + } + + protected async removeJS(name: string, module: ModuleExports): Promise { + await this.enableDisableExtension(false, module.name); + } + + protected async loadJS(name: string, module: ModuleExports): Promise { + // stop if already started + await this.enableDisableExtension(false, module.name); + await this.addExtension( + // @ts-expect-error `module` is the interface, not the actual passed class + new module( + this.zigbee, + this.mqtt, + this.state, + this.publishEntityState, + this.eventBus, + this.enableDisableExtension, + this.restartCallback, + this.addExtension, + settings, + logger, + ), + ); + + logger.info(`Loaded external extension '${name}'.`); + } +} diff --git a/lib/extension/externalJS.ts b/lib/extension/externalJS.ts new file mode 100644 index 0000000000..438659de51 --- /dev/null +++ b/lib/extension/externalJS.ts @@ -0,0 +1,192 @@ +import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from 'lib/types/api'; + +import fs from 'node:fs'; +import path from 'node:path'; +import {Context, runInNewContext} from 'node:vm'; + +import bind from 'bind-decorator'; +import stringify from 'json-stable-stringify-without-jsonify'; + +import data from '../util/data'; +import logger from '../util/logger'; +import * as settings from '../util/settings'; +import utils from '../util/utils'; +import Extension from './extension'; + +const SUPPORTED_OPERATIONS = ['save', 'remove']; + +export default abstract class ExternalJSExtension extends Extension { + protected mqttTopic: string; + protected requestRegex: RegExp; + protected basePath: string; + + constructor( + zigbee: Zigbee, + mqtt: MQTT, + state: State, + publishEntityState: PublishEntityState, + eventBus: EventBus, + enableDisableExtension: (enable: boolean, name: string) => Promise, + restartCallback: () => Promise, + addExtension: (extension: Extension) => Promise, + mqttTopic: string, + folderName: string, + ) { + super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); + + this.mqttTopic = mqttTopic; + this.requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/${mqttTopic}/(save|remove)`); + this.basePath = data.joinPath(folderName); + } + + override async start(): Promise { + await super.start(); + this.eventBus.onMQTTMessage(this, this.onMQTTMessage); + await this.loadFiles(); + await this.publishExternalJS(); + } + + private getFilePath(name: string, mkBasePath: boolean = false): string { + if (mkBasePath && !fs.existsSync(this.basePath)) { + fs.mkdirSync(this.basePath, {recursive: true}); + } + + return path.join(this.basePath, name); + } + + protected getFileCode(name: string): string { + return fs.readFileSync(path.join(this.basePath, name), 'utf8'); + } + + protected *getFiles(): Generator<{name: string; code: string}> { + if (!fs.existsSync(this.basePath)) { + return; + } + + for (const fileName of fs.readdirSync(this.basePath)) { + /* istanbul ignore else */ + if (fileName.endsWith('.js')) { + yield {name: fileName, code: this.getFileCode(fileName)}; + } + } + } + + @bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise { + const match = data.topic.match(this.requestRegex); + + if (match && SUPPORTED_OPERATIONS.includes(match[1].toLowerCase())) { + const message = utils.parseJSON(data.message, data.message); + + try { + let response; + + if (match[1].toLowerCase() === 'save') { + response = await this.save( + message as Zigbee2MQTTAPI['bridge/request/converter/save'] | Zigbee2MQTTAPI['bridge/request/extension/save'], + ); + } else { + response = await this.remove( + message as Zigbee2MQTTAPI['bridge/request/converter/remove'] | Zigbee2MQTTAPI['bridge/request/extension/remove'], + ); + } + + await this.mqtt.publish(`bridge/response/${this.mqttTopic}/${match[1]}`, stringify(response)); + } catch (error) { + logger.error(`Request '${data.topic}' failed with error: '${(error as Error).message}'`); + + const response = utils.getResponse(message, {}, `${(error as Error).message}`); + + await this.mqtt.publish(`bridge/response/${this.mqttTopic}/${match[1]}`, stringify(response)); + } + } + } + + protected abstract removeJS(name: string, module: M): Promise; + + protected abstract loadJS(name: string, module: M): Promise; + + @bind private async remove( + message: Zigbee2MQTTAPI['bridge/request/converter/remove'] | Zigbee2MQTTAPI['bridge/request/extension/remove'], + ): Promise> { + if (!message.name) { + return utils.getResponse(message, {}, `Invalid payload`); + } + + const {name} = message; + const toBeRemoved = this.getFilePath(name); + + if (fs.existsSync(toBeRemoved)) { + await this.removeJS(name, this.loadModuleFromText(this.getFileCode(name), name)); + + fs.rmSync(toBeRemoved, {force: true}); + logger.info(`${name} (${toBeRemoved}) removed.`); + await this.publishExternalJS(); + + return utils.getResponse(message, {}); + } else { + return utils.getResponse(message, {}, `${name} (${toBeRemoved}) doesn't exists`); + } + } + + @bind private async save( + message: Zigbee2MQTTAPI['bridge/request/converter/save'] | Zigbee2MQTTAPI['bridge/request/extension/save'], + ): Promise> { + if (!message.name || !message.code) { + return utils.getResponse(message, {}, `Invalid payload`); + } + + const {name, code} = message; + + try { + await this.loadJS(name, this.loadModuleFromText(code, name)); + + const filePath = this.getFilePath(name, true); + + fs.writeFileSync(filePath, code, 'utf8'); + logger.info(`${name} loaded. Contents written to '${filePath}'.`); + await this.publishExternalJS(); + + return utils.getResponse(message, {}); + } catch (error) { + return utils.getResponse(message, {}, `${name} contains invalid code: ${(error as Error).message}`); + } + } + + private async loadFiles(): Promise { + for (const extension of this.getFiles()) { + await this.loadJS(extension.name, this.loadModuleFromText(extension.code, extension.name)); + } + } + + private async publishExternalJS(): Promise { + await this.mqtt.publish( + `bridge/${this.mqttTopic}s`, + stringify(Array.from(this.getFiles())), + { + retain: true, + qos: 0, + }, + settings.get().mqtt.base_topic, + true, + ); + } + + private loadModuleFromText(moduleCode: string, name: string): M { + const moduleFakePath = path.join(__dirname, '..', '..', 'data', 'extension', name); + const sandbox: Context = { + require: require, + module: {}, + console, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + setImmediate, + clearImmediate, + }; + + runInNewContext(moduleCode, sandbox, moduleFakePath); + + return sandbox.module.exports; + } +} diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 471a193edb..ceb4d75194 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -1,12 +1,12 @@ -import type {IncomingMessage, Server, ServerResponse} from 'http'; -import type {Socket} from 'net'; +import type {IncomingMessage, Server, ServerResponse} from 'node:http'; +import type {Socket} from 'node:net'; -import assert from 'assert'; -import {existsSync, readFileSync} from 'fs'; -import {createServer} from 'http'; -import {createServer as createSecureServer} from 'https'; -import {posix} from 'path'; -import {parse} from 'url'; +import assert from 'node:assert'; +import {existsSync, readFileSync} from 'node:fs'; +import {createServer} from 'node:http'; +import {createServer as createSecureServer} from 'node:https'; +import {posix} from 'node:path'; +import {parse} from 'node:url'; import bind from 'bind-decorator'; import expressStaticGzip, {RequestHandler} from 'express-static-gzip'; @@ -49,7 +49,7 @@ export default class Frontend extends Extension { super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension); const frontendSettings = settings.get().frontend; - assert(frontendSettings, 'Frontend extension created without having frontend settings'); + assert(frontendSettings.enabled, `Frontend extension created with setting 'enabled: false'`); this.host = frontendSettings.host; this.port = frontendSettings.port; this.sslCert = frontendSettings.ssl_cert; diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index c462359b6a..43446b2921 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -1,4 +1,6 @@ -import assert from 'assert'; +import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from 'lib/types/api'; + +import assert from 'node:assert'; import bind from 'bind-decorator'; import equals from 'fast-deep-equal/es6'; @@ -14,8 +16,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, @@ -32,96 +32,21 @@ const STATE_PROPERTIES: Readonly { 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 { @@ -242,8 +167,10 @@ export default class Groups extends Extension { if (this.state.exists(device)) { const state = this.state.get(device); + const endpointNames = device.isDevice() && device.getEndpointNames(); + const stateKey = endpointNames && endpointNames.length >= member.ID ? `state_${endpointNames[member.ID - 1]}` : 'state'; - if (state.state === 'ON' || state.state === 'OPEN') { + if (state[stateKey] === 'ON' || state[stateKey] === 'OPEN') { return false; } } @@ -252,223 +179,147 @@ export default class Groups extends Extension { return true; } - private async parseMQTTMessage(data: eventdata.MQTTMessage): Promise { - 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 triggeredViaLegacyApi: ParsedMQTTMessage['triggeredViaLegacyApi'] = false; - 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); - 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`); + if (topicRegexMatch) { + const type = topicRegexMatch[1] as 'remove' | 'add' | 'remove_all'; + let resolvedGroup; + let groupKey; + let skipDisableReporting = false; + const message = JSON.parse(data.message) as Zigbee2MQTTAPI['bridge/request/group/members/add']; - /* 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'; + if (typeof message !== 'object' || message.device == undefined) { + return [message, {type, skipDisableReporting}, 'Invalid payload']; } - const parsedEntity = this.zigbee.resolveEntityAndEndpoint(data.message); - resolvedEntityDevice = parsedEntity.entity as Device; + const deviceKey = message.device; + skipDisableReporting = message.skip_disable_reporting != undefined ? message.skip_disable_reporting : false; - 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"}; + if (type !== 'remove_all') { + groupKey = message.group; - await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message})); + if (message.group == undefined) { + return [message, {type, skipDisableReporting}, `Invalid payload`]; } - 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) { - 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; - - if (type !== 'remove_all') { - groupKey = message.group; - resolvedEntityGroup = this.zigbee.resolveEntity(message.group) as Group; + resolvedGroup = this.zigbee.resolveEntity(message.group); - if (!resolvedEntityGroup || !(resolvedEntityGroup instanceof Group)) { - error = `Group '${message.group}' does not exist`; + 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, - triggeredViaLegacyApi, - skipDisableReporting, - resolvedEntityEndpoint, - }; } @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { - const parsed = await this.parseMQTTMessage(data); + const [raw, parsed, error] = this.parseMQTTMessage(data); - if (!parsed || !parsed.type) { + if (!raw || !parsed) { return; } - const { - resolvedEntityGroup, - resolvedEntityDevice, - type, - triggeredViaLegacyApi, - groupKey, - deviceKey, - skipDisableReporting, - resolvedEntityEndpoint, - } = parsed; - let error = parsed.error; + if (error) { + await this.publishResponse(parsed.type, raw, {}, error); + return; + } + + const {resolvedGroup, resolvedDevice, resolvedEndpoint, type, groupKey, deviceKey, endpointKey, skipDisableReporting} = parsed; const changedGroups: Group[] = []; - 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}`); - } + 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); + await this.publishResponse<'bridge/response/group/members/add'>(parsed.type, raw, { + device: deviceKey!, // valid from resolved asserts + endpoint: endpointKey!, // valid from resolved asserts + group: groupKey!, // valid from resolved asserts + }); + } 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); + await this.publishResponse<'bridge/response/group/members/remove'>(parsed.type, raw, { + device: deviceKey!, // valid from resolved asserts + endpoint: endpointKey!, // valid from resolved asserts + group: groupKey!, // valid from resolved asserts + }); + } else { + // remove_all + logger.info(`Removing '${resolvedDevice.name}' from all groups`); - if (!endpointNameLocal) { - keys.push(resolvedEntityDevice.name); - keys.push(resolvedEntityDevice.ieeeAddr); + for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEndpoint))) { + changedGroups.push(group); } - 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); - - /* 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`); - - for (const group of this.zigbee.groupsIterator((g) => g.members.includes(resolvedEntityEndpoint))) { - changedGroups.push(group); - } - - await resolvedEntityEndpoint.removeFromAllGroups(); - - 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) { - error = `Failed to ${type} from group (${(e as Error).message})`; - logger.debug((e as Error).stack!); + await resolvedEndpoint.removeFromAllGroups(); + await this.publishResponse<'bridge/response/group/members/remove_all'>(parsed.type, raw, { + device: deviceKey!, // valid from resolved asserts + endpoint: endpointKey!, // valid from resolved asserts + }); } + } 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; } - if (!triggeredViaLegacyApi) { - 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))); + for (const group of changedGroups) { + this.eventBus.emitGroupMembersChanged({group, action: type, endpoint: resolvedEndpoint, skipDisableReporting}); } + } + + private async publishResponse( + type: ParsedMQTTMessage['type'], + request: KeyValue, + data: Zigbee2MQTTAPI[T], + error?: string, + ): Promise { + const response = utils.getResponse(request, data, error); + await this.mqtt.publish(`bridge/response/group/members/${type}`, stringify(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/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index f73bdd1e45..8e98e7d4a2 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1,4 +1,4 @@ -import assert from 'assert'; +import assert from 'node:assert'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -43,75 +43,13 @@ 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']; -const DEFAULT_STATUS_TOPIC = 'homeassistant/status'; const COVER_OPENING_LOOKUP: ReadonlyArray = ['opening', 'open', 'forward', 'up', 'rising']; 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 +77,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 +179,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'}, internal_temperature: { device_class: 'temperature', entity_category: 'diagnostic', @@ -453,8 +389,7 @@ export default class HomeAssistant extends Extension { private discoveryRegex: RegExp; private discoveryRegexWoTopic = new RegExp(`(.*)/(.*)/(.*)/config`); private statusTopic: string; - private entityAttributes: boolean; - private legacyTrigger: boolean; + private legacyActionSensor: boolean; private experimentalEventEntities: boolean; // @ts-expect-error initialized in `start` private zigbee2MQTTVersion: string; @@ -482,12 +417,11 @@ export default class HomeAssistant extends Extension { } const haSettings = settings.get().homeassistant; - assert(haSettings, 'Home Assistant extension used without settings'); + assert(haSettings.enabled, `Home Assistant extension created with setting 'enabled: false'`); 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.legacyActionSensor = haSettings.legacy_action_sensor; 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}')`); @@ -518,8 +452,7 @@ 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); /** * Prevent unnecessary re-discovery of entities by waiting 5 seconds for retained discovery messages to come in. @@ -536,9 +469,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); @@ -547,9 +480,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 { @@ -823,7 +753,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', @@ -832,35 +762,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; } @@ -878,7 +784,7 @@ export default class HomeAssistant extends Extension { const motorState = allExposes ?.filter(isEnumExpose) .find((e) => ['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE); - const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running'); + const running = allExposes?.filter(isBinaryExpose)?.find((e) => e.name === 'running'); const discoveryEntry: DiscoveryEntry = { type: 'cover', @@ -898,8 +804,8 @@ export default class HomeAssistant extends Extension { if (running) { assert(position, `Cover must have 'position' when it has 'running'`); discoveryEntry.discovery_payload.value_template = - `{% if "${running.property}" in value_json ` + - `and value_json.${running.property} %} {% if value_json.${position.property} > 0 %} closing ` + + `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json ` + + `and value_json.${featurePropertyWithoutEndpoint(running)} %} {% if value_json.${featurePropertyWithoutEndpoint(position)} > 0 %} closing ` + `{% else %} opening {% endif %} {% else %} stopped {% endif %}`; } @@ -916,8 +822,8 @@ export default class HomeAssistant extends Extension { discoveryEntry.discovery_payload.state_closing = closingState; discoveryEntry.discovery_payload.state_stopped = stoppedState; discoveryEntry.discovery_payload.value_template = - `{% if "${motorState.property}" in value_json ` + - `and value_json.${motorState.property} %} {{ value_json.${motorState.property} }} {% else %} ` + + `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json ` + + `and value_json.${featurePropertyWithoutEndpoint(motorState)} %} {{ value_json.${featurePropertyWithoutEndpoint(motorState)} }} {% else %} ` + `${stoppedState} {% endif %}`; } } @@ -1072,6 +978,43 @@ export default class HomeAssistant extends Extension { } case 'numeric': { assertNumericExpose(firstExpose); + const allowsSet = firstExpose.access & ACCESS_SET; + + /** + * If numeric attribute has SET access then expose as SELECT entity. + */ + if (allowsSet) { + const discoveryEntry: DiscoveryEntry = { + type: 'number', + object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, + mockProperties: [{property: firstExpose.property, value: null}], + discovery_payload: { + name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, + value_template: `{{ value_json.${firstExpose.property} }}`, + command_topic: true, + command_topic_prefix: endpoint, + command_topic_postfix: firstExpose.property, + ...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}), + ...(firstExpose.value_step && {step: firstExpose.value_step}), + ...NUMERIC_DISCOVERY_LOOKUP[firstExpose.name], + }, + }; + + if (NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.device_class === 'temperature') { + discoveryEntry.discovery_payload.device_class = NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.device_class; + } else { + delete discoveryEntry.discovery_payload.device_class; + } + + // istanbul ignore else + if (firstExpose.value_min != null) discoveryEntry.discovery_payload.min = firstExpose.value_min; + // istanbul ignore else + if (firstExpose.value_max != null) discoveryEntry.discovery_payload.max = firstExpose.value_max; + + discoveryEntries.push(discoveryEntry); + break; + } + const extraAttrs = {}; // If a variable includes Wh, mark it as energy @@ -1079,8 +1022,6 @@ export default class HomeAssistant extends Extension { Object.assign(extraAttrs, {device_class: 'energy', state_class: 'total_increasing'}); } - const allowsSet = firstExpose.access & ACCESS_SET; - let key = firstExpose.name; // Home Assistant uses a different voc device_class for µg/m³ versus ppb or ppm. @@ -1115,96 +1056,70 @@ export default class HomeAssistant extends Extension { } discoveryEntries.push(discoveryEntry); - - /** - * If numeric attribute has SET access then expose as SELECT entity too. - * Note: currently both sensor and number are discovered, this is to avoid - * breaking changes for sensors already existing in HA (legacy). - */ - if (allowsSet) { - const discoveryEntry: DiscoveryEntry = { - type: 'number', - object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, - mockProperties: [{property: firstExpose.property, value: null}], - discovery_payload: { - name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, - value_template: `{{ value_json.${firstExpose.property} }}`, - command_topic: true, - command_topic_prefix: endpoint, - command_topic_postfix: firstExpose.property, - ...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}), - ...(firstExpose.value_step && {step: firstExpose.value_step}), - ...NUMERIC_DISCOVERY_LOOKUP[firstExpose.name], - }, - }; - - if (NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.device_class === 'temperature') { - discoveryEntry.discovery_payload.device_class = NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.device_class; - } else { - delete discoveryEntry.discovery_payload.device_class; - } - - // istanbul ignore else - if (firstExpose.value_min != null) discoveryEntry.discovery_payload.min = firstExpose.value_min; - // istanbul ignore else - if (firstExpose.value_max != null) discoveryEntry.discovery_payload.max = firstExpose.value_max; - - discoveryEntries.push(discoveryEntry); - } break; } case 'enum': { assertEnumExpose(firstExpose); - const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; - - if (firstExpose.access & ACCESS_STATE) { - discoveryEntries.push({ - type: 'sensor', - object_id: firstExpose.property, - mockProperties: [{property: firstExpose.property, value: null}], - discovery_payload: { - name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, - value_template: valueTemplate, - enabled_by_default: !(firstExpose.access & ACCESS_SET), - ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], - }, - }); - } - /** * 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' - ) { + 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: [], + 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 (!this.legacyActionSensor) { + break; + } + } + + const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; + + /** + * If enum has only one item and has SET access then expose as BUTTON entity. + */ + if (firstExpose.access & ACCESS_SET && firstExpose.values.length === 1) { discoveryEntries.push({ - type: 'event', + type: 'button', 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, + state_topic: false, + command_topic_prefix: endpoint, + command_topic: true, + command_topic_postfix: firstExpose.property, + payload_press: firstExpose.values[0].toString(), ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], }, }); + break; } /** - * If enum attribute has SET access then expose as SELECT entity too. - * Note: currently both sensor and select are discovered, this is to avoid - * breaking changes for sensors already existing in HA (legacy). + * If enum attribute has SET access then expose as SELECT entity. */ if (firstExpose.access & ACCESS_SET) { discoveryEntries.push({ type: 'select', object_id: firstExpose.property, - mockProperties: [], // Already mocked above in case access STATE is supported + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, value_template: valueTemplate, @@ -1213,29 +1128,24 @@ export default class HomeAssistant extends Extension { command_topic: true, command_topic_postfix: firstExpose.property, options: firstExpose.values.map((v) => v.toString()), - enabled_by_default: firstExpose.values.length !== 1, // hide if button is exposed ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], }, }); + break; } /** - * If enum has only item and only supports SET then expose as button entity. - * Note: select entity is hidden by default to avoid breaking changes - * for selects already existing in HA (legacy). + * Otherwise expose as SENSOR entity. */ - if (firstExpose.access & ACCESS_SET && firstExpose.values.length === 1) { + /* istanbul ignore else */ + if (firstExpose.access & ACCESS_STATE) { discoveryEntries.push({ - type: 'button', + type: 'sensor', object_id: firstExpose.property, - mockProperties: [], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { - name: endpoint ? /* istanbul ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label, - state_topic: false, - command_topic_prefix: endpoint, - command_topic: true, - command_topic_postfix: firstExpose.property, - payload_press: firstExpose.values[0].toString(), + name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label, + value_template: valueTemplate, ...ENUM_DISCOVERY_LOOKUP[firstExpose.name], }, }); @@ -1245,37 +1155,34 @@ export default class HomeAssistant extends Extension { case 'text': case 'composite': case 'list': { - // Deprecated: 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) { - const discoveryEntry: DiscoveryEntry = { - type: 'sensor', + if (firstExposeTyped.type === 'text' && firstExposeTyped.access & ACCESS_SET) { + discoveryEntries.push({ + type: 'text', object_id: firstExposeTyped.property, mockProperties: [{property: firstExposeTyped.property, value: null}], discovery_payload: { name: endpoint ? `${firstExposeTyped.label} ${endpoint}` : firstExposeTyped.label, - // Truncate text if it's too long - // https://github.com/Koenkk/zigbee2mqtt/issues/23199 - value_template: `{{ value_json.${firstExposeTyped.property} | default('',True) | string | truncate(254, True, '', 0) }}`, - enabled_by_default: !settableText, + state_topic: firstExposeTyped.access & ACCESS_STATE, + value_template: `{{ value_json.${firstExposeTyped.property} }}`, + command_topic_prefix: endpoint, + command_topic: true, + command_topic_postfix: firstExposeTyped.property, ...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name], }, - }; - discoveryEntries.push(discoveryEntry); + }); + break; } - if (settableText) { + if (firstExposeTyped.access & ACCESS_STATE) { discoveryEntries.push({ - type: 'text', + type: 'sensor', object_id: firstExposeTyped.property, - mockProperties: [], // Already mocked above in case access STATE is supported + mockProperties: [{property: firstExposeTyped.property, value: null}], discovery_payload: { name: endpoint ? `${firstExposeTyped.label} ${endpoint}` : firstExposeTyped.label, - state_topic: firstExposeTyped.access & ACCESS_STATE, - value_template: `{{ value_json.${firstExposeTyped.property} }}`, - command_topic_prefix: endpoint, - command_topic: true, - command_topic_postfix: firstExposeTyped.property, + // Truncate text if it's too long + // https://github.com/Koenkk/zigbee2mqtt/issues/23199 + value_template: `{{ value_json.${firstExposeTyped.property} | default('',True) | string | truncate(254, True, '', 0) }}`, ...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name], }, }); @@ -1378,11 +1285,8 @@ export default class HomeAssistant extends Extension { * 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]: ''}); - } + if (this.legacyActionSensor && data.message.action) { + await this.publishEntityState(data.entity, {action: ''}); } /** @@ -1391,13 +1295,10 @@ export default class HomeAssistant extends Extension { * 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 && data.message.action) { + const value = data.message['action'].toString(); + await this.publishDeviceTriggerDiscover(entity, 'action', value); + await this.mqtt.publish(`${data.entity.name}/action`, value, {}); } } @@ -1441,20 +1342,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[]} = {}; @@ -1512,35 +1399,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', @@ -1584,14 +1442,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)); @@ -1655,10 +1505,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 @@ -1698,7 +1544,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 { @@ -1905,7 +1751,7 @@ export default class HomeAssistant extends Extension { } else if (entity) { this.getDiscovered(entity).messages[topic] = {payload: stringify(message), published: true}; } - } else if ((data.topic === this.statusTopic || data.topic === DEFAULT_STATUS_TOPIC) && data.message.toLowerCase() === 'online') { + } else if (data.topic === this.statusTopic && data.message.toLowerCase() === 'online') { const timer = setTimeout(async () => { // Publish all device states. for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) { @@ -2074,7 +1920,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. @@ -2088,7 +1933,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, @@ -2184,20 +2029,6 @@ export default class HomeAssistant extends Extension { json_attributes_template: '{{ value_json.data.value | tojson }}', }, }, - { - type: 'sensor', - object_id: 'permit_join_timeout', - mockProperties: [], - discovery_payload: { - name: 'Permit join timeout', - device_class: 'duration', - unit_of_measurement: 's', - entity_category: 'diagnostic', - state_topic: true, - state_topic_postfix: 'info', - value_template: '{{ iif(value_json.permit_join_timeout is defined, value_json.permit_join_timeout, None) }}', - }, - }, // Switches. { @@ -2211,8 +2042,10 @@ 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', - payload_off: 'false', + state_on: 'true', + state_off: 'false', + payload_on: '{"time": 254}', + payload_off: '{"time": 0}', }, }, ); @@ -2220,7 +2053,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); @@ -2284,11 +2117,11 @@ export default class HomeAssistant extends Extension { ` {% set ns.r = ns.r|rejectattr(0, 'eq', key)|list + [(key, value)] %}\n` + ` {% endfor %}\n` + `{% endfor %}\n` + - `{% if ns.r|selectattr(0, 'eq', 'actionPrefix')|first is defined %}\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'))|reject('eq', ('event_type', None))|reject('eq', ('event_type', '')))|to_json}}`; return value_template; } 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..f30d1781d0 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -1,3 +1,5 @@ +import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from 'lib/types/api'; + import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -6,87 +8,60 @@ import * as settings from '../util/settings'; import utils from '../util/utils'; import Extension from './extension'; -interface Link { - source: {ieeeAddr: string; networkAddress: number}; - target: {ieeeAddr: string; networkAddress: number}; - linkquality: number; - depth: number; - routes: zh.RoutingTableEntry[]; - sourceIeeeAddr: string; - targetIeeeAddr: string; - sourceNwkAddr: number; - lqi: number; - relationship: number; -} - -interface Topology { - nodes: { - ieeeAddr: string; - friendlyName: string; - type: string; - networkAddress: number; - manufacturerName: string | undefined; - modelID: string | undefined; - failed: string[]; - lastSeen: number | undefined; - definition?: {model: string; vendor: string; supports: string; description: string}; - }[]; - links: Link[]; -} +const SUPPORTED_FORMATS = ['raw', 'graphviz', 'plantuml']; /** * 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, - graphviz: this.graphviz, - plantuml: this.plantuml, - }; override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); } @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); + const message = utils.parseJSON(data.message, data.message) as Zigbee2MQTTAPI['bridge/request/networkmap']; + try { const type = typeof message === 'object' ? message.type : message; - if (this.supportedFormats[type] === undefined) { - throw new Error(`Type '${type}' not supported, allowed are: ${Object.keys(this.supportedFormats)}`); + + if (!SUPPORTED_FORMATS.includes(type)) { + throw new Error(`Type '${type}' not supported, allowed are: ${SUPPORTED_FORMATS.join(',')}`); } const routes = typeof message === 'object' && message.routes; const topology = await this.networkScan(routes); - const value = this.supportedFormats[type](topology); - await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {routes, type, value}))); + let responseData: Zigbee2MQTTAPI['bridge/response/networkmap']; + + switch (type) { + case 'raw': { + responseData = {type, routes, value: this.raw(topology)}; + break; + } + case 'graphviz': { + responseData = {type, routes, value: this.graphviz(topology)}; + break; + } + case 'plantuml': { + responseData = {type, routes, value: this.plantuml(topology)}; + break; + } + } + + await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, responseData))); } catch (error) { await this.mqtt.publish('bridge/response/networkmap', stringify(utils.getResponse(message, {}, (error as Error).message))); } } } - @bind raw(topology: Topology): KeyValue { + raw(topology: Zigbee2MQTTNetworkMap): Zigbee2MQTTNetworkMap { return topology; } - @bind graphviz(topology: Topology): string { + graphviz(topology: Zigbee2MQTTNetworkMap): string { const colors = settings.get().map_options.graphviz.colors; let text = 'digraph G {\nnode[shape=record];\n'; @@ -152,7 +127,7 @@ export default class NetworkMap extends Extension { return text.replace(/\0/g, ''); } - @bind plantuml(topology: Topology): string { + plantuml(topology: Zigbee2MQTTNetworkMap): string { const text = []; text.push(`' paste into: https://www.planttext.com/`); @@ -207,7 +182,7 @@ export default class NetworkMap extends Extension { return text.join(`\n`); } - async networkScan(includeRoutes: boolean): Promise { + async networkScan(includeRoutes: boolean): Promise { logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`); const lqis: Map = new Map(); const routingTables: Map = new Map(); @@ -242,6 +217,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()); @@ -257,7 +233,7 @@ export default class NetworkMap extends Extension { logger.info(`Network scan finished`); - const topology: Topology = {nodes: [], links: []}; + const topology: Zigbee2MQTTNetworkMap = {nodes: [], links: []}; // XXX: display GP/disabled devices in the map, better feedback than just hiding them? for (const device of this.zigbee.devicesIterator((d) => d.type !== 'GreenPower')) { @@ -313,7 +289,7 @@ export default class NetworkMap extends Extension { } } - const link: Link = { + const link: Zigbee2MQTTNetworkMap['links'][number] = { source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress}, target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress}, linkquality: neighbor.linkquality, @@ -329,6 +305,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..5dfaddeea0 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -1,12 +1,14 @@ -import assert from 'assert'; -import path from 'path'; +import type {Zigbee2MQTTAPI} from 'lib/types/api'; +import type {Ota} from 'zigbee-herdsman-converters'; + +import assert from 'node:assert'; +import path from 'node:path'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; -import * as URI from 'uri-js'; import {Zcl} from 'zigbee-herdsman'; -import * as zhc from 'zigbee-herdsman-converters'; +import {ota} from 'zigbee-herdsman-converters'; import Device from '../model/device'; import dataDir from '../util/data'; @@ -15,21 +17,8 @@ import * as settings from '../util/settings'; import utils from '../util/utils'; import Extension from './extension'; -function isValidUrl(url: string): boolean { - let parsed; - try { - parsed = URI.parse(url); - } catch { - // istanbul ignore next - return false; - } - return parsed.scheme === 'http' || parsed.scheme === 'https'; -} - type UpdateState = 'updating' | 'idle' | 'available'; interface UpdatePayload { - update_available?: boolean; - update: { progress?: number; remaining?: number; @@ -39,34 +28,33 @@ 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'); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)/?(downgrade)?`, '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); this.eventBus.onDeviceMessage(this, this.onZigbeeEvent); - if (settings.get().ota.ikea_ota_use_test_url) { - zhc.ota.tradfri.useTestURL(); - } - // Let zigbeeOTA module know if the override index file is provided - let overrideOTAIndex = settings.get().ota.zigbee_ota_override_index_location; - if (overrideOTAIndex) { - // If the file name is not a full path, then treat it as a relative to the data directory - if (!isValidUrl(overrideOTAIndex) && !path.isAbsolute(overrideOTAIndex)) { - overrideOTAIndex = dataDir.joinPath(overrideOTAIndex); - } + const otaSettings = settings.get().ota; + // Let OTA module know if the override index file is provided + let overrideIndexLocation = otaSettings.zigbee_ota_override_index_location; - zhc.ota.zigbeeOTA.useIndexOverride(overrideOTAIndex); + // If the file name is not a full path, then treat it as a relative to the data directory + if (overrideIndexLocation && !ota.isValidUrl(overrideIndexLocation) && !path.isAbsolute(overrideIndexLocation)) { + overrideIndexLocation = dataDir.joinPath(overrideIndexLocation); } // In order to support local firmware files we need to let zigbeeOTA know where the data directory is - zhc.ota.setDataDir(dataDir.getPath()); + ota.setConfiguration({ + dataDir: dataDir.getPath(), + overrideIndexLocation, + // TODO: implement me + imageBlockResponseDelay: otaSettings.image_block_response_delay, + defaultMaximumDataSize: otaSettings.default_maximum_data_size, + }); // In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them. for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) { @@ -80,8 +68,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 { @@ -102,26 +94,20 @@ export default class OTAUpdate extends Extension { if (!check) return; this.lastChecked[data.device.ieeeAddr] = Date.now(); - let availableResult: zhc.OtaUpdateAvailableResult | undefined; + let availableResult: Ota.UpdateAvailableResult | undefined; try { - availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, data.data as zhc.ota.ImageInfo); + // never use 'previous' when responding to device request + availableResult = await ota.isUpdateAvailable(data.device.zh, data.device.otaExtraMetas, data.data as Ota.ImageInfo, false); } catch (error) { 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})); - } } } @@ -153,7 +139,7 @@ export default class OTAUpdate extends Extension { private getEntityPublishPayload( device: Device, - state: zhc.OtaUpdateAvailableResult | UpdateState, + state: Ota.UpdateAvailableResult | UpdateState, progress?: number, remaining?: number, ): UpdatePayload { @@ -166,32 +152,33 @@ 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)) { + const topicMatch = data.topic.match(topicRegex); + + if (!topicMatch) { return; } - const message = utils.parseJSON(data.message, data.message); + const message = utils.parseJSON(data.message, data.message) as + | Zigbee2MQTTAPI['bridge/request/device/ota_update/check'] + | Zigbee2MQTTAPI['bridge/request/device/ota_update/check/downgrade'] + | Zigbee2MQTTAPI['bridge/request/device/ota_update/update'] + | Zigbee2MQTTAPI['bridge/request/device/ota_update/update/downgrade']; 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 type = topicMatch[1]; + const downgrade = Boolean(topicMatch[2]); let error: string | undefined; let errorStack: string | undefined; @@ -199,12 +186,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,85 +195,54 @@ 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 availableResult = await ota.isUpdateAvailable(device.zh, device.otaExtraMetas, undefined, downgrade); 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; + const response = utils.getResponse<'bridge/response/device/ota_update/check'>(message, { + id: ID, + update_available: availableResult.available, + }); + + await this.mqtt.publish(`bridge/response/device/ota_update/check`, stringify(response)); } 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`; + const msg = `Updating '${device.name}' to ${downgrade ? 'previous' : '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 firmwareFrom = await this.readSoftwareBuildIDAndDateCode(device, 'immediate'); + const fileVersion = await ota.update(device.zh, device.otaExtraMetas, downgrade, async (progress, remaining) => { let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`; + if (remaining) { msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`; } 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); - const to = await this.readSoftwareBuildIDAndDateCode(device); - const [fromS, toS] = [stringify(from_), stringify(to)]; - logger.info(`Device '${device.name}' was updated from '${fromS}' to '${toS}'`); - responseData.from = from_ ? utils.toSnakeCaseObject(from_) : null; - responseData.to = to ? utils.toSnakeCaseObject(to) : null; + await this.publishEntityState( + device, + this.getEntityPublishPayload(device, {available: false, currentFileVersion: fileVersion, otaFileVersion: fileVersion}), + ); + + const firmwareTo = await this.readSoftwareBuildIDAndDateCode(device); + + logger.info(() => `Device '${device.name}' was updated from '${stringify(firmwareFrom)}' to '${stringify(firmwareTo)}'`); + /** * Re-configure after reading software build ID and date code, some devices use a * custom attribute for this (e.g. Develco SMSZB-120) @@ -300,39 +250,30 @@ 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})); - } + const response = utils.getResponse<'bridge/response/device/ota_update/update'>(message, { + id: ID, + from: firmwareFrom ? {software_build_id: firmwareFrom.softwareBuildID, date_code: firmwareFrom.dateCode} : undefined, + to: firmwareTo ? {software_build_id: firmwareTo.softwareBuildID, date_code: firmwareTo.dateCode} : undefined, + }); + + await this.mqtt.publish(`bridge/response/device/ota_update/update`, stringify(response)); } 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 (error) { + const response = utils.getResponse(message, {}, error); - if (!triggeredViaLegacyApi) { - const response = utils.getResponse(message, responseData, error); await this.mqtt.publish(`bridge/response/device/ota_update/${type}`, stringify(response)); - } - - if (error) { logger.error(error); if (errorStack) { diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index 925bc0bd76..e31e235f8e 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; @@ -48,7 +28,7 @@ interface ParsedTopic { } export default class Publish extends Extension { - async start(): Promise { + override async start(): Promise { this.eventBus.onMQTTMessage(this, this.onMQTTMessage); } @@ -96,43 +76,13 @@ 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 * the color temperature. This would lead to 2 zigbee publishes, where the first one * (state) is probably unnecessary. */ - if (settings.get().homeassistant) { + if (settings.get().homeassistant.enabled) { const hasColorTemp = message.color_temp !== undefined; const hasColor = message.color !== undefined; const hasBrightness = message.brightness !== undefined; @@ -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); @@ -260,7 +202,8 @@ export default class Publish extends Extension { /* istanbul ignore next */ // Match any key if the toZigbee converter defines no key. const converter = converters.find( - (c) => (!c.key || c.key.includes(key)) && (!c.endpoints || (endpointName && c.endpoints.includes(endpointName))), + (c) => + (!c.key || c.key.includes(key)) && (re instanceof Group || !c.endpoints || (endpointName && c.endpoints.includes(endpointName))), ); if (parsedTopic.type === 'set' && converter && usedConverters[endpointOrGroupID].includes(converter)) { @@ -329,8 +272,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 +283,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 +302,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/extension/receive.ts b/lib/extension/receive.ts index 5cb3584e86..fc3e8474f6 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -1,4 +1,4 @@ -import assert from 'assert'; +import assert from 'node:assert'; import bind from 'bind-decorator'; import debounce from 'debounce'; @@ -19,7 +19,7 @@ export default class Receive extends Extension { private debouncers: {[s: string]: {payload: KeyValue; publish: DebounceFunction}} = {}; private throttlers: {[s: string]: {publish: PublishEntityState}} = {}; - async start(): Promise { + override async start(): Promise { this.eventBus.onPublishEntityState(this, this.onPublishEntityState); this.eventBus.onDeviceMessage(this, this.onDeviceMessage); } diff --git a/lib/model/device.ts b/lib/model/device.ts index e995c9f391..1b5f281189 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -1,4 +1,4 @@ -import assert from 'assert'; +import assert from 'node:assert'; import * as zhc from 'zigbee-herdsman-converters'; import {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; @@ -29,6 +29,9 @@ export default class Device { get customClusters(): CustomClusters { return this.zh.customClusters; } + get otaExtraMetas(): zhc.Ota.ExtraMetas { + return typeof this.definition?.ota === 'object' ? this.definition.ota : {}; + } constructor(device: zh.Device) { this.zh = device; @@ -45,7 +48,7 @@ export default class Device { } } - async resolveDefinition(ignoreCache = false): Promise { + async resolveDefinition(ignoreCache: boolean = false): Promise { if (!this.zh.interviewing && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) { this.definition = await zhc.findByDevice(this.zh, true); this._definitionModelID = this.zh.modelID; @@ -117,10 +120,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..49d6b7c47d 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -1,9 +1,11 @@ -import type {QoS} from 'mqtt-packet'; +import type {IClientOptions, IClientPublishOptions, MqttClient} from 'mqtt'; -import fs from 'fs'; +import type {Zigbee2MQTTAPI} from './types/api'; + +import fs from 'node:fs'; import bind from 'bind-decorator'; -import * as mqtt from 'mqtt'; +import {connectAsync} from 'mqtt'; import logger from './util/logger'; import * as settings from './util/settings'; @@ -14,13 +16,11 @@ const NS = 'z2m:mqtt'; export default class MQTT { private publishedTopics: Set = new Set(); private connectionTimer?: NodeJS.Timeout; - // @ts-expect-error initialized in `connect` - private client: mqtt.MqttClient; + private client!: MqttClient; private eventBus: EventBus; - private initialConnect = true; private republishRetainedTimer?: NodeJS.Timeout; public retainedMessages: { - [s: string]: {payload: string; options: MQTTOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string}; + [s: string]: {payload: string; options: IClientPublishOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string}; } = {}; constructor(eventBus: EventBus) { @@ -32,13 +32,14 @@ export default class MQTT { logger.info(`Connecting to MQTT server at ${mqttSettings.server}`); - const options: mqtt.IClientOptions = { + const options: 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, }, + properties: {maximumPacketSize: mqttSettings.maximum_packet_size}, }; if (mqttSettings.version) { @@ -80,67 +81,70 @@ export default class MQTT { options.rejectUnauthorized = false; } - return await new Promise((resolve, reject) => { - 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. - clearTimeout(this.connectionTimer); - this.connectionTimer = setInterval(() => { - if (this.client.reconnecting) { - logger.error('Not connected to MQTT server!'); - } - }, utils.seconds(10)); - - logger.info('Connected to MQTT server'); - await this.publishStateOnline(); - - if (!this.initialConnect) { - this.republishRetainedTimer = setTimeout(async () => { - // Republish retained messages in case MQTT broker does not persist them. - // https://github.com/Koenkk/zigbee2mqtt/issues/9629 - for (const msg of Object.values(this.retainedMessages)) { - await this.publish(msg.topic, msg.payload, msg.options, msg.base, msg.skipLog, msg.skipReceive); - } - }, 2000); - } - - this.initialConnect = false; - this.subscribe(`${settings.get().mqtt.base_topic}/#`); - resolve(); - }); + this.client = await connectAsync(mqttSettings.server, options); - this.client.on('error', (err) => { - logger.error(`MQTT error: ${err.message}`); - reject(err); - }); + // https://github.com/Koenkk/zigbee2mqtt/issues/9822 + this.client.stream.setMaxListeners(0); - this.client.on('message', this.onMessage); + this.client.on('error', (err) => { + logger.error(`MQTT error: ${err.message}`); }); - } - @bind async publishStateOnline(): Promise { - await this.publish('bridge/state', utils.availabilityPayload('online', settings.get()), {retain: true, qos: 0}); + if (mqttSettings.version != undefined && mqttSettings.version >= 5) { + this.client.on('disconnect', (packet) => { + logger.error(`MQTT disconnect: reason ${packet.reasonCode} (${packet.properties?.reasonString})`); + }); + } + + this.client.on('message', this.onMessage); + + await this.onConnect(); + + this.client.on('connect', this.onConnect); + + this.republishRetainedTimer = setTimeout(async () => { + // Republish retained messages in case MQTT broker does not persist them. + // https://github.com/Koenkk/zigbee2mqtt/issues/9629 + for (const msg of Object.values(this.retainedMessages)) { + await this.publish(msg.topic, msg.payload, msg.options, msg.base, msg.skipLog, msg.skipReceive); + } + }, 2000); + + // Set timer at interval to check if connected to MQTT server. + this.connectionTimer = setInterval(() => { + if (!this.isConnected()) { + logger.error('Not connected to MQTT server!'); + } + }, utils.seconds(10)); } async disconnect(): Promise { clearTimeout(this.connectionTimer); clearTimeout(this.republishRetainedTimer); - await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()), {retain: true, qos: 0}); + + const stateData: Zigbee2MQTTAPI['bridge/state'] = {state: 'offline'}; + + await this.publish('bridge/state', JSON.stringify(stateData), {retain: true, qos: 0}); this.eventBus.removeListeners(this); logger.info('Disconnecting from MQTT server'); - this.client?.end(); + await this.client?.endAsync(); } - subscribe(topic: string): void { - this.client.subscribe(topic); + async subscribe(topic: string): Promise { + await this.client.subscribeAsync(topic); } - unsubscribe(topic: string): void { - this.client.unsubscribe(topic); + async unsubscribe(topic: string): Promise { + await this.client.unsubscribeAsync(topic); + } + + @bind private async onConnect(): Promise { + logger.info('Connected to MQTT server'); + + const stateData: Zigbee2MQTTAPI['bridge/state'] = {state: 'online'}; + + await this.publish('bridge/state', JSON.stringify(stateData), {retain: true, qos: 0}); + await this.subscribe(`${settings.get().mqtt.base_topic}/#`); } @bind public onMessage(topic: string, message: Buffer): void { @@ -158,18 +162,18 @@ export default class MQTT { } isConnected(): boolean { - return this.client && !this.client.reconnecting; + return this.client && !this.client.reconnecting && !this.client.disconnecting && !this.client.disconnected; } async publish( topic: string, payload: string, - options: MQTTOptions = {}, + options: IClientPublishOptions = {}, base = settings.get().mqtt.base_topic, skipLog = false, skipReceive = true, ): Promise { - const defaultOptions: {qos: QoS; retain: boolean} = {qos: 0, retain: false}; + const defaultOptions = {qos: 0 as const, retain: false}; topic = `${base}/${topic}`; if (skipReceive) { @@ -200,14 +204,20 @@ export default class MQTT { logger.info(() => `MQTT publish: topic '${topic}', payload '${payload}'`, NS); } - const actualOptions: mqtt.IClientPublishOptions = {...defaultOptions, ...options}; + const actualOptions: IClientPublishOptions = {...defaultOptions, ...options}; if (settings.get().mqtt.force_disable_retain) { actualOptions.retain = false; } - return await new Promise((resolve) => { - this.client.publish(topic, payload, actualOptions, () => resolve()); - }); + try { + await this.client.publishAsync(topic, payload, actualOptions); + } catch (error) { + /* istanbul ignore else */ + if (!skipLog) { + logger.error(`MQTT server error: ${(error as Error).message}`); + logger.error(`Could not send message: topic: '${topic}', payload: '${payload}`); + } + } } } diff --git a/lib/state.ts b/lib/state.ts index 84f38ae79e..27600583b7 100644 --- a/lib/state.ts +++ b/lib/state.ts @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'node:fs'; import objectAssignDeep from 'object-assign-deep'; @@ -15,7 +15,6 @@ const dontCacheProperties = [ 'button', 'button_left', 'button_right', - 'click', 'forgotten', 'keyerror', 'step_size', diff --git a/lib/types/api.ts b/lib/types/api.ts new file mode 100644 index 0000000000..2eaad7a007 --- /dev/null +++ b/lib/types/api.ts @@ -0,0 +1,687 @@ +import type * as zhc from 'zigbee-herdsman-converters'; +import type {ClusterDefinition, ClusterName, CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype'; + +import type {LogLevel, schemaJson} from '../util/settings'; + +export interface Zigbee2MQTTScene { + id: number; + name: string; +} + +interface Zigbee2MQTTDeviceEndpoint { + bindings: Zigbee2MQTTDeviceEndpointBinding[]; + configured_reportings: Zigbee2MQTTDeviceEndpointConfiguredReporting[]; + clusters: {input: string[]; output: string[]}; + scenes: Zigbee2MQTTScene[]; +} + +interface Zigbee2MQTTDeviceEndpointBinding { + cluster: string; + target: Zigbee2MQTTDeviceEndpointBindingTarget; +} + +interface Zigbee2MQTTDeviceEndpointBindingTarget { + type: string; + endpoint?: number; + ieee_address?: string; + id?: number; +} + +interface Zigbee2MQTTDeviceEndpointConfiguredReporting { + cluster: string; + attribute: string | number; + minimum_report_interval: number; + maximum_report_interval: number; + reportable_change: number; +} + +interface Zigbee2MQTTDeviceDefinition { + model: string; + vendor: string; + description: string; + exposes: zhc.Expose[]; + supports_ota: boolean; + options: zhc.Option[]; + icon: string; +} + +export interface Zigbee2MQTTDevice { + ieee_address: zh.Device['ieeeAddr']; + type: zh.Device['type']; + network_address: zh.Device['networkAddress']; + supported: boolean; + friendly_name: string; + disabled: boolean; + description: string | undefined; + definition: Zigbee2MQTTDeviceDefinition | undefined; + power_source: zh.Device['powerSource']; + software_build_id: zh.Device['softwareBuildID']; + date_code: zh.Device['dateCode']; + model_id: zh.Device['modelID']; + interviewing: zh.Device['interviewing']; + interview_completed: zh.Device['interviewCompleted']; + manufacturer: zh.Device['manufacturerName']; + endpoints: Record; +} + +export interface Zigbee2MQTTGroupMember { + ieee_address: zh.Device['ieeeAddr']; + endpoint: number; +} + +export interface Zigbee2MQTTGroup { + id: number; + friendly_name: 'default_bind_group' | string; + description: string | undefined; + scenes: Zigbee2MQTTScene[]; + members: Zigbee2MQTTGroupMember[]; +} + +export interface Zigbee2MQTTNetworkMap { + nodes: { + ieeeAddr: string; + friendlyName: string; + type: string; + networkAddress: number; + manufacturerName: string | undefined; + modelID: string | undefined; + failed: string[]; + lastSeen: number | undefined; + definition?: {model: string; vendor: string; supports: string; description: string}; + }[]; + links: { + source: {ieeeAddr: string; networkAddress: number}; + target: {ieeeAddr: string; networkAddress: number}; + linkquality: number; + depth: number; + routes: { + destinationAddress: number; + status: string; + nextHop: number; + }[]; + sourceIeeeAddr: string; + targetIeeeAddr: string; + sourceNwkAddr: number; + lqi: number; + relationship: number; + }[]; +} + +/** + * Zigbee2MQTT state/request/response API endpoints + */ +export interface Zigbee2MQTTAPI { + 'bridge/logging': { + message: string; + level: LogLevel; + namespace: string; + }; + + 'bridge/state': { + state: 'online' | 'offline'; + }; + + 'bridge/definition': { + clusters: Readonly>>; + custom_clusters: Record; + }; + + 'bridge/event': + | { + type: 'device_leave' | 'device_joined' | 'device_announce'; + data: { + friendly_name: string; + ieee_address: string; + }; + } + | { + type: 'device_interview'; + data: + | { + friendly_name: string; + ieee_address: string; + status: 'started' | 'failed'; + } + | { + friendly_name: string; + ieee_address: string; + status: 'successful'; + supported: boolean; + definition: Zigbee2MQTTDeviceDefinition | undefined; + }; + }; + + 'bridge/info': { + version: string; + commit: string | undefined; + zigbee_herdsman_converters: {version: string}; + zigbee_herdsman: {version: string}; + coordinator: { + ieee_address: string; + type: string; + meta: { + [s: string]: number | string; + }; + }; + network: { + pan_id: number; + /** `0x${string}` 8-len */ + extended_pan_id: string; + channel: number; + }; + log_level: 'debug' | 'info' | 'warning' | 'error'; + permit_join: boolean; + permit_join_end: number | undefined; + restart_required: boolean; + config: Settings; + config_schema: typeof schemaJson; + }; + + 'bridge/devices': Zigbee2MQTTDevice[]; + + 'bridge/groups': Zigbee2MQTTGroup[]; + + 'bridge/request/permit_join': + | { + /** [0-254], 0 meaning disable */ + time: number; + device?: string; + } + | `${number}`; + + 'bridge/response/permit_join': { + /** [0-254], 0 meaning disable */ + time: number; + device?: string; + }; + + 'bridge/request/health_check': ''; + + 'bridge/response/health_check': { + /** XXX: currently always returns true */ + healthy: boolean; + }; + + 'bridge/request/coordinator_check': ''; + + 'bridge/response/coordinator_check': { + missing_routers: { + ieee_address: string; + friendly_name: string; + }[]; + }; + + 'bridge/request/restart': ''; + + 'bridge/response/restart': Record; + + 'bridge/request/networkmap': + | { + type: 'raw' | 'graphviz' | 'plantuml'; + routes: boolean; + } + | 'raw' + | 'graphviz' + | 'plantuml'; + + 'bridge/response/networkmap': + | { + type: 'raw'; + routes: boolean; + value: Zigbee2MQTTNetworkMap; + } + | { + type: 'graphviz' | 'plantuml'; + routes: boolean; + value: string; + }; + + 'bridge/request/extension/save': { + name: string; + code: string; + }; + + 'bridge/response/extension/save': Record; + + 'bridge/request/extension/remove': { + name: string; + }; + + 'bridge/response/extension/remove': Record; + + 'bridge/request/converter/save': { + name: string; + code: string; + }; + + 'bridge/response/converter/save': Record; + + 'bridge/request/converter/remove': { + name: string; + }; + + 'bridge/response/converter/remove': Record; + + 'bridge/request/backup': ''; + + 'bridge/response/backup': { + /** base64 encoded ZIP archive */ + zip: string; + }; + + 'bridge/request/install_code/add': { + value: string; + }; + + 'bridge/response/install_code/add': { + value: string; + }; + + /** + * Applied on-the-fly: + * - newSettings.homeassistant + * - newSettings.advanced?.log_level + * - newSettings.advanced?.log_namespaced_levels + * - newSettings.advanced?.log_debug_namespace_ignore + */ + 'bridge/request/options': { + options: Record; + }; + + 'bridge/response/options': { + restart_required: boolean; + }; + + 'bridge/request/device/bind': { + from: string; + from_endpoint: string | number | 'default'; + to: string; + to_endpoint?: string | number; + clusters?: string[]; + skip_disable_reporting?: boolean; + }; + + 'bridge/response/device/bind': { + from: string; + from_endpoint: string | number; + to: string; + to_endpoint: string | number | undefined; + clusters: string[]; + failed: string[]; + }; + + 'bridge/request/device/unbind': { + from: string; + from_endpoint: string | number | 'default'; + to: string; + to_endpoint?: string | number; + clusters?: string[]; + skip_disable_reporting?: boolean; + }; + + 'bridge/response/device/unbind': { + from: string; + from_endpoint: string | number; + to: string; + to_endpoint: string | number | undefined; + clusters: string[]; + failed: string[]; + }; + + 'bridge/request/device/configure': + | { + id: string | number; + } + | string; + + 'bridge/response/device/configure': { + id: string | number; + }; + + 'bridge/request/device/remove': { + id: string; + block?: boolean; + force?: boolean; + }; + + 'bridge/response/device/remove': { + id: string; + block: boolean; + force: boolean; + }; + + 'bridge/request/device/ota_update/check': { + id: string; + }; + + 'bridge/request/device/ota_update/check/downgrade': { + id: string; + }; + + 'bridge/response/device/ota_update/check': { + id: string; + update_available: boolean; + }; + + 'bridge/request/device/ota_update/update': { + id: string; + }; + + 'bridge/request/device/ota_update/update/downgrade': { + id: string; + }; + + 'bridge/response/device/ota_update/update': { + id: string; + from: + | { + software_build_id: string; + date_code: string; + } + | undefined; + to: + | { + software_build_id: string; + date_code: string; + } + | undefined; + }; + + 'bridge/request/device/interview': { + id: string | number; + }; + + 'bridge/response/device/interview': { + id: string | number; + }; + + 'bridge/request/device/generate_external_definition': { + id: string | number; + }; + + 'bridge/response/device/generate_external_definition': { + id: string | number; + source: string; + }; + + 'bridge/request/device/options': { + id: string; + options: Record; + }; + + 'bridge/response/device/options': { + id: string; + from: Record; + to: Record; + restart_required: boolean; + }; + + 'bridge/request/device/rename': + | { + last: true; + from?: string; + to: string; + homeassistant_rename?: boolean; + } + | { + last: false | undefined; + from: string; + to: string; + homeassistant_rename?: boolean; + }; + + 'bridge/response/device/rename': { + from: string; + to: string; + homeassistant_rename: boolean; + }; + + 'bridge/request/device/configure_reporting': { + id: string; + endpoint: string | number; + cluster: string | number; + attribute: string | number | {ID: number; type: number}; + minimum_report_interval: number; + maximum_report_interval: number; + reportable_change: number; + option: Record; + }; + + 'bridge/response/device/configure_reporting': { + id: string; + endpoint: string | number; + cluster: string | number; + attribute: string | number | {ID: number; type: number}; + minimum_report_interval: number; + maximum_report_interval: number; + reportable_change: number; + }; + + 'bridge/request/group/remove': { + id: string; + force?: boolean; + }; + + 'bridge/response/group/remove': { + id: string; + force: boolean; + }; + + 'bridge/request/group/add': { + friendly_name: string; + id: string; + }; + + 'bridge/response/group/add': { + friendly_name: string; + id: number; + }; + + 'bridge/request/group/rename': { + from: string; + to: string; + homeassistant_rename?: boolean; + }; + + 'bridge/response/group/rename': { + from: string; + to: string; + homeassistant_rename: boolean; + }; + + 'bridge/request/group/options': { + id: string; + options: Record; + }; + + 'bridge/response/group/options': { + id: string; + from: Record; + to: Record; + restart_required: boolean; + }; + + 'bridge/request/group/members/add': { + device: string; + group: string; + endpoint: string | number | 'default'; + skip_disable_reporting?: boolean; + }; + + 'bridge/response/group/members/add': { + device: string; + group: string; + endpoint: string | number | 'default'; + }; + + 'bridge/request/group/members/remove': { + device: string; + group: string; + endpoint: string | number | 'default'; + skip_disable_reporting?: boolean; + }; + + 'bridge/response/group/members/remove': { + device: string; + group: string; + endpoint: string | number | 'default'; + }; + + 'bridge/request/group/members/remove_all': { + device: string; + endpoint: string | number | 'default'; + skip_disable_reporting?: boolean; + }; + + 'bridge/response/group/members/remove_all': { + device: string; + endpoint: string | number | 'default'; + }; + + 'bridge/request/touchlink/factory_reset': + | { + ieee_address: string; + channel: number; + } + | ''; + + 'bridge/response/touchlink/factory_reset': + | { + ieee_address: string; + channel: number; + } + | Record; + + 'bridge/request/touchlink/scan': ''; + + 'bridge/response/touchlink/scan': { + found: { + ieee_address: string; + channel: number; + }[]; + }; + + 'bridge/request/touchlink/identify': { + ieee_address: string; + channel: number; + }; + + 'bridge/response/touchlink/identify': { + ieee_address: string; + channel: number; + }; + + /** + * entity state response + */ + '{friendlyName}': { + [key: string]: unknown; + }; + + '{friendlyName}/availability': { + state: 'online' | 'offline'; + }; + + /** entity set request */ + '{friendlyName}/set': { + [key: string]: unknown; + }; + + /** entity get request */ + '{friendlyName}/get': { + [key: string]: unknown; + }; +} + +export type Zigbee2MQTTRequestEndpoints = + | 'bridge/request/permit_join' + | 'bridge/request/health_check' + | 'bridge/request/coordinator_check' + | 'bridge/request/restart' + | 'bridge/request/networkmap' + | 'bridge/request/extension/save' + | 'bridge/request/extension/remove' + | 'bridge/request/converter/save' + | 'bridge/request/converter/remove' + | 'bridge/request/backup' + | 'bridge/request/install_code/add' + | 'bridge/request/options' + | 'bridge/request/device/bind' + | 'bridge/request/device/unbind' + | 'bridge/request/device/configure' + | 'bridge/request/device/remove' + | 'bridge/request/device/ota_update/check' + | 'bridge/request/device/ota_update/check/downgrade' + | 'bridge/request/device/ota_update/update' + | 'bridge/request/device/ota_update/update/downgrade' + | 'bridge/request/device/interview' + | 'bridge/request/device/generate_external_definition' + | 'bridge/request/device/options' + | 'bridge/request/device/rename' + | 'bridge/request/device/configure_reporting' + | 'bridge/request/group/remove' + | 'bridge/request/group/add' + | 'bridge/request/group/rename' + | 'bridge/request/group/options' + | 'bridge/request/group/members/add' + | 'bridge/request/group/members/remove' + | 'bridge/request/group/members/remove_all' + | 'bridge/request/touchlink/factory_reset' + | 'bridge/request/touchlink/scan' + | 'bridge/request/touchlink/identify'; + +export type Zigbee2MQTTResponseEndpoints = + | 'bridge/response/permit_join' + | 'bridge/response/health_check' + | 'bridge/response/coordinator_check' + | 'bridge/response/restart' + | 'bridge/response/networkmap' + | 'bridge/response/extension/save' + | 'bridge/response/extension/remove' + | 'bridge/response/converter/save' + | 'bridge/response/converter/remove' + | 'bridge/response/backup' + | 'bridge/response/install_code/add' + | 'bridge/response/options' + | 'bridge/response/device/bind' + | 'bridge/response/device/unbind' + | 'bridge/response/device/configure' + | 'bridge/response/device/remove' + | 'bridge/response/device/ota_update/check' + | 'bridge/response/device/ota_update/check' + | 'bridge/response/device/ota_update/update' + | 'bridge/response/device/ota_update/update' + | 'bridge/response/device/interview' + | 'bridge/response/device/generate_external_definition' + | 'bridge/response/device/options' + | 'bridge/response/device/rename' + | 'bridge/response/device/configure_reporting' + | 'bridge/response/group/remove' + | 'bridge/response/group/add' + | 'bridge/response/group/rename' + | 'bridge/response/group/options' + | 'bridge/response/group/members/add' + | 'bridge/response/group/members/remove' + | 'bridge/response/group/members/remove_all' + | 'bridge/response/touchlink/factory_reset' + | 'bridge/response/touchlink/scan' + | 'bridge/response/touchlink/identify'; + +export type Zigbee2MQTTRequest = { + transaction?: string; +} & Zigbee2MQTTAPI[T]; + +export type Zigbee2MQTTResponseOK = { + status: 'ok'; + data: Zigbee2MQTTAPI[T]; + transaction?: string; +}; + +export type Zigbee2MQTTResponseError = { + status: 'error'; + data: Record; + error: string; + transaction?: string; +}; + +export type Zigbee2MQTTResponse = Zigbee2MQTTResponseOK | Zigbee2MQTTResponseError; diff --git a/lib/types/mkdir-recursive.ts b/lib/types/mkdir-recursive.ts deleted file mode 100644 index 8f3ad456e6..0000000000 --- a/lib/types/mkdir-recursive.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'mkdir-recursive' { - export function mkdirSync(path: string): void; -} diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 8aa313c7ff..84b105717b 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -5,14 +5,11 @@ 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, LQI as ZHLQI, NetworkParameters as ZHNetworkParameters, RoutingTable as ZHRoutingTable, - RoutingTableEntry as ZHRoutingTableEntry, } from 'zigbee-herdsman/dist/adapter/tstype'; import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events'; import type {Device as ZHDevice, Endpoint as ZHEndpoint, Group as ZHGroup} from 'zigbee-herdsman/dist/controller/model'; @@ -33,19 +30,6 @@ declare global { type Extension = TypeExtension; // Types - type ExternalDefinition = zhc.Definition & {homeassistant: unknown}; - interface MQTTResponse { - data: KeyValue; - status: 'error' | 'ok'; - error?: string; - transaction?: string; - } - interface MQTTOptions { - qos?: QoS; - retain?: boolean; - properties?: {messageExpiryInterval: number}; - } - type Scene = {id: number; name: string}; type StateChangeReason = 'publishDebounce' | 'groupOptimistic' | 'lastSeenChanged' | 'publishCached' | 'publishThrottle'; type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise; type RecursivePartial = {[P in keyof T]?: RecursivePartial}; @@ -61,12 +45,10 @@ declare global { type Group = ZHGroup; type LQI = ZHLQI; type RoutingTable = ZHRoutingTable; - type RoutingTableEntry = ZHRoutingTableEntry; type CoordinatorVersion = ZHCoordinatorVersion; type NetworkParameters = ZHNetworkParameters; - type Cluster = ZHCluster; interface Bind { - cluster: zh.Cluster; + cluster: ZHCluster; target: zh.Endpoint | zh.Group; } } @@ -113,19 +95,19 @@ declare global { // Settings interface Settings { - homeassistant?: { + version?: number; + homeassistant: { + enabled: boolean; discovery_topic: string; status_topic: string; - legacy_entity_attributes: boolean; - legacy_triggers: boolean; experimental_event_entities: boolean; + legacy_action_sensor: boolean; }; - permit_join: boolean; - availability?: { + availability: { + enabled: boolean; active: {timeout: number}; passive: {timeout: number}; }; - external_converters: string[]; mqtt: { base_topic: string; include_device_information: boolean; @@ -140,6 +122,7 @@ declare global { cert?: string; client_id?: string; reject_unauthorized?: boolean; + maximum_packet_size: number; }; serial: { disable_led: boolean; @@ -174,9 +157,11 @@ declare global { update_check_interval: number; disable_automatic_update_check: boolean; zigbee_ota_override_index_location?: string; - ikea_ota_use_test_url?: boolean; + image_block_response_delay?: number; + default_maximum_data_size?: number; }; - frontend?: { + frontend: { + enabled: boolean; auth_token?: string; host?: string; port: number; @@ -186,11 +171,9 @@ declare global { ssl_key?: string; }; devices: {[s: string]: DeviceOptions}; - groups: {[s: string]: OptionalProps, 'devices'>}; + groups: {[s: string]: Omit}; 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 +198,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 +206,6 @@ declare global { retention?: number; availability?: boolean | {timeout: number}; optimistic?: boolean; - retrieve_state?: boolean; debounce?: number; debounce_ignore?: string[]; throttle?: number; @@ -240,7 +214,6 @@ declare global { filtered_optimistic?: string[]; icon?: string; homeassistant?: KeyValue; - legacy?: boolean; friendly_name: string; description?: string; qos?: 0 | 1 | 2; @@ -251,14 +224,12 @@ declare global { } interface GroupOptions { - devices: string[]; ID: number; optimistic?: boolean; off_state?: 'all_members_off' | 'last_member_state'; filtered_attributes?: string[]; filtered_cache?: string[]; filtered_optimistic?: string[]; - retrieve_state?: boolean; homeassistant?: KeyValue; friendly_name: string; description?: string; diff --git a/lib/types/zigbee2mqtt-frontend.d.ts b/lib/types/zigbee2mqtt-frontend.d.ts index a38c2f128f..b0eeff4fbf 100644 --- a/lib/types/zigbee2mqtt-frontend.d.ts +++ b/lib/types/zigbee2mqtt-frontend.d.ts @@ -10,7 +10,7 @@ declare module 'http' { } declare module 'express-static-gzip' { - import {IncomingMessage, ServerResponse} from 'http'; + import {IncomingMessage, ServerResponse} from 'node:http'; export type RequestHandler = (req: IncomingMessage, res: ServerResponse, finalhandler: (err: unknown) => void) => void; export default function expressStaticGzip(root: string, options?: Record): RequestHandler; } diff --git a/lib/util/data.ts b/lib/util/data.ts index 8480778c65..ef78006cc3 100644 --- a/lib/util/data.ts +++ b/lib/util/data.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; function setPath(): string { return process.env.ZIGBEE2MQTT_DATA ? process.env.ZIGBEE2MQTT_DATA : path.normalize(path.join(__dirname, '..', '..', 'data')); diff --git a/lib/util/logger.ts b/lib/util/logger.ts index 4fbdf3903d..53a3a1a0d0 100644 --- a/lib/util/logger.ts +++ b/lib/util/logger.ts @@ -1,8 +1,7 @@ -import assert from 'assert'; -import fs from 'fs'; -import path from 'path'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; -import fx from 'mkdir-recursive'; import moment from 'moment'; import {rimrafSync} from 'rimraf'; import winston from 'winston'; @@ -73,7 +72,7 @@ class Logger { logging += `, file (filename: ${logFilename})`; // Make sure that log directory exists when not logging to stdout only - fx.mkdirSync(this.directory); + fs.mkdirSync(this.directory, {recursive: true}); if (settings.get().advanced.log_symlink_current) { const current = settings.get().advanced.log_directory.replace('%TIMESTAMP%', 'current'); @@ -104,6 +103,7 @@ class Logger { this.fileTransport = new winston.transports.File(transportFileOptions); this.logger.add(this.fileTransport); + this.cleanup(); } /* istanbul ignore next */ @@ -220,7 +220,7 @@ class Logger { } // Cleanup any old log directory. - public cleanup(): void { + private cleanup(): void { if (settings.get().advanced.log_directory.includes('%TIMESTAMP%')) { const rootDirectory = path.join(this.directory, '..'); diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index afe0dbf96e..68a47fee91 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -3,116 +3,93 @@ "properties": { "homeassistant": { "title": "Home Assistant integration", - "requiresRestart": true, "description": "Home Assistant integration (MQTT discovery)", - "default": false, - "oneOf": [ - { + "type": "object", + "properties": { + "enabled": { "type": "boolean", - "title": "Home Assistant (simple)" + "title": "Enabled", + "description": "Enable Home Assistant integration", + "default": false, + "requiresRestart": true }, - { - "type": "object", - "title": "Home Assistant (advanced)", - "properties": { - "legacy_triggers": { - "type": "boolean", - "title": "Home Assistant legacy triggers", - "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 discoverd", - "default": true - }, - "discovery_topic": { - "type": "string", - "title": "Homeassistant discovery topic", - "description": "Home Assistant discovery topic", - "requiresRestart": true, - "examples": ["homeassistant"] - }, - "legacy_entity_attributes": { - "type": "boolean", - "title": "Home Assistant legacy entity attributes", - "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", - "default": true - }, - "status_topic": { - "type": "string", - "title": "Home Assistant status topic", - "description": "Home Assistant status topic", - "requiresRestart": true, - "examples": ["homeassistant/status"] - }, - "experimental_event_entities": { - "type": "boolean", - "title": "Home Assistant experimental event entities", - "description": "Home Assistant experimental event entities, when enabled Zigbee2MQTT will add event entities for exposed actions. The events and attributes are currently deemed experimental and subject to change.", - "default": false - } - } + "discovery_topic": { + "type": "string", + "title": "Homeassistant discovery topic", + "description": "Home Assistant discovery topic", + "default": "homeassistant", + "requiresRestart": true, + "examples": ["homeassistant"] + }, + "status_topic": { + "type": "string", + "title": "Home Assistant status topic", + "description": "Home Assistant status topic", + "default": "homeassistant/status", + "requiresRestart": true, + "examples": ["homeassistant/status"] + }, + "legacy_action_sensor": { + "type": "boolean", + "title": "Home Assistant legacy action sensors", + "description": "Home Assistant legacy actions sensor, when enabled a action sensor will be discoverd and an empty `action` will be send after every published action.", + "default": false + }, + "experimental_event_entities": { + "type": "boolean", + "title": "Home Assistant experimental event entities", + "description": "Home Assistant experimental event entities, when enabled Zigbee2MQTT will add event entities for exposed actions. The events and attributes are currently deemed experimental and subject to change.", + "default": false } - ] - }, - "permit_join": { - "type": "boolean", - "default": false, - "title": "Permit join", - "description": "Allow new devices to join (re-applied at restart)" + }, + "required": ["enabled"] }, "availability": { - "oneOf": [ - { + "type": "object", + "title": "Availability", + "description": "Checks whether devices are online/offline", + "properties": { + "enabled": { "type": "boolean", - "title": "Availability (simple)" + "title": "Enabled", + "description": "Enable availability checks", + "default": false, + "requiresRestart": true }, - { + "active": { "type": "object", - "title": "Availability (advanced)", + "title": "Active", + "requiresRestart": true, + "description": "Options for active devices (routers/mains powered)", "properties": { - "active": { - "type": "object", - "title": "Active", + "timeout": { + "type": "number", + "title": "Timeout", "requiresRestart": true, - "description": "Options for active devices (routers/mains powered)", - "properties": { - "timeout": { - "type": "number", - "title": "Timeout", - "requiresRestart": true, - "default": 10, - "description": "Time after which an active device will be marked as offline in minutes" - } - } - }, - "passive": { - "type": "object", - "title": "Passive", + "default": 10, + "description": "Time after which an active device will be marked as offline in minutes" + } + }, + "required": ["timeout"] + }, + "passive": { + "type": "object", + "title": "Passive", + "requiresRestart": true, + "description": "Options for passive devices (mostly battery powered)", + "properties": { + "timeout": { + "type": "number", + "title": "Timeout", "requiresRestart": true, - "description": "Options for passive devices (mostly battery powered)", - "properties": { - "timeout": { - "type": "number", - "title": "Timeout", - "requiresRestart": true, - "default": 1500, - "description": "Time after which an passive device will be marked as offline in minutes" - } - } + "default": 1500, + "description": "Time after which an passive device will be marked as offline in minutes" } - } + }, + "required": ["timeout"] } - ], - "title": "Availability", - "requiresRestart": true, - "description": "Checks whether devices are online/offline" - }, - "external_converters": { - "type": "array", - "title": "External converters", - "description": "You can define external converters to e.g. add support for a DiY device", - "requiresRestart": true, - "items": { - "type": "string" }, - "examples": ["DIYRuZ_FreePad.js"] + "required": ["enabled"] }, "mqtt": { "type": "object", @@ -207,8 +184,17 @@ "type": "boolean", "title": "Force disable retain", "requiresRestart": true, - "description": "Disable retain for all send messages. ONLY enable if you MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", + "description": "Disable retain for all send messages. ONLY enable if your MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", "default": false + }, + "maximum_packet_size": { + "type": "number", + "title": "Maximum packet size", + "requiresRestart": true, + "description": "Specifies the maximum allowed packet length (in bytes) that the server can send to Zigbee2MQTT. NOTE: The same value exists in your MQTT broker but for the length the client can send to it instead.", + "default": 1048576, + "minimum": 20, + "maximum": 268435456 } }, "required": ["server"] @@ -343,83 +329,91 @@ "description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.", "default": false }, - "ikea_ota_use_test_url": { - "type": "boolean", - "title": "IKEA TRADFRI OTA use test url", - "requiresRestart": true, - "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", - "default": false - }, "zigbee_ota_override_index_location": { "type": ["string", "null"], "title": "OTA index override file name", "requiresRestart": true, "description": "Location of override OTA index file", "examples": ["index.json"] + }, + "image_block_response_delay": { + "type": "number", + "title": "Image block response delay", + "description": "Limits the rate of requests (in milliseconds) during OTA updates to reduce network congestion. You can increase this value if your network appears unstable during OTA.", + "default": 250, + "minimum": 50, + "requiresRestart": true + }, + "default_maximum_data_size": { + "type": "number", + "title": "Default maximum data size", + "description": "The size of file chunks sent during an update (in bytes). Note: This value may get ignored for manufacturers that require specific values.", + "default": 50, + "minimum": 10, + "maximum": 100, + "requiresRestart": true } } }, "frontend": { - "oneOf": [ - { + "type": "object", + "title": "Frontend", + "properties": { + "enabled": { "type": "boolean", - "title": "Frontend (simple)" + "title": "Enabled", + "description": "Enable frontend", + "default": false, + "requiresRestart": true }, - { - "type": "object", - "title": "Frontend (advanced)", - "properties": { - "port": { - "type": "number", - "title": "Port", - "description": "Frontend binding port. Ignored when using a unix domain socket", - "default": 8080, - "requiresRestart": true - }, - "host": { - "type": ["string", "null"], - "title": "Bind host", - "description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.", - "examples": ["127.0.0.1", "::1", "/run/zigbee2mqtt/zigbee2mqtt.sock"], - "requiresRestart": true - }, - "auth_token": { - "type": ["string", "null"], - "title": "Auth token", - "description": "Enables authentication, disabled by default", - "requiresRestart": true - }, - "url": { - "type": ["string", "null"], - "title": "URL", - "description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page", - "requiresRestart": true - }, - "ssl_cert": { - "type": ["string", "null"], - "title": "Certificate file path", - "description": "SSL Certificate file path for exposing HTTPS. The sibling property 'ssl_key' must be set for HTTPS to be activated.", - "requiresRestart": true - }, - "ssl_key": { - "type": ["string", "null"], - "title": "key file path", - "description": "SSL key file path for exposing HTTPS. The sibling property 'ssl_cert' must be set for HTTPS to be activated.", - "requiresRestart": true - }, - "base_url": { - "type": "string", - "pattern": "^\\/.*", - "title": "Base URL", - "description": "Base URL for the frontend. If hosted under a subpath, e.g. 'http://localhost:8080/z2m', set this to '/z2m'", - "default": "/", - "requiresRestart": true - } - } + "port": { + "type": "number", + "title": "Port", + "description": "Frontend binding port. Ignored when using a unix domain socket", + "default": 8080, + "requiresRestart": true + }, + "host": { + "type": ["string", "null"], + "title": "Bind host", + "description": "Frontend binding host. Binds to a unix socket when an absolute path is given instead.", + "examples": ["127.0.0.1", "::1", "/run/zigbee2mqtt/zigbee2mqtt.sock"], + "requiresRestart": true + }, + "auth_token": { + "type": ["string", "null"], + "title": "Auth token", + "description": "Enables authentication, disabled by default", + "requiresRestart": true + }, + "url": { + "type": ["string", "null"], + "title": "URL", + "description": "URL on which the frontend can be reached, currently only used for the Home Assistant device configuration page", + "requiresRestart": true + }, + "ssl_cert": { + "type": ["string", "null"], + "title": "Certificate file path", + "description": "SSL Certificate file path for exposing HTTPS. The sibling property 'ssl_key' must be set for HTTPS to be activated.", + "requiresRestart": true + }, + "ssl_key": { + "type": ["string", "null"], + "title": "key file path", + "description": "SSL key file path for exposing HTTPS. The sibling property 'ssl_cert' must be set for HTTPS to be activated.", + "requiresRestart": true + }, + "base_url": { + "type": "string", + "pattern": "^\\/.*", + "title": "Base URL", + "description": "Base URL for the frontend. If hosted under a subpath, e.g. 'http://localhost:8080/z2m', set this to '/z2m'", + "default": "/", + "requiresRestart": true } - ], - "title": "Frontend", - "requiresRestart": true + }, + "required": ["enabled"] }, "devices": { "type": "object", @@ -451,20 +445,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", @@ -733,85 +713,6 @@ "maximum": 127, "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" }, - "output": { - "type": "string", - "enum": ["attribute_and_json", "attribute", "json"], - "title": "MQTT output type", - "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" - }, - "homeassistant_discovery_topic": { - "type": "string", - "title": "Homeassistant discovery topic", - "description": "Home Assistant discovery topic", - "requiresRestart": true, - "examples": ["homeassistant"] - }, - "homeassistant_legacy_entity_attributes": { - "type": "boolean", - "title": "Home Assistant legacy entity attributes", - "description": "Home Assistant legacy entity attributes, when enabled Zigbee2MQTT will add state attributes to each entity, additional to the separate entities and devices it already creates", - "default": true - }, - "homeassistant_status_topic": { - "type": "string", - "title": "Home Assistant status topic", - "description": "Home Assistant status topic", - "requiresRestart": true, - "examples": ["homeassistant/status"] - }, - "homeassistant_legacy_triggers": { - "type": "boolean", - "title": "Home Assistant legacy triggers", - "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)", - "requiresRestart": true, - "description": "Baud rate speed for serial port, this can be anything firmware support but default is 115200 for Z-Stack and EZSP, 38400 for Deconz, however note that some EZSP firmware need 57600", - "examples": [38400, 57600, 115200] - }, - "rtscts": { - "type": "boolean", - "title": "RTS / CTS (deprecated)", - "requiresRestart": true, - "description": "RTS / CTS Hardware Flow Control for serial port" - }, - "ikea_ota_use_test_url": { - "type": "boolean", - "title": "IKEA TRADFRI OTA use test url (deprecated)", - "requiresRestart": true, - "description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation", - "default": false - } - } - }, - "experimental": { - "type": "object", - "title": "Experimental (deprecated)", - "properties": { - "transmit_power": { - "type": ["number", "null"], - "title": "Transmit power", - "requiresRestart": true, - "description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)" - }, "output": { "type": "string", "enum": ["attribute_and_json", "attribute", "json"], @@ -819,24 +720,6 @@ "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)" } } - }, - "whitelist": { - "readOnly": true, - "type": "array", - "requiresRestart": true, - "title": "Whitelist (deprecated, use passlist)", - "items": { - "type": "string" - } - }, - "ban": { - "readOnly": true, - "type": "array", - "requiresRestart": true, - "title": "Ban (deprecated, use blocklist)", - "items": { - "type": "string" - } } }, "required": ["mqtt"], @@ -946,12 +829,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 588fb98cf7..053091c122 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; import Ajv, {ValidateFunction} from 'ajv'; import objectAssignDeep from 'object-assign-deep'; @@ -8,33 +8,13 @@ import schemaJson from './settings.schema.json'; import utils from './utils'; import yaml, {YAMLFileException} from './yaml'; -export let schema: KeyValue = schemaJson; - -schema = {}; -objectAssignDeep(schema, schemaJson); - -// Remove legacy settings from schema -{ - delete schema.properties.advanced.properties.homeassistant_discovery_topic; - 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; - delete schema.properties.experimental; - delete (schemaJson as KeyValue).properties.whitelist; - delete (schemaJson as KeyValue).properties.ban; -} - +export {schemaJson}; +export const CURRENT_VERSION = 3; /** NOTE: by order of priority, lower index is lower level (more important) */ export const LOG_LEVELS: readonly string[] = ['error', 'warning', 'info', 'debug'] as const; -export type LogLevel = (typeof LOG_LEVELS)[number]; +export type LogLevel = 'error' | 'warning' | 'info' | 'debug'; -// DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697 -const file = process.env.ZIGBEE2MQTT_CONFIG ?? data.joinPath('configuration.yaml'); +const CONFIG_FILE_PATH = data.joinPath('configuration.yaml'); const NULLABLE_SETTINGS = ['homeassistant']; const ajvSetting = new Ajv({allErrors: true}).addKeyword('requiresRestart').compile(schemaJson); const ajvRestartRequired = new Ajv({allErrors: true}).addKeyword({keyword: 'requiresRestart', validate: (s: unknown) => !s}).compile(schemaJson); @@ -44,13 +24,30 @@ const ajvRestartRequiredDeviceOptions = new Ajv({allErrors: true}) 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: [], +export const defaults: RecursivePartial = { + homeassistant: { + enabled: false, + discovery_topic: 'homeassistant', + status_topic: 'homeassistant/status', + legacy_action_sensor: false, + experimental_event_entities: false, + }, + availability: { + enabled: false, + active: {timeout: 10}, + passive: {timeout: 1500}, + }, + frontend: { + enabled: false, + port: 8080, + base_url: '/', + }, mqtt: { base_topic: 'zigbee2mqtt', include_device_information: false, force_disable_retain: false, + // 1MB = roughly 3.5KB per device * 300 devices for `/bridge/devices` + maximum_packet_size: 1048576, }, serial: { disable_led: false, @@ -80,11 +77,11 @@ const defaults: RecursivePartial = { ota: { update_check_interval: 24 * 60, disable_automatic_update_check: false, + image_block_response_delay: 250, + default_maximum_data_size: 50, }, device_options: {}, advanced: { - legacy_api: true, - legacy_availability_payload: true, log_rotation: true, log_symlink_current: false, log_output: ['console', 'file'], @@ -108,13 +105,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, }, }; @@ -125,7 +115,8 @@ function loadSettingsWithDefaults(): void { if (!_settings) { _settings = read(); } - _settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings; + + _settingsWithDefaults = objectAssignDeep({}, defaults, getPersistedSettings()) as Settings; if (!_settingsWithDefaults.devices) { _settingsWithDefaults.devices = {}; @@ -134,101 +125,6 @@ function loadSettingsWithDefaults(): void { if (!_settingsWithDefaults.groups) { _settingsWithDefaults.groups = {}; } - - if (_settingsWithDefaults.homeassistant) { - const defaults = { - discovery_topic: 'homeassistant', - status_topic: 'hass/status', - legacy_entity_attributes: true, - legacy_triggers: true, - experimental_event_entities: false, - }; - const sLegacy = {}; - if (_settingsWithDefaults.advanced) { - for (const key of [ - 'homeassistant_legacy_triggers', - 'homeassistant_discovery_topic', - 'homeassistant_legacy_entity_attributes', - 'homeassistant_status_topic', - ]) { - // @ts-expect-error ignore typing - if (_settingsWithDefaults.advanced[key] !== undefined) { - // @ts-expect-error ignore typing - sLegacy[key.replace('homeassistant_', '')] = _settingsWithDefaults.advanced[key]; - } - } - } - - const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {}; - // @ts-expect-error ignore typing - _settingsWithDefaults.homeassistant = {}; - // @ts-expect-error ignore typing - objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s); - } - - if (_settingsWithDefaults.availability || _settingsWithDefaults.advanced?.availability_timeout) { - const defaults = {}; - const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {}; - // @ts-expect-error ignore typing - _settingsWithDefaults.availability = {}; - // @ts-expect-error ignore typing - objectAssignDeep(_settingsWithDefaults.availability, defaults, s); - } - - if (_settingsWithDefaults.frontend) { - const defaults = {port: 8080, auth_token: null, base_url: '/'}; - const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {}; - // @ts-expect-error ignore typing - _settingsWithDefaults.frontend = {}; - // @ts-expect-error ignore typing - objectAssignDeep(_settingsWithDefaults.frontend, defaults, s); - } - - // @ts-expect-error ignore typing - if (_settings.advanced?.baudrate !== undefined && _settings.serial?.baudrate == null) { - // @ts-expect-error ignore typing - _settingsWithDefaults.serial.baudrate = _settings.advanced.baudrate; - } - - // @ts-expect-error ignore typing - if (_settings.advanced?.rtscts !== undefined && _settings.serial?.rtscts == null) { - // @ts-expect-error ignore typing - _settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts; - } - - // @ts-expect-error ignore typing - if (_settings.advanced?.ikea_ota_use_test_url !== undefined && _settings.ota?.ikea_ota_use_test_url == null) { - // @ts-expect-error ignore typing - _settingsWithDefaults.ota.ikea_ota_use_test_url = _settings.advanced.ikea_ota_use_test_url; - } - - // @ts-expect-error ignore typing - if (_settings.experimental?.transmit_power !== undefined && _settings.advanced?.transmit_power == null) { - // @ts-expect-error ignore typing - _settingsWithDefaults.advanced.transmit_power = _settings.experimental.transmit_power; - } - - // @ts-expect-error ignore typing - if (_settings.experimental?.output !== undefined && _settings.advanced?.output == null) { - // @ts-expect-error ignore typing - _settingsWithDefaults.advanced.output = _settings.experimental.output; - } - - if (_settings.advanced?.log_level === 'warn') { - _settingsWithDefaults.advanced.log_level = 'warning'; - } - - // @ts-expect-error ignore typing - if (_settingsWithDefaults.ban) { - // @ts-expect-error ignore typing - _settingsWithDefaults.blocklist.push(..._settingsWithDefaults.ban); - } - - // @ts-expect-error ignore typing - if (_settingsWithDefaults.whitelist) { - // @ts-expect-error ignore typing - _settingsWithDefaults.passlist.push(..._settingsWithDefaults.whitelist); - } } function parseValueRef(text: string): {filename: string; key: string} | null { @@ -246,11 +142,11 @@ function parseValueRef(text: string): {filename: string; key: string} | null { } function write(): void { - const settings = getInternalSettings(); + const settings = getPersistedSettings(); const toWrite: KeyValue = objectAssignDeep({}, settings); // Read settings to check if we have to split devices/groups into separate file. - const actual = yaml.read(file); + const actual = yaml.read(CONFIG_FILE_PATH); // In case the setting is defined in a separate file (e.g. !secret network_key) update it there. for (const path of [ @@ -292,16 +188,16 @@ function write(): void { writeDevicesOrGroups('devices'); writeDevicesOrGroups('groups'); - - yaml.writeIfChanged(file, toWrite); + yaml.writeIfChanged(CONFIG_FILE_PATH, toWrite); _settings = read(); + loadSettingsWithDefaults(); } export function validate(): string[] { try { - getInternalSettings(); + getPersistedSettings(); } catch (error) { if (error instanceof YAMLFileException) { return [`Your YAML file: '${error.file}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`]; @@ -356,6 +252,7 @@ export function validate(): string[] { }; const settingsWithDefaults = get(); + Object.values(settingsWithDefaults.devices).forEach((d) => check(d)); Object.values(settingsWithDefaults.groups).forEach((g) => check(g)); @@ -367,24 +264,11 @@ 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; } -function read(): Settings { - const s = yaml.read(file) as Settings; +function read(): Partial { + const s = yaml.read(CONFIG_FILE_PATH) as Partial; applyEnvironmentVariables(s); // Read !secret MQTT username and password if set @@ -425,7 +309,7 @@ function read(): Settings { s[type] = {}; for (const file of files) { const content = yaml.readIfExists(data.joinPath(file)); - /* eslint-disable-line */ // @ts-expect-error + // @ts-expect-error noMutate not typed properly s[type] = objectAssignDeep.noMutate(s[type], content); } } @@ -457,23 +341,22 @@ function applyEnvironmentVariables(settings: Partial): void { if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) { try { - // @ts-expect-error ignore typing - setting[key] = JSON.parse(envVariable); + setting[key as keyof Settings] = JSON.parse(envVariable); } catch { - // @ts-expect-error ignore typing - setting[key] = envVariable; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setting[key as keyof Settings] = envVariable as any; } } else if (type.indexOf('number') >= 0) { - // @ts-expect-error ignore typing - setting[key] = (envVariable as unknown as number) * 1; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setting[key as keyof Settings] = ((envVariable as unknown as number) * 1) as any; } else if (type.indexOf('boolean') >= 0) { - // @ts-expect-error ignore typing - setting[key] = envVariable.toLowerCase() === 'true'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setting[key as keyof Settings] = (envVariable.toLowerCase() === 'true') as any; } else { /* istanbul ignore else */ if (type.indexOf('string') >= 0) { - // @ts-expect-error ignore typing - setting[key] = envVariable; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setting[key as keyof Settings] = envVariable as any; } } } @@ -495,7 +378,12 @@ function applyEnvironmentVariables(settings: Partial): void { iterate(schemaJson.properties, []); } -function getInternalSettings(): Partial { +/** + * Get the settings actually written in the yaml. + * Env vars are applied on top. + * Defaults merged on startup are not included. + */ +export function getPersistedSettings(): Partial { if (!_settings) { _settings = read(); } @@ -513,7 +401,7 @@ export function get(): Settings { export function set(path: string[], value: string | number | boolean | KeyValue): void { /* eslint-disable-next-line */ - let settings: any = getInternalSettings(); + let settings: any = getPersistedSettings(); for (let i = 0; i < path.length; i++) { const key = path[i]; @@ -531,16 +419,21 @@ export function set(path: string[], value: string | number | boolean | KeyValue) write(); } -export function apply(settings: Record): boolean { - getInternalSettings(); // Ensure _settings is initialized. - // @ts-expect-error ignore typing +export function apply(settings: Record, throwOnError: boolean = true): boolean { + getPersistedSettings(); // Ensure _settings is initialized. + // @ts-expect-error noMutate not typed properly const newSettings = objectAssignDeep.noMutate(_settings, settings); + utils.removeNullPropertiesFromObject(newSettings, NULLABLE_SETTINGS); ajvSetting(newSettings); - const errors = ajvSetting.errors && ajvSetting.errors.filter((e) => e.keyword !== 'required'); - if (errors?.length) { - const error = errors[0]; - throw new Error(`${error.instancePath.substring(1)} ${error.message}`); + + if (throwOnError) { + const errors = ajvSetting.errors && ajvSetting.errors.filter((e) => e.keyword !== 'required'); + + if (errors?.length) { + const error = errors[0]; + throw new Error(`${error.instancePath.substring(1)} ${error.message}`); + } } _settings = newSettings; @@ -558,26 +451,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); @@ -619,7 +504,7 @@ export function addDevice(ID: string): DeviceOptionsWithId { throw new Error(`Device '${ID}' already exists`); } - const settings = getInternalSettings(); + const settings = getPersistedSettings(); if (!settings.devices) { settings.devices = {}; @@ -631,22 +516,8 @@ 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(); + const settings = getPersistedSettings(); if (!settings.blocklist) { settings.blocklist = []; } @@ -657,18 +528,8 @@ export function blockDevice(ID: string): void { export function removeDevice(IDorName: string): void { const device = getDeviceThrowIfNotExists(IDorName); - const settings = getInternalSettings(); + const settings = getPersistedSettings(); 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(); } @@ -679,7 +540,7 @@ export function addGroup(name: string, ID?: string): GroupOptions { throw new Error(`friendly_name '${name}' is already in use`); } - const settings = getInternalSettings(); + const settings = getPersistedSettings(); if (!settings.groups) { settings.groups = {}; } @@ -706,56 +567,16 @@ 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(); + const settings = getPersistedSettings(); delete settings.groups![groupID]; write(); } export function changeEntityOptions(IDorName: string, newOptions: KeyValue): boolean { - const settings = getInternalSettings(); + const settings = getPersistedSettings(); delete newOptions.friendly_name; delete newOptions.devices; let validator: ValidateFunction; @@ -791,7 +612,7 @@ export function changeFriendlyName(IDorName: string, newName: string): void { throw new Error(`friendly_name '${newName}' is already in use`); } - const settings = getInternalSettings(); + const settings = getPersistedSettings(); const device = getDevice(IDorName); if (device) { @@ -811,7 +632,7 @@ export function changeFriendlyName(IDorName: string, newName: string): void { export function reRead(): void { _settings = undefined; - getInternalSettings(); + getPersistedSettings(); _settingsWithDefaults = undefined; get(); } @@ -823,4 +644,5 @@ export const testing = { _settingsWithDefaults = undefined; }, defaults, + CURRENT_VERSION, }; diff --git a/lib/util/settingsMigration.ts b/lib/util/settingsMigration.ts new file mode 100644 index 0000000000..101f79d642 --- /dev/null +++ b/lib/util/settingsMigration.ts @@ -0,0 +1,542 @@ +import {copyFileSync, writeFileSync} from 'node:fs'; + +import data from './data'; +import * as settings from './settings'; + +interface SettingsMigration { + path: string[]; + note: string; + noteIf?: (previousValue: unknown) => boolean; +} + +interface SettingsAdd extends Omit { + value: unknown; +} + +type SettingsRemove = SettingsMigration; + +interface SettingsChange extends SettingsMigration { + previousValueAnyOf?: unknown[]; + newValue: unknown; +} + +interface SettingsTransfer extends SettingsMigration { + newPath: string[]; +} + +interface SettingsCustomHandler extends Omit { + execute: (currentSettings: Partial) => [validPath: boolean, previousValue: unknown, changed: boolean]; +} + +const SUPPORTED_VERSIONS: Settings['version'][] = [undefined, 2, settings.CURRENT_VERSION]; + +function backupSettings(version: number): void { + const filePath = data.joinPath('configuration.yaml'); + + copyFileSync(filePath, filePath.replace('.yaml', `_backup_v${version}.yaml`)); +} + +/** + * Set the given path in given settings to given value. If requested, create path. + * + * @param currentSettings + * @param path + * @param value + * @param createPathIfNotExist + * @returns Returns true if value was set, false if not. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setValue(currentSettings: any, path: string[], value: unknown, createPathIfNotExist: boolean = false): boolean { + for (let i = 0; i < path.length; i++) { + const key = path[i]; + + if (i === path.length - 1) { + currentSettings[key] = value; + } else { + if (!currentSettings[key]) { + /* istanbul ignore else */ + if (createPathIfNotExist) { + currentSettings[key] = {}; + } else { + // invalid path + // ignored in test since currently call is always guarded by get-validated path, so this is never reached + return false; + } + } + + currentSettings = currentSettings[key]; + } + } + + return true; +} + +/** + * Get the value at the given path in given settings. + * + * @param currentSettings + * @param path + * @returns + * - true if path was valid + * - the value at path + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getValue(currentSettings: any, path: string[]): [validPath: boolean, value: unknown] { + for (let i = 0; i < path.length; i++) { + const key = path[i]; + + if (i === path.length - 1) { + return [true, currentSettings[key]]; + } else { + if (!currentSettings[key]) { + // invalid path + break; + } + + currentSettings = currentSettings[key]; + } + } + + return [false, undefined]; +} + +/** + * Add a value at given path, path is created as needed. + * @param currentSettings + * @param addition + */ +function addValue(currentSettings: Partial, addition: SettingsAdd): void { + setValue(currentSettings, addition.path, addition.value, true); +} + +/** + * Remove value at given path, if path is valid. + * Value is actually set to undefined, which triggers removal when `settings.apply` is called. + * @param currentSettings + * @param removal + * @returns + */ +function removeValue(currentSettings: Partial, removal: SettingsRemove): [validPath: boolean, previousValue: unknown] { + const [validPath, previousValue] = getValue(currentSettings, removal.path); + + if (validPath && previousValue != undefined) { + setValue(currentSettings, removal.path, undefined); + } + + return [validPath, previousValue]; +} + +/** + * Change value at given path, if path is valid, and value matched one of the defined values (if any). + * @param currentSettings + * @param change + * @returns + */ +function changeValue(currentSettings: Partial, change: SettingsChange): [validPath: boolean, previousValue: unknown, changed: boolean] { + const [validPath, previousValue] = getValue(currentSettings, change.path); + let changed: boolean = false; + + if (validPath && previousValue !== change.newValue) { + if (!change.previousValueAnyOf || change.previousValueAnyOf.includes(previousValue)) { + setValue(currentSettings, change.path, change.newValue); + + changed = true; + } + } + + return [validPath, previousValue, changed]; +} + +/** + * Transfer value at given path, to new path. + * Given path must be valid. + * New path must not be valid or new path value must be nullish, otherwise given path is removed only. + * Value at given path is actually set to undefined, which triggers removal when `settings.apply` is called. + * New path is created as needed. + * @param currentSettings + * @param transfer + * @returns + */ +function transferValue( + currentSettings: Partial, + transfer: SettingsTransfer, +): [validPath: boolean, previousValue: unknown, transfered: boolean] { + const [validPath, previousValue] = getValue(currentSettings, transfer.path); + const [destValidPath, destValue] = getValue(currentSettings, transfer.newPath); + const transfered = validPath && previousValue != undefined && (!destValidPath || destValue == undefined || Array.isArray(destValue)); + + // no point in set if already undefined + if (validPath && previousValue != undefined) { + setValue(currentSettings, transfer.path, undefined); + } + + if (transfered) { + if (Array.isArray(previousValue) && Array.isArray(destValue)) { + setValue(currentSettings, transfer.newPath, [...previousValue, ...destValue], true); + } else { + setValue(currentSettings, transfer.newPath, previousValue, true); + } + } + + return [validPath, previousValue, transfered]; +} + +const noteIfWasTrue = (previousValue: unknown): boolean => previousValue === true; +const noteIfWasDefined = (previousValue: unknown): boolean => previousValue != undefined; +const noteIfWasNonEmptyArray = (previousValue: unknown): boolean => Array.isArray(previousValue) && previousValue.length > 0; + +function migrateToTwo( + currentSettings: Partial, + transfers: SettingsTransfer[], + changes: SettingsChange[], + additions: SettingsAdd[], + removals: SettingsRemove[], + customHandlers: SettingsCustomHandler[], +): void { + transfers.push( + { + path: ['advanced', 'homeassistant_discovery_topic'], + note: `HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.`, + noteIf: noteIfWasDefined, + newPath: ['homeassistant', 'discovery_topic'], + }, + { + path: ['advanced', 'homeassistant_status_topic'], + note: `HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.`, + noteIf: noteIfWasDefined, + newPath: ['homeassistant', 'status_topic'], + }, + { + path: ['advanced', 'baudrate'], + note: `Baudrate was moved from advanced.baudrate to serial.baudrate.`, + noteIf: noteIfWasDefined, + newPath: ['serial', 'baudrate'], + }, + { + path: ['advanced', 'rtscts'], + note: `RTSCTS was moved from advanced.rtscts to serial.rtscts.`, + noteIf: noteIfWasDefined, + newPath: ['serial', 'rtscts'], + }, + { + path: ['experimental', 'transmit_power'], + note: `Transmit power was moved from experimental.transmit_power to advanced.transmit_power.`, + noteIf: noteIfWasDefined, + newPath: ['advanced', 'transmit_power'], + }, + { + path: ['experimental', 'output'], + note: `Output was moved from experimental.output to advanced.output.`, + noteIf: noteIfWasDefined, + newPath: ['advanced', 'output'], + }, + { + path: ['ban'], + note: `ban was renamed to passlist.`, + noteIf: noteIfWasDefined, + newPath: ['blocklist'], + }, + { + path: ['whitelist'], + note: `whitelist was renamed to passlist.`, + noteIf: noteIfWasDefined, + newPath: ['passlist'], + }, + ); + + changes.push({ + path: ['advanced', 'log_level'], + note: `Log level 'warn' has been renamed to 'warning'.`, + noteIf: (previousValue): boolean => previousValue === 'warn', + previousValueAnyOf: ['warn'], + newValue: 'warning', + }); + + additions.push({ + path: ['version'], + note: `Migrated settings to version 2`, + value: 2, + }); + + const haLegacyTriggers: SettingsRemove = { + path: ['homeassistant', 'legacy_triggers'], + note: `Action and click sensors have been removed (homeassistant.legacy_triggers setting). This means all sensor.*_action and sensor.*_click entities are removed. Use the MQTT device trigger instead.`, + noteIf: noteIfWasTrue, + }; + const haLegacyEntityAttrs: SettingsRemove = { + path: ['homeassistant', 'legacy_entity_attributes'], + note: `Entity attributes (homeassistant.legacy_entity_attributes setting) has been removed. This means that entities discovered by Zigbee2MQTT will no longer have entity attributes (Home Assistant entity attributes are accessed via e.g. states.binary_sensor.my_sensor.attributes).`, + noteIf: noteIfWasTrue, + }; + const otaIkeaUseTestUrl: SettingsRemove = { + path: ['ota', 'ikea_ota_use_test_url'], + note: `Due to the OTA rework, the ota.ikea_ota_use_test_url option has been removed.`, + noteIf: noteIfWasTrue, + }; + + removals.push( + haLegacyTriggers, + haLegacyEntityAttrs, + { + path: ['advanced', 'homeassistant_legacy_triggers'], + note: haLegacyTriggers.note, + noteIf: haLegacyTriggers.noteIf, + }, + { + path: ['advanced', 'homeassistant_legacy_entity_attributes'], + note: haLegacyEntityAttrs.note, + noteIf: haLegacyEntityAttrs.noteIf, + }, + { + path: ['permit_join'], + note: `The permit_join setting has been removed, use the frontend or MQTT to permit joining.`, + noteIf: noteIfWasTrue, + }, + otaIkeaUseTestUrl, + { + path: ['advanced', 'ikea_ota_use_test_url'], + note: otaIkeaUseTestUrl.note, + noteIf: otaIkeaUseTestUrl.noteIf, + }, + { + path: ['advanced', 'legacy_api'], + note: `The MQTT legacy API has been removed (advanced.legacy_api setting). See link below for affected topics.`, + noteIf: noteIfWasTrue, + }, + { + path: ['advanced', 'legacy_availability_payload'], + note: `Due to the removal of advanced.legacy_availability_payload, zigbee2mqtt/bridge/state will now always be a JSON object ({"state":"online"} or {"state":"offline"})`, + noteIf: noteIfWasTrue, + }, + { + path: ['advanced', 'soft_reset_timeout'], + note: `Removed deprecated: Soft reset feature (advanced.soft_reset_timeout setting)`, + noteIf: noteIfWasDefined, + }, + { + path: ['advanced', 'report'], + note: `Removed deprecated: Report feature (advanced.report setting)`, + noteIf: noteIfWasTrue, + }, + { + path: ['advanced', 'availability_timeout'], + note: `Removed deprecated: advanced.availability_timeout availability settings`, + noteIf: noteIfWasDefined, + }, + { + path: ['advanced', 'availability_blocklist'], + note: `Removed deprecated: advanced.availability_blocklist availability settings`, + noteIf: noteIfWasNonEmptyArray, + }, + { + path: ['advanced', 'availability_passlist'], + note: `Removed deprecated: advanced.availability_passlist availability settings`, + noteIf: noteIfWasNonEmptyArray, + }, + { + path: ['advanced', 'availability_blacklist'], + note: `Removed deprecated: advanced.availability_blacklist availability settings`, + noteIf: noteIfWasNonEmptyArray, + }, + { + path: ['advanced', 'availability_whitelist'], + note: `Removed deprecated: advanced.availability_whitelist availability settings`, + noteIf: noteIfWasNonEmptyArray, + }, + { + path: ['device_options', 'legacy'], + note: `Removed everything that was enabled through device_options.legacy. See link below for affected devices.`, + noteIf: noteIfWasTrue, + }, + { + path: ['experimental'], + note: `The entire experimental section was removed.`, + noteIf: noteIfWasDefined, + }, + { + path: ['external_converters'], + note: `External converters are now automatically loaded from the 'data/external_converters' directory without requiring settings to be set. Make sure your external converters are still needed (might be supported out-of-the-box now), and if so, move them to that directory.`, + noteIf: noteIfWasNonEmptyArray, + }, + ); + + // note only once + const noteEntityOptionsRetrieveState = `Retrieve state option ((devices|groups).xyz.retrieve_state setting)`; + + for (const deviceKey in currentSettings.devices) { + removals.push({ + path: ['devices', deviceKey, 'retrieve_state'], + note: noteEntityOptionsRetrieveState, + noteIf: noteIfWasTrue, + }); + } + + for (const groupKey in currentSettings.groups) { + removals.push({ + path: ['groups', groupKey, 'retrieve_state'], + note: noteEntityOptionsRetrieveState, + noteIf: noteIfWasTrue, + }); + removals.push({ + path: ['groups', groupKey, 'devices'], + note: `Removed configuring group members through configuration.yaml (groups.xyz.devices setting). This will not impact current group members; however, you will no longer be able to add or remove devices from a group through the configuration.yaml.`, + noteIf: noteIfWasDefined, + }); + } + + customHandlers.push(); +} + +function migrateToThree( + currentSettings: Partial, + transfers: SettingsTransfer[], + changes: SettingsChange[], + additions: SettingsAdd[], + removals: SettingsRemove[], + customHandlers: SettingsCustomHandler[], +): void { + transfers.push(); + changes.push({ + path: ['version'], + note: `Migrated settings to version 3`, + newValue: 3, + }); + additions.push(); + removals.push(); + + const changeToObject = (currentSettings: Partial, path: string[]): ReturnType => { + const [validPath, previousValue] = getValue(currentSettings, path); + + /* istanbul ignore else */ + if (validPath) { + if (typeof previousValue === 'boolean') { + setValue(currentSettings, path, {enabled: previousValue}); + } else { + setValue(currentSettings, path, {enabled: true, ...(previousValue as object)}); + } + } + + return [validPath, previousValue, validPath]; + }; + + customHandlers.push( + { + note: `Property 'homeassistant' is now always an object.`, + noteIf: () => true, + execute: (currentSettings) => changeToObject(currentSettings, ['homeassistant']), + }, + { + note: `Property 'frontend' is now always an object.`, + noteIf: () => true, + execute: (currentSettings) => changeToObject(currentSettings, ['frontend']), + }, + { + note: `Property 'availability' is now always an object.`, + noteIf: () => true, + execute: (currentSettings) => changeToObject(currentSettings, ['availability']), + }, + ); +} + +/** + * Order of execution: + * - Transfer + * - Change + * - Add + * - Remove + * + * Should allow the most flexibility whenever combination of migrations is necessary (e.g. Transfer + Change) + */ +export function migrateIfNecessary(): void { + let currentSettings = settings.getPersistedSettings(); + + if (!SUPPORTED_VERSIONS.includes(currentSettings.version)) { + throw new Error( + `Your configuration.yaml has an unsupported version ${currentSettings.version}, expected one of ${SUPPORTED_VERSIONS.map((v) => String(v)).join(',')}.`, + ); + } + + /* istanbul ignore next */ + const finalVersion = process.env.JEST_WORKER_ID ? settings.testing.CURRENT_VERSION : settings.CURRENT_VERSION; + + // when same version as current, nothing left to do + while (currentSettings.version !== finalVersion) { + let migrationNotesFileName: string | undefined; + // don't duplicate outputs + const migrationNotes: Set = new Set(); + const transfers: SettingsTransfer[] = []; + const changes: SettingsChange[] = []; + const additions: SettingsAdd[] = []; + const removals: SettingsRemove[] = []; + const customHandlers: SettingsCustomHandler[] = []; + + backupSettings(currentSettings.version || 1); + + // each version should only bump to the next version so as to gradually migrate if necessary + /* istanbul ignore else */ + if (currentSettings.version == undefined) { + // migrating from 1 (`version` did not exist) to 2 + migrationNotesFileName = 'migration-1-to-2.log'; + + migrateToTwo(currentSettings, transfers, changes, additions, removals, customHandlers); + } else if (currentSettings.version === 2) { + migrationNotesFileName = 'migration-2-to-3.log'; + + migrateToThree(currentSettings, transfers, changes, additions, removals, customHandlers); + } /* else if (currentSettings.version === 2.1) {} */ + + for (const transfer of transfers) { + const [validPath, previousValue, transfered] = transferValue(currentSettings, transfer); + + if (validPath && (!transfer.noteIf || transfer.noteIf(previousValue))) { + migrationNotes.add(`[${transfered ? 'TRANSFER' : 'REMOVAL'}] ${transfer.note}`); + } + } + + for (const change of changes) { + const [validPath, previousValue, changed] = changeValue(currentSettings, change); + + if (validPath && changed && (!change.noteIf || change.noteIf(previousValue))) { + migrationNotes.add(`[CHANGE] ${change.note}`); + } + } + + for (const addition of additions) { + addValue(currentSettings, addition); + + migrationNotes.add(`[ADDITION] ${addition.note}`); + } + + for (const removal of removals) { + const [validPath, previousValue] = removeValue(currentSettings, removal); + + if (validPath && (!removal.noteIf || removal.noteIf(previousValue))) { + migrationNotes.add(`[REMOVAL] ${removal.note}`); + } + } + + for (const customHandler of customHandlers) { + const [validPath, previousValue, changed] = customHandler.execute(currentSettings); + + /* istanbul ignore else */ + if (validPath && changed && (!customHandler.noteIf || customHandler.noteIf(previousValue))) { + migrationNotes.add(`[SPECIAL] ${customHandler.note}`); + } + } + + /* istanbul ignore else */ + if (migrationNotesFileName && migrationNotes.size > 0) { + migrationNotes.add(`For more details, see https://github.com/Koenkk/zigbee2mqtt/discussions/24198`); + const migrationNotesFilePath = data.joinPath(migrationNotesFileName); + + writeFileSync(migrationNotesFilePath, Array.from(migrationNotes).join(`\r\n\r\n`), 'utf8'); + + console.log(`Migration notes written in ${migrationNotesFilePath}`); + } + + // don't throw to allow stepping through versions (validates against current schema) + settings.apply(currentSettings as unknown as Record, false); + settings.reRead(); + currentSettings = settings.getPersistedSettings(); + } +} diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 2748ec881a..e67a26a6c1 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -1,14 +1,17 @@ +import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from 'lib/types/api'; import type * as zhc from 'zigbee-herdsman-converters'; -import assert from 'assert'; -import fs from 'fs'; -import path from 'path'; -import vm from 'vm'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; import equals from 'fast-deep-equal/es6'; 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: @@ -17,10 +20,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() + @@ -77,10 +76,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}; } @@ -120,22 +117,39 @@ function equalsPartial(object: KeyValue, expected: KeyValue): boolean { return true; } -function getObjectProperty(object: KeyValue, key: string, defaultValue: unknown): unknown { +function getObjectProperty(object: KeyValue, key: string, defaultValue: NoInfer): T { return object && object[key] !== undefined ? object[key] : defaultValue; } -function getResponse(request: KeyValue | string, data: KeyValue, error?: string): MQTTResponse { - const response: MQTTResponse = {data, status: error ? 'error' : 'ok'}; +function getResponse( + request: KeyValue | string, + data: Zigbee2MQTTAPI[T], + error?: string, +): Zigbee2MQTTResponse { + if (error !== undefined) { + const response: Zigbee2MQTTResponse = { + data: {}, // always return an empty `data` payload on error + status: 'error', + error: error, + }; + + if (typeof request === 'object' && request.transaction !== undefined) { + response.transaction = request.transaction; + } - if (error) { - response.error = error; - } + return response; + } else { + const response: Zigbee2MQTTResponse = { + data, // valid from error check + status: 'ok', + }; - if (typeof request === 'object' && request['transaction'] !== undefined) { - response.transaction = request.transaction; - } + if (typeof request === 'object' && request.transaction !== undefined) { + response.transaction = request.transaction; + } - return response; + return response; + } } function parseJSON(value: string, fallback: string): KeyValue | string { @@ -146,48 +160,6 @@ function parseJSON(value: string, fallback: string): KeyValue | string { } } -function loadModuleFromText(moduleCode: string, name?: string): unknown { - const moduleFakePath = path.join(__dirname, '..', '..', 'data', 'extension', name || 'externally-loaded.js'); - const sandbox = { - require: require, - module: {}, - console, - setTimeout, - clearTimeout, - setInterval, - clearInterval, - setImmediate, - clearImmediate, - }; - vm.runInNewContext(moduleCode, sandbox, moduleFakePath); - /* eslint-disable-line */ // @ts-ignore - return sandbox.module.exports; -} - -function loadModuleFromFile(modulePath: string): unknown { - const moduleCode = fs.readFileSync(modulePath, {encoding: 'utf8'}); - return loadModuleFromText(moduleCode); -} - -export function* loadExternalConverter(moduleName: string): Generator { - let converter; - - if (moduleName.endsWith('.js')) { - converter = loadModuleFromFile(data.joinPath(moduleName)); - } else { - // eslint-disable-next-line @typescript-eslint/no-require-imports - converter = require(moduleName); - } - - if (Array.isArray(converter)) { - for (const item of converter) { - yield item; - } - } else { - yield converter; - } -} - /** * Delete all keys from passed object that have null/undefined values. * @@ -196,8 +168,12 @@ export function* loadExternalConverter(moduleName: string): Generator '_' + y.toLowerCase()) - .replace(/^_/, '') - .replace('_i_d', '_id'); -} - function charRange(start: string, stop: string): number[] { const result = []; for (let idx = start.charCodeAt(0), end = stop.charCodeAt(0); idx <= end; ++idx) { @@ -306,23 +262,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.enabled) { 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 +285,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; @@ -407,8 +346,8 @@ export function isLightExpose(expose: zhc.Expose): expose is zhc.Light { return expose.type === 'light'; } -function getScenes(entity: zh.Endpoint | zh.Group): Scene[] { - const scenes: {[id: number]: Scene} = {}; +function getScenes(entity: zh.Endpoint | zh.Group): Zigbee2MQTTScene[] { + const scenes: {[id: number]: Zigbee2MQTTScene} = {}; const endpoints = isZHEndpoint(entity) ? [entity] : entity.members; const groupID = isZHEndpoint(entity) ? 0 : entity.groupID; @@ -444,12 +383,8 @@ export default { getObjectProperty, getResponse, parseJSON, - loadModuleFromText, - loadModuleFromFile, removeNullPropertiesFromObject, toNetworkAddressHex, - toSnakeCaseString, - toSnakeCaseObject, isZHEndpoint, isZHGroup, hours, @@ -460,7 +395,6 @@ export default { sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, - availabilityPayload, getAllFiles, filterProperties, flatten, diff --git a/lib/util/yaml.ts b/lib/util/yaml.ts index 2c77db2c45..877decab02 100644 --- a/lib/util/yaml.ts +++ b/lib/util/yaml.ts @@ -1,4 +1,5 @@ -import fs from 'fs'; +import assert from 'node:assert'; +import fs from 'node:fs'; import equals from 'fast-deep-equal/es6'; import yaml, {YAMLException} from 'js-yaml'; @@ -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/lib/zigbee.ts b/lib/zigbee.ts index 4ead28e382..e558d03aa5 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -1,4 +1,4 @@ -import {randomInt} from 'crypto'; +import {randomInt} from 'node:crypto'; import bind from 'bind-decorator'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -29,16 +29,15 @@ export default class Zigbee { async start(): Promise<'reset' | 'resumed' | 'restored'> { const infoHerdsman = await utils.getDependencyVersion('zigbee-herdsman'); logger.info(`Starting zigbee-herdsman (${infoHerdsman.version})`); + const panId = settings.get().advanced.pan_id; + const extPanId = settings.get().advanced.ext_pan_id; + const networkKey = settings.get().advanced.network_key; const herdsmanSettings = { network: { - panID: settings.get().advanced.pan_id === 'GENERATE' ? this.generatePanID() : (settings.get().advanced.pan_id as number), - extendedPanID: - settings.get().advanced.ext_pan_id === 'GENERATE' ? this.generateExtPanID() : (settings.get().advanced.ext_pan_id as number[]), + panID: panId === 'GENERATE' ? this.generatePanID() : panId, + extendedPanID: extPanId === 'GENERATE' ? this.generateExtPanID() : extPanId, channelList: [settings.get().advanced.channel], - networkKey: - settings.get().advanced.network_key === 'GENERATE' - ? this.generateNetworkKey() - : (settings.get().advanced.network_key as number[]), + networkKey: networkKey === 'GENERATE' ? this.generateNetworkKey() : networkKey, }, databasePath: data.joinPath('database.db'), databaseBackupPath: data.joinPath('database.db.backup'), @@ -72,9 +71,7 @@ export default class Zigbee { throw error; } - for (const device of this.devicesIterator(utils.deviceNotCoordinator)) { - await device.resolveDefinition(); - } + await this.resolveDevicesDefinitions(); this.herdsman.on('adapterDisconnected', () => this.eventBus.emitAdapterDisconnected()); this.herdsman.on('lastSeenChanged', (data: ZHEvents.LastSeenChangedPayload) => { @@ -219,10 +216,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(); @@ -233,21 +226,23 @@ export default class Zigbee { return this.herdsman.getPermitJoin(); } - getPermitJoinTimeout(): number | undefined { - return this.herdsman.getPermitJoinTimeout(); + getPermitJoinEnd(): number | undefined { + return this.herdsman.getPermitJoinEnd(); } - 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); + } + + async resolveDevicesDefinitions(ignoreCache: boolean = false): Promise { + for (const device of this.devicesIterator(utils.deviceNotCoordinator)) { + await device.resolveDefinition(ignoreCache); } } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 62963f26ff..0000000000 --- a/package-lock.json +++ /dev/null @@ -1,8610 +0,0 @@ -{ - "name": "zigbee2mqtt", - "version": "1.42.0", - "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/package.json b/package.json index f36df0b9bf..2c804a84b5 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": { @@ -48,53 +48,51 @@ "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", + "mqtt": "^5.10.3", "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" + "zigbee-herdsman": "3.0.4", + "zigbee-herdsman-converters": "21.7.0", + "zigbee2mqtt-frontend": "0.9.1" }, "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", + "@eslint/core": "^0.9.1", + "@eslint/js": "^9.17.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/node": "^22.10.2", "@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": "^9.17.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", - "prettier": "^3.3.3", + "prettier": "^3.4.2", "tmp": "^0.2.3", "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0" + "typescript-eslint": "^8.18.0" }, - "overrides": { - "zigbee-herdsman-converters": { + "pnpm": { + "overrides": { "zigbee-herdsman": "$zigbee-herdsman" } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..4e0c5dc861 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5738 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + zigbee-herdsman: 3.0.4 + +importers: + + .: + dependencies: + ajv: + specifier: ^8.17.1 + version: 8.17.1 + bind-decorator: + specifier: ^1.0.11 + version: 1.0.11 + debounce: + 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 + 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 + moment: + specifier: ^2.30.1 + version: 2.30.1 + mqtt: + specifier: ^5.10.3 + version: 5.10.3 + 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 + winston: + specifier: ^3.17.0 + version: 3.17.0 + winston-syslog: + specifier: ^2.7.1 + version: 2.7.1(winston@3.17.0) + winston-transport: + specifier: ^4.9.0 + version: 4.9.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 + zigbee-herdsman: + specifier: 3.0.4 + version: 3.0.4 + zigbee-herdsman-converters: + specifier: 21.7.0 + version: 21.7.0 + zigbee2mqtt-frontend: + specifier: 0.9.1 + version: 0.9.1 + optionalDependencies: + sd-notify: + specifier: ^2.8.0 + version: 2.8.0 + devDependencies: + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@babel/plugin-proposal-decorators': + specifier: ^7.25.9 + version: 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': + specifier: ^7.26.0 + version: 7.26.0(@babel/core@7.26.0) + '@babel/preset-typescript': + specifier: ^7.26.0 + version: 7.26.0(@babel/core@7.26.0) + '@eslint/core': + specifier: ^0.9.1 + version: 0.9.1 + '@eslint/js': + specifier: ^9.17.0 + version: 9.17.0 + '@ianvs/prettier-plugin-sort-imports': + specifier: ^4.4.0 + version: 4.4.0(prettier@3.4.2) + '@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.14 + version: 29.5.14 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 + '@types/node': + specifier: ^22.10.2 + version: 22.10.2 + '@types/object-assign-deep': + specifier: ^0.4.3 + version: 0.4.3 + '@types/readable-stream': + 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.13 + version: 8.5.13 + babel-jest: + specifier: ^29.7.0 + version: 29.7.0(@babel/core@7.26.0) + eslint: + specifier: ^9.17.0 + version: 9.17.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.17.0) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.10.2) + prettier: + specifier: ^3.4.2 + version: 3.4.2 + tmp: + specifier: ^0.2.3 + version: 0.2.3 + typescript: + specifier: ^5.7.2 + version: 5.7.2 + typescript-eslint: + specifier: ^8.18.0 + version: 8.18.0(eslint@9.17.0)(typescript@5.7.2) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@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.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.26.3': + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@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.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@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.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + engines: {node: '>=6.9.0'} + + '@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.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@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.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@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/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + 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.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.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.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.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@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 + + '@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.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.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + 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: + '@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.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: + '@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.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.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.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.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.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.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.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.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@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.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.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.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.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.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.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.26.3': + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.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.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.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.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.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.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.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.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.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.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.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.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.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.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.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.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.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.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.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.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.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.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.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.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.26.3': + resolution: {integrity: sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@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.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.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.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + 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.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + 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.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + 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.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@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.17.0': + resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + 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'} + + '@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 + 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.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + 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/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@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.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + + '@types/object-assign-deep@0.4.3': + resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==} + + '@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.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + + '@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.18.0': + resolution: {integrity: sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==} + 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: '>=4.8.4 <5.8.0' + + '@typescript-eslint/parser@8.18.0': + resolution: {integrity: sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/scope-manager@8.18.0': + resolution: {integrity: sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.18.0': + resolution: {integrity: sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/types@8.18.0': + resolution: {integrity: sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.18.0': + resolution: {integrity: sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.18.0': + resolution: {integrity: sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.18.0': + resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} + 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.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + 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@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==} + + 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.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + 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.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + 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==} + + 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.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} + + 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.3: + resolution: {integrity: sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==} + 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.30001688: + resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==} + + 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==} + + 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} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + + 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.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} + 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.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + 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'} + + 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.73: + resolution: {integrity: sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==} + + 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@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.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.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.17.0: + resolution: {integrity: sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + 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: + 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} + + 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==} + + 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.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + 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.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + 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@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'} + + 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.16.0: + resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} + 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 + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + 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.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + 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@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'} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mqtt-packet@9.0.1: + resolution: {integrity: sha512-koZF1V/X2RZUI6uD9wN5OK1JxxcG1ofAR4H3LjCw1FkeKzruZQ26aAA6v2m1lZyWONZIR5wMMJFrZJDRNzbiQw==} + + mqtt@5.10.3: + resolution: {integrity: sha512-hA/6YrUS4fywhBGCjH/XXUuLeueJiPqruVVWjK2A24Ma4KcWfZ/x8x07aoesBV+HXDWBC08tbT4IWfSXNW0Jtw==} + 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.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + 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.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + 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.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + 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'} + + 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==} + + 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.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + 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.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.9: + resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} + 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'} + + 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'} + + 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@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'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + 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-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.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + 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.18.0: + resolution: {integrity: sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + engines: {node: '>=14.17'} + hasBin: true + + 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==} + 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.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + 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@21.7.0: + resolution: {integrity: sha512-Dvzg7Du8dvTGZQJhy9Nf3SbXPHt0pRu/+8fVC7o6sfCdPZHyMz4fWJit5WIVD8dwaA8gwOTLzFzZtkzgkaWZ1g==} + + zigbee-herdsman@3.0.4: + resolution: {integrity: sha512-HhC1yqbP/HY2akNH+K4bF6Wu4rAR/OXJhT8L1XBQPkUgAM14hknCEVbFWYF6PKxaLvPTuCB265W5lcUy/ZaryQ==} + + zigbee2mqtt-frontend@0.9.1: + resolution: {integrity: sha512-obpvrDkTqiK87c9M8fJNqR0zbIxBoCp5X4b8i0WsS2rEL60XEF76d8P7eY+4pXkvrmkN4nd794+yNyDnqUxfdg==} + engines: {node: '>=18'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.3': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@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.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.3(@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.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@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.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + + '@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.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.0)': + dependencies: + '@babel/core': 7.26.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.3(@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.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.0(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@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.26.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@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 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.3(@babel/core@7.26.0)': + dependencies: + '@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-systemjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@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 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@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.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@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-sticky-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typescript@7.26.3(@babel/core@7.26.0)': + dependencies: + '@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/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.26.3(@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.26.3(@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.26.3(@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.3 + '@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.26.3(@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.26.3(@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.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.3 + esutils: 2.0.3 + + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': + dependencies: + '@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.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.3': + 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': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0)': + dependencies: + eslint: 9.17.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.1': + dependencies: + '@eslint/object-schema': 2.1.5 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.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.17.0': {} + + '@eslint/object-schema@2.1.5': {} + + '@eslint/plugin-kit@0.2.4': + dependencies: + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@ianvs/prettier-plugin-sort-imports@4.4.0(prettier@3.4.2)': + dependencies: + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + prettier: 3.4.2 + 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.10.2 + 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.10.2 + 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.10.2) + 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.10.2 + 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.10.2 + 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.10.2 + 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.26.0 + '@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.10.2 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.8': + 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.26.3 + '@babel/types': 7.26.3 + '@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.26.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.3 + + '@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.10.2 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.10.2 + + '@types/http-errors@2.0.4': {} + + '@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.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/js-yaml@4.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/mime@1.3.5': {} + + '@types/node@22.10.2': + dependencies: + undici-types: 6.20.0 + + '@types/object-assign-deep@0.4.3': {} + + '@types/readable-stream@4.0.18': + dependencies: + '@types/node': 22.10.2 + 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.2 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.10.2 + '@types/send': 0.17.4 + + '@types/stack-utils@2.0.3': {} + + '@types/triple-beam@1.3.5': {} + + '@types/ws@8.5.13': + dependencies: + '@types/node': 22.10.2 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/type-utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 + eslint: 9.17.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 + debug: 4.4.0 + eslint: 9.17.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.18.0': + dependencies: + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 + + '@typescript-eslint/type-utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + debug: 4.4.0 + eslint: 9.17.0 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.18.0': {} + + '@typescript-eslint/typescript-estree@8.18.0(typescript@5.7.2)': + dependencies: + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 + debug: 4.4.0 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + eslint: 9.17.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.18.0': + dependencies: + '@typescript-eslint/types': 8.18.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.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + 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.3 + 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@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: {} + + babel-jest@29.7.0(@babel/core@7.26.0): + dependencies: + '@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.26.0) + 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.9 + '@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.9 + '@babel/types': 7.26.3 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + 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.26.0(@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.26.0) + + balanced-match@1.0.2: {} + + 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.18 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.5.2 + + bonjour-service@1.3.0: + 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.3: + dependencies: + caniuse-lite: 1.0.30001688 + electron-to-chromium: 1.5.73 + node-releases: 2.0.19 + update-browserslist-db: 1.1.1(browserslist@4.24.3) + + 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.30001688: {} + + 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 + + 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 + + convert-source-map@2.0.0: {} + + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.3 + + core-util-is@1.0.3: {} + + create-jest@29.7.0(@types/node@22.10.2): + 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.10.2) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + 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: + ms: 2.0.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + dedent@1.5.3: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + 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.73: {} + + 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@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@9.1.0(eslint@9.17.0): + dependencies: + eslint: 9.17.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.2.0: {} + + eslint@9.17.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.17.0 + '@eslint/plugin-kit': 0.2.4 + '@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.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.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 + 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 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.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.6 + 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 + + 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-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.26.0 + tslib: 2.8.1 + + fast-uri@3.0.3: {} + + 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.2 + keyv: 4.5.4 + + flatted@3.3.2: {} + + fn.name@1.1.0: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + 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@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 + + 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.16.0: + 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.26.0 + '@babel/parser': 7.26.3 + '@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.26.0 + '@babel/parser': 7.26.3 + '@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.4.0 + 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.10.2 + 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.10.2): + 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.10.2) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.10.2) + 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.10.2): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + 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.10.2 + 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.10.2 + 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.10.2 + 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.26.2 + '@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.10.2 + 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.9 + resolve.exports: 2.0.3 + 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.10.2 + 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.10.2 + 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.26.0 + '@babel/generator': 7.26.3 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.3 + '@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.26.0) + 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.10.2 + 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.10.2 + 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.10.2 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.10.2): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.10.2) + 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: {} + + jsesc@3.1.0: {} + + 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.7.0: + 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.2: {} + + 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@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: {} + + moment@2.30.1: {} + + mqtt-packet@9.0.1: + dependencies: + bl: 6.0.16 + debug: 4.4.0 + process-nextick-args: 2.0.1 + transitivePeerDependencies: + - supports-color + + mqtt@5.10.3: + dependencies: + '@types/readable-stream': 4.0.18 + '@types/ws': 8.5.13 + commist: 3.2.0 + concat-stream: 2.0.0 + debug: 4.4.0 + help-me: 5.0.0 + lru-cache: 10.4.3 + minimist: 1.2.8 + mqtt-packet: 9.0.1 + 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.19: {} + + 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.4.0 + 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.26.2 + 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.2 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + 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.4.2: {} + + 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 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + queue-microtask@1.2.3: {} + + 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.26.0 + + regexpu-core@6.2.0: + 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.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + 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.3: {} + + resolve@1.22.9: + dependencies: + is-core-module: 2.16.0 + 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 + + 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: {} + + 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@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: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-hex@1.0.0: {} + + throttleit@2.1.0: {} + + thunky@1.1.0: {} + + tmp@0.2.3: {} + + tmpl@1.0.5: {} + + 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.4.3(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + tslib@2.8.1: {} + + 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.18.0(eslint@9.17.0)(typescript@5.7.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + eslint: 9.17.0 + typescript: 5.7.2 + transitivePeerDependencies: + - supports-color + + typescript@5.7.2: {} + + undici-types@6.20.0: {} + + 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.3): + dependencies: + browserslist: 4.24.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + 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.17.0): + dependencies: + glossy: 0.1.7 + triple-beam: 1.4.1 + winston: 3.17.0 + winston-transport: 4.9.0 + optionalDependencies: + unix-dgram: 2.0.6 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + 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.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.9.0 + + word-wrap@1.2.5: {} + + worker-timers-broker@6.1.8: + dependencies: + '@babel/runtime': 7.26.0 + fast-unique-numbers: 8.0.13 + tslib: 2.8.1 + worker-timers-worker: 7.0.71 + + worker-timers-worker@7.0.71: + dependencies: + '@babel/runtime': 7.26.0 + tslib: 2.8.1 + + worker-timers@7.1.8: + dependencies: + '@babel/runtime': 7.26.0 + tslib: 2.8.1 + 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@21.7.0: + dependencies: + buffer-crc32: 1.0.0 + iconv-lite: 0.6.3 + semver: 7.6.3 + zigbee-herdsman: 3.0.4 + transitivePeerDependencies: + - supports-color + + zigbee-herdsman@3.0.4: + dependencies: + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/stream': 12.0.0 + bonjour-service: 1.3.0 + debounce: 2.2.0 + fast-deep-equal: 3.1.3 + mixin-deep: 2.0.1 + slip: 1.0.2 + transitivePeerDependencies: + - supports-color + + zigbee2mqtt-frontend@0.9.1: {} 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/assets/mock-external-converter-multiple.js b/test/assets/external_converters/mock-external-converter-multiple.js similarity index 66% rename from test/assets/mock-external-converter-multiple.js rename to test/assets/external_converters/mock-external-converter-multiple.js index d48543f7d9..264e7fc731 100644 --- a/test/assets/mock-external-converter-multiple.js +++ b/test/assets/external_converters/mock-external-converter-multiple.js @@ -1,19 +1,7 @@ -const homeassistantSwitch = { - type: 'switch', - object_id: 'switch', - discovery_payload: { - payload_off: 'OFF', - payload_on: 'ON', - value_template: '{{ value_json.state }}', - command_topic: true, - }, -}; - const mockDevices = [ { mock: 1, model: 'external_converters_device_1', - homeassistant: [homeassistantSwitch], zigbeeModel: ['external_converter_device_1'], vendor: 'external_1', description: 'external_1', diff --git a/test/assets/mock-external-converter.js b/test/assets/external_converters/mock-external-converter.js similarity index 100% rename from test/assets/mock-external-converter.js rename to test/assets/external_converters/mock-external-converter.js diff --git a/test/assets/external_extensions/example2Extension.js b/test/assets/external_extensions/example2Extension.js new file mode 100644 index 0000000000..258de01b71 --- /dev/null +++ b/test/assets/external_extensions/example2Extension.js @@ -0,0 +1,16 @@ +class Example2 { + constructor(zigbee, mqtt, state, publishEntityState, eventBus) { + this.mqtt = mqtt; + this.mqtt.publish('example2/extension', 'call2 from constructor'); + } + + start() { + this.mqtt.publish('example2/extension', 'call2 from start'); + } + + stop() { + this.mqtt.publish('example/extension', 'call2 from stop'); + } +} + +module.exports = Example2; diff --git a/test/assets/exampleExtension.js b/test/assets/external_extensions/exampleExtension.js similarity index 61% rename from test/assets/exampleExtension.js rename to test/assets/external_extensions/exampleExtension.js index 149cc96f48..aec02a8934 100644 --- a/test/assets/exampleExtension.js +++ b/test/assets/external_extensions/exampleExtension.js @@ -5,7 +5,11 @@ class Example { } start() { - this.mqtt.publish('example/extension', 'test'); + this.mqtt.publish('example/extension', 'call from start'); + } + + stop() { + this.mqtt.publish('example/extension', 'call from stop'); } } diff --git a/test/bind.test.js b/test/bind.test.js deleted file mode 100644 index 35924b6826..0000000000 --- a/test/bind.test.js +++ /dev/null @@ -1,943 +0,0 @@ -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) => { - for (const endpoint of device.endpoints) { - endpoint.read.mockClear(); - endpoint.write.mockClear(); - endpoint.configureReporting.mockClear(); - endpoint.bind = jest.fn(); - endpoint.bind.mockClear(); - endpoint.unbind.mockClear(); - } - }; - - let resetExtension = async () => { - await controller.enableDisableExtension(false, 'Bind'); - await controller.enableDisableExtension(true, 'Bind'); - }; - - beforeAll(async () => { - jest.useFakeTimers(); - controller = new Controller(jest.fn(), jest.fn()); - await controller.start(); - await flushPromises(); - }); - - 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(); - await resetExtension(); - MQTT.publish.mockClear(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - 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); - - // Setup - 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); - 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'})); - await flushPromises(); - expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); - expect(endpoint.bind).toHaveBeenCalledTimes(4); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); - expect(target.configureReporting).toHaveBeenCalledTimes(3); - expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, - ]); - expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - ]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, - 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)); - - // Teardown - target.binds = originalTargetBinds; - 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); - - // 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 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); - 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'})); - await flushPromises(); - - expect(target.read).toHaveBeenCalledWith('lightingColorCtrl', ['colorCapabilities']); - expect(endpoint.bind).toHaveBeenCalledTimes(4); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); - expect(target.configureReporting).toHaveBeenCalledTimes(2); - expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, - ]); - // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - ]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, - 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)); - - // Teardown - target.binds = originalTargetBinds; - target.inputClusters = originalTargetInputClusters; - target.outputClusters = originalTargetOutputClusters; - 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); - - // Setup - 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); - mockClear(device); - target.configureReporting.mockImplementationOnce(() => { - throw new Error('timeout'); - }); - const originalTargetCR = target.configuredReportings; - target.configuredReportings = [ - { - cluster: {name: 'genLevelCtrl'}, - attribute: {name: 'currentLevel', ID: 0}, - minimumReportInterval: 0, - maximumReportInterval: 3600, - reportableChange: 0, - }, - ]; - - MQTT.events.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); - expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); - expect(target.configureReporting).toHaveBeenCalledTimes(2); - expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, - ]); - // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); - expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, - ]); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({ - transaction: '1234', - data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], failed: []}, - 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)); - - // Teardown - target.configuredReportings = originalTargetCR; - target.binds = originalTargetBinds; - 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); - mockClear(device); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('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/request/device/bind', stringify({from: 'remote', to: 'button'})); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(0); - expect(MQTT.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}, - expect.any(Function), - ); - }); - - it('Should unbind', async () => { - const device = zigbeeHerdsman.devices.remote; - const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); - - // setup - target.configureReporting.mockImplementationOnce(() => { - throw new Error('timeout'); - }); - 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)}, - ]; - - 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'})); - 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); - - // Disable reporting - expect(target.configureReporting).toHaveBeenCalledTimes(3); - expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 5, reportableChange: 1}, - ]); - expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ - {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, - {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( - 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - - // Teardown - target.binds = originalTargetBinds; - 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); - mockClear(device); - endpoint.unbind.mockClear(); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - target.members.push(target1Member); - target1Member.configureReporting.mockClear(); - mockClear(device); - MQTT.events.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); - expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); - expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); - expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, - ]); - expect(MQTT.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}, - expect.any(Function), - ); - - // Should configure reproting for device added to group - target1Member.configureReporting.mockClear(); - await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb'); - await flushPromises(); - expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, - ]); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, - ]); - }); - - 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); - target.members.push(target1Member); - target1Member.configureReporting.mockClear(); - mockClear(device); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/unbind', - stringify({data: {from: 'remote', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - target.members.push(target1Member); - - // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. - const originalBinds = endpoint.binds; - endpoint.binds = []; - - target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.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})); - await flushPromises(); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - // with skip_disable_reporting set to false, we don't expect it to reconfigure reporting - expect(target1Member.configureReporting).toHaveBeenCalledTimes(0); - endpoint.binds = originalBinds; - }); - - 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); - target.members.push(target1Member); - - // The device unbind mock doesn't remove binds, therefore remove them here already otherwise configure reporiting is not disabled. - const originalBinds = endpoint.binds; - endpoint.binds = []; - - target1Member.binds = [ - {cluster: {name: 'genLevelCtrl'}, target: zigbeeHerdsman.devices.coordinator.getEndpoint(1)}, - {cluster: {name: 'genOnOff'}, target: zigbeeHerdsman.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})); - await flushPromises(); - expect(endpoint.unbind).toHaveBeenCalledTimes(3); - // with skip_disable_reporting set, we expect it to reconfigure reporting - expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ - {attribute: 'currentLevel', maximumReportInterval: 65535, minimumReportInterval: 5, reportableChange: 1}, - ]); - expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ - {attribute: 'onOff', maximumReportInterval: 65535, minimumReportInterval: 0, reportableChange: 0}, - ]); - endpoint.binds = originalBinds; - }); - - it('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/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( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Should log when bind fails', async () => { - logger.error.mockClear(); - const device = zigbeeHerdsman.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'})); - await flushPromises(); - expect(endpoint.bind).toHaveBeenCalledTimes(3); - expect(MQTT.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', - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote/ep2', to: 'wall_switch_double/right', clusters: ['genOnOff'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'temperature_sensor', to: 'heating_actuator', clusters: ['msTemperatureMeasurement'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.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( - 'zigbee2mqtt/bridge/response/device/bind', - stringify({data: {from: 'remote/ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('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/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( - 'zigbee2mqtt/bridge/response/device/unbind', - stringify({ - data: {from: 'remote', to: 'default_bind_group', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, - status: 'ok', - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); - await flushPromises(); - expect(MQTT.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", - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote/not_existing_endpoint', to: 'bulb_color'})); - await flushPromises(); - expect(MQTT.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'", - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'})); - await flushPromises(); - expect(MQTT.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", - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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); - mockClear(device); - MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color/not_existing_endpoint'})); - await flushPromises(); - expect(MQTT.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'", - }), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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}; - const payload = { - data, - cluster: 'manuSpecificPhilips', - device: remote, - endpoint: remote.getEndpoint(2), - type: 'commandHueNotification', - linkquality: 10, - groupID: 0, - }; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.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 data = {action: 'recall_2_row_1'}; - zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read.mockImplementationOnce(() => { - throw new Error('failed'); - }); - const payload = { - data, - cluster: 'genScenes', - device: remote, - endpoint: remote.getEndpoint(10), - type: 'commandRecall', - linkquality: 10, - groupID: 0, - }; - await zigbeeHerdsman.events.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', - ]); - }); - - 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; - const data = {stepmode: 0, stepsize: 43, transtime: 5}; - const payload = { - data, - cluster: 'genLevelCtrl', - device: remote, - endpoint: remote.getEndpoint(1), - type: 'commandStepWithOnOff', - linkquality: 10, - groupID: 15071, - }; - await zigbeeHerdsman.events.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']); - - // Should also only debounce once - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(debounce).toHaveBeenCalledTimes(2); - expect(zigbeeHerdsman.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); - }); -}); diff --git a/test/controller.test.js b/test/controller.test.js deleted file mode 100644 index b71723c37d..0000000000 --- a/test/controller.test.js +++ /dev/null @@ -1,1005 +0,0 @@ -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 = [ - zigbeeHerdsman.permitJoin, - MQTT.end, - zigbeeHerdsman.stop, - logger.debug, - MQTT.publish, - MQTT.connect, - zigbeeHerdsman.devices.bulb_color.removeFromNetwork, - zigbeeHerdsman.devices.bulb.removeFromNetwork, - logger.error, -]; - -const fs = require('fs'); - -const LOG_MQTT_NS = 'z2m:mqtt'; - -jest.mock( - 'sd-notify', - () => { - return { - watchdogInterval: () => { - return 3000; - }, - startWatchdogMode: (interval) => {}, - stopWatchdogMode: () => {}, - ready: () => {}, - stopping: () => {}, - }; - }, - {virtual: true}, -); - -describe('Controller', () => { - let controller; - let mockExit; - - beforeAll(async () => { - jest.useFakeTimers(); - }); - - beforeEach(() => { - MQTT.restoreOnMock(); - zigbeeHerdsman.returnDevices.splice(0); - mockExit = jest.fn(); - data.writeDefaultConfiguration(); - settings.reRead(); - controller = new Controller(jest.fn(), mockExit); - mocksClear.forEach((m) => m.mockClear()); - settings.reRead(); - data.writeDefaultState(); - }); - - afterAll(async () => { - jest.useRealTimers(); - }); - - it('Start controller', async () => { - settings.set(['advanced', 'transmit_power'], 14); - await controller.start(); - expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({ - network: { - panID: 6754, - extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], - channelList: [11], - networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], - }, - databasePath: path.join(data.mockDir, 'database.db'), - databaseBackupPath: path.join(data.mockDir, 'database.db.backup'), - backupPath: path.join(data.mockDir, 'coordinator_backup.json'), - acceptJoiningDeviceHandler: expect.any(Function), - 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(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)', - ); - 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', { - will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, - }); - expect(MQTT.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)); - }); - - 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'); - const key = tmp.fileSync().name; - fs.writeFileSync(key, 'key'); - const cert = tmp.fileSync().name; - fs.writeFileSync(cert, 'cert'); - - const configuration = { - base_topic: 'zigbee2mqtt', - server: 'mqtt://localhost', - keepalive: 30, - ca, - cert, - key, - password: 'pass', - user: 'user1', - client_id: 'my_client_id', - reject_unauthorized: false, - version: 5, - }; - settings.set(['mqtt'], configuration); - await controller.start(); - await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); - const expected = { - will: {payload: Buffer.from('offline'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, - keepalive: 30, - ca: Buffer.from([99, 97]), - key: Buffer.from([107, 101, 121]), - cert: Buffer.from([99, 101, 114, 116]), - password: 'pass', - username: 'user1', - clientId: 'my_client_id', - rejectUnauthorized: false, - protocolVersion: 5, - }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); - }); - - it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => { - settings.set(['advanced', 'network_key'], 'GENERATE'); - settings.set(['advanced', 'pan_id'], 'GENERATE'); - 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(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)); - }); - - it('Start controller should publish cached states', async () => { - data.writeDefaultState(); - await controller.start(); - await flushPromises(); - expect(MQTT.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)); - }); - - it('Start controller should not publish cached states when disabled', async () => { - settings.set(['advanced', 'cache_state_send_on_startup'], false); - data.writeDefaultState(); - await controller.start(); - await flushPromises(); - const publishedTopics = MQTT.publish.mock.calls.map((m) => m[0]); - expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote'])); - }); - - it('Start controller should not publish cached states when cache_state is false', async () => { - settings.set(['advanced', 'cache_state'], false); - data.writeDefaultState(); - await controller.start(); - await flushPromises(); - expect(MQTT.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)); - }); - - it('Log when MQTT client is unavailable', async () => { - await controller.start(); - await flushPromises(); - logger.error.mockClear(); - controller.mqtt.client.reconnecting = true; - jest.advanceTimersByTime(11 * 1000); - expect(logger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); - controller.mqtt.client.reconnecting = false; - }); - - it('Dont publish to mqtt when client is unavailable', async () => { - await controller.start(); - await flushPromises(); - logger.error.mockClear(); - controller.mqtt.client.reconnecting = true; - const device = controller.zigbee.resolveEntity('bulb'); - await controller.publishEntityState(device, { - state: 'ON', - brightness: 50, - color_temp: 370, - color: {r: 100, g: 50, b: 10}, - 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( - '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"}', - ); - controller.mqtt.client.reconnecting = false; - }); - - it('Load empty state when state file does not exist', async () => { - data.removeState(); - await controller.start(); - await flushPromises(); - 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(() => { - throw new Error('dummy'); - }); - await controller.start(); - await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); - }); - - it('Should remove device on blocklist on startup', async () => { - settings.set(['blocklist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); - await controller.start(); - await flushPromises(); - expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); - }); - - it('Start controller fails', async () => { - zigbeeHerdsman.start.mockImplementationOnce(() => { - throw new Error('failed'); - }); - await controller.start(); - expect(mockExit).toHaveBeenCalledTimes(1); - }); - - it('Start controller fails due to MQTT', async () => { - MQTT.on.mockImplementation((type, handler) => { - if (type === 'error') handler({message: 'addr not found'}); - }); - 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(mockExit).toHaveBeenCalledTimes(1); - 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); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.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'); - }); - await controller.start(); - await controller.stop(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.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'); - }); - await controller.start(); - await zigbeeHerdsman.events.adapterDisconnected(); - await flushPromises(); - expect(MQTT.end).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.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(); - - await controller.start(); - logger.debug.mockClear(); - await MQTT.events.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); - }); - - it('Skip MQTT messages on topic we published to', async () => { - const eventbus = controller.eventBus; - let spyEventbusEmitMQTTMessage = jest.spyOn(eventbus, 'emitMQTTMessage').mockImplementation(); - - await controller.start(); - logger.debug.mockClear(); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'zigbee2mqtt/skip-this-topic', message: 'skipped'}); - logger.debug.mockClear(); - await controller.mqtt.publish('skip-this-topic', '', {}); - await MQTT.events.message('zigbee2mqtt/skip-this-topic', 'skipped'); - expect(logger.debug).toHaveBeenCalledTimes(0); - }); - - it('On zigbee event message', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = { - device, - endpoint: device.getEndpoint(1), - type: 'attributeReport', - linkquality: 10, - cluster: 'genBasic', - data: {modelId: device.modelID}, - }; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( - 'debug', - `Received Zigbee message from 'bulb', type 'attributeReport', cluster 'genBasic', data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`, - 'z2m', - ); - }); - - it('On zigbee event message with group ID', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = { - device, - endpoint: device.getEndpoint(1), - type: 'attributeReport', - linkquality: 10, - groupID: 0, - cluster: 'genBasic', - data: {modelId: device.modelID}, - }; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(logger.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', - ); - }); - - it('Should add entities which are missing from configuration but are in database to configuration', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.notInSettings; - expect(settings.getDevice(device.ieeeAddr)).not.toBeUndefined(); - }); - - it('On zigbee deviceJoined', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = {device}; - await zigbeeHerdsman.events.deviceJoined(payload); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('acceptJoiningDeviceHandler reject device on blocklist', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.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; - settings.set(['blocklist'], ['123']); - const handler = zigbeeHerdsman.constructor.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; - settings.set(['passlist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.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; - settings.set(['passlist'], ['123']); - const handler = zigbeeHerdsman.constructor.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; - settings.set(['passlist'], [device.ieeeAddr]); - settings.set(['blocklist'], [device.ieeeAddr]); - const handler = zigbeeHerdsman.constructor.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; - 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 payload = {device}; - zigbeeHerdsman.events.deviceJoined(payload); - zigbeeHerdsman.events.deviceJoined(payload); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'device_connected', message: {friendly_name: 'bulb'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('On zigbee deviceInterview started', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = {device, status: 'started'}; - await zigbeeHerdsman.events.deviceInterview(payload); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'pairing', message: 'interview_started', meta: {friendly_name: 'bulb'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('On zigbee deviceInterview failed', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = {device, status: 'failed'}; - await zigbeeHerdsman.events.deviceInterview(payload); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/log', - stringify({type: 'pairing', message: 'interview_failed', meta: {friendly_name: 'bulb'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('On zigbee deviceInterview successful supported', async () => { - await controller.start(); - 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), - ); - }); - - it('On zigbee deviceInterview successful not supported', async () => { - await controller.start(); - 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), - ); - }); - - it('On zigbee event device announce', async () => { - await controller.start(); - const device = zigbeeHerdsman.devices.bulb; - const payload = {device}; - await zigbeeHerdsman.events.deviceAnnounce(payload); - 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'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('On zigbee event device leave (removed from database and settings)', async () => { - await controller.start(); - zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); - settings.set(['devices'], {}); - MQTT.publish.mockClear(); - const device = zigbeeHerdsman.devices.bulb; - const payload = {ieeeAddr: device.ieeeAddr}; - 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'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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(); - const payload = {ieeeAddr: device.ieeeAddr}; - 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'}}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - it('Publish entity state attribute output', async () => { - await controller.start(); - settings.set(['experimental', 'output'], 'attribute'); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); - await controller.publishEntityState(device, { - dummy: {1: 'yes', 2: 'no'}, - color: {r: 100, g: 50, b: 10}, - state: 'ON', - test: undefined, - test1: null, - color_temp: 370, - 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)); - }); - - 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'); - 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( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - 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'); - 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( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 200}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - it('Publish entity state attribute_json output filtered (device_options)', async () => { - 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'); - 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( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 200}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - 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(); - - const device = controller.zigbee.resolveEntity('bulb'); - 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(); - - 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( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - it('Publish entity state attribute_json output filtered cache (device_options)', async () => { - await controller.start(); - settings.set(['advanced', 'output'], 'attribute_and_json'); - settings.set(['device_options', 'filtered_cache'], ['linkquality']); - MQTT.publish.mockClear(); - - const device = controller.zigbee.resolveEntity('bulb'); - 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(); - - 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( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - 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'); - await controller.publishEntityState(device, {state: 'ON'}); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({ - state: 'ON', - brightness: 50, - color_temp: 370, - linkquality: 99, - device: { - friendlyName: 'bulb', - model: 'LED1545G12', - ieeeAddr: '0x000b57fffec6a5b2', - networkAddress: 40369, - type: 'Router', - manufacturerID: 4476, - powerSource: 'Mains (single phase)', - dateCode: null, - softwareBuildID: null, - }, - }), - {qos: 0, retain: true}, - expect.any(Function), - ); - - // Unsupported device should have model "unknown" - device = controller.zigbee.resolveEntity('unsupported2'); - await controller.publishEntityState(device, {state: 'ON'}); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/unsupported2', - stringify({ - state: 'ON', - device: { - friendlyName: 'unsupported2', - model: 'notSupportedModelID', - ieeeAddr: '0x0017880104e45529', - networkAddress: 6536, - type: 'EndDevice', - manufacturerID: 0, - powerSource: 'Battery', - dateCode: null, - softwareBuildID: null, - }, - }), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - 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'); - await controller.publishEntityState(device, {state: 'ON'}); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), - {qos: 0, retain: false}, - expect.any(Function), - ); - }); - - 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'); - await controller.publishEntityState(device, {state: 'ON'}); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - 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'); - await controller.publishEntityState(device, {state: 'ON'}); - await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bulb', - stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), - {qos: 0, retain: true, properties: {messageExpiryInterval: 37}}, - expect.any(Function), - ); - }); - - it('Publish entity state no empty messages', async () => { - data.writeEmptyState(); - await controller.start(); - MQTT.publish.mockClear(); - const device = controller.zigbee.resolveEntity('bulb'); - await controller.publishEntityState(device, {}); - await flushPromises(); - expect(MQTT.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'); - 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({state: 'ON', brightness: 200}), - {qos: 0, retain: true}, - expect.any(Function), - ); - await controller.stop(); - expect(data.stateExists()).toBeFalsy(); - }); - - it('Shouldnt crash when it cannot save state', async () => { - data.removeState(); - await controller.start(); - logger.error.mockClear(); - controller.state.file = '/'; - await controller.state.save(); - expect(logger.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'); - 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)); - }); - - it('Should start when state is corrupted', async () => { - fs.writeFileSync(path.join(data.mockDir, 'state.json'), 'corrupted'); - await controller.start(); - await flushPromises(); - expect(controller.state.state).toStrictEqual({}); - }); - - it('Start controller with force_disable_retain', async () => { - settings.set(['mqtt', 'force_disable_retain'], true); - await controller.start(); - await flushPromises(); - expect(MQTT.connect).toHaveBeenCalledTimes(1); - const expected = { - will: {payload: Buffer.from('offline'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, - }; - expect(MQTT.connect).toHaveBeenCalledWith('mqtt://localhost', expected); - }); - - it('Should republish retained messages on MQTT reconnect', async () => { - await controller.start(); - MQTT.publish.mockClear(); - MQTT.events['connect'](); - await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers - expect(MQTT.publish).toHaveBeenCalledTimes(14); - expect(MQTT.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'](); - await flushPromises(); - await MQTT.events.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)); - }); - - it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => { - settings.set(['mqtt', 'force_disable_retain'], true); - await controller.mqtt.connect(); - MQTT.publish.mockClear(); - 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)); - }); - - 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(); - 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( - 'zigbee2mqtt/remote', - stringify({brightness: 255, last_seen: 1000}), - {qos: 0, retain: true}, - expect.any(Function), - ); - }); - - it('Should not publish last seen changes when reason is messageEmitted', 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: 'messageEmitted'}); - expect(MQTT.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 payload = { - device, - endpoint: device.getEndpoint(1), - type: 'attributeReport', - linkquality: 10, - cluster: 'genBasic', - data: {modelId: device.modelID}, - }; - await zigbeeHerdsman.events.message(payload); - await flushPromises(); - expect(logger.log).toHaveBeenCalledWith( - 'debug', - `Received Zigbee message from 'Coordinator', type 'attributeReport', cluster 'genBasic', data '{"modelId":null}' 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'); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 50, color_temp: 370, linkquality: 99, state: 'ON'}); - device.zh.isDeleted = true; - await controller.stop(); - expect(controller.state.state[device.ieeeAddr]).toStrictEqual(undefined); - }); - - it('EventBus should handle errors', async () => { - const eventbus = controller.eventBus; - const callback = jest.fn().mockImplementation(async () => { - throw new Error('Whoops!'); - }); - eventbus.onStateChange('test', callback); - eventbus.emitStateChange({}); - await flushPromises(); - expect(callback).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`EventBus error 'String/stateChange': Whoops!`); - }); -}); diff --git a/test/controller.test.ts b/test/controller.test.ts new file mode 100644 index 0000000000..67a65851d0 --- /dev/null +++ b/test/controller.test.ts @@ -0,0 +1,1046 @@ +import * as data from './mocks/data'; +import {mockLogger} from './mocks/logger'; +import {mockMQTT, mockMQTTConnectAsync, events as mockMQTTEvents} from './mocks/mqtt'; +import {flushPromises, JestMockAny} from './mocks/utils'; +import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from './mocks/zigbeeHerdsman'; + +import fs from 'node:fs'; +import path from 'node:path'; + +import stringify from 'json-stable-stringify-without-jsonify'; +import tmp from 'tmp'; + +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: jest.fn(() => 3000), + startWatchdogMode: jest.fn(), + stopWatchdogMode: jest.fn(), + ready: jest.fn(), + stopping: jest.fn(), + }; + }, + {virtual: true}, +); + +const mocksClear = [ + mockZHController.stop, + mockMQTT.endAsync, + mockMQTT.publishAsync, + mockMQTT.subscribeAsync, + mockMQTT.unsubscribeAsync, + mockMQTT.endAsync, + mockMQTTConnectAsync, + devices.bulb_color.removeFromNetwork, + devices.bulb.removeFromNetwork, + mockLogger.log, + mockLogger.debug, + mockLogger.info, + mockLogger.error, +]; + +describe('Controller', () => { + let controller: Controller; + let mockExit: JestMockAny; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + beforeEach(() => { + returnDevices.splice(0); + mockExit = jest.fn(); + data.writeDefaultConfiguration(); + settings.reRead(); + controller = new Controller(jest.fn(), mockExit); + mocksClear.forEach((m) => m.mockClear()); + settings.reRead(); + data.writeDefaultState(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + afterEach(async () => { + await controller?.stop(); + }); + + it('Start controller', async () => { + settings.set(['advanced', 'transmit_power'], 14); + await controller.start(); + expect(ZHController).toHaveBeenCalledWith({ + network: { + panID: 6754, + extendedPanID: [221, 221, 221, 221, 221, 221, 221, 221], + channelList: [11], + networkKey: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], + }, + databasePath: path.join(data.mockDir, 'database.db'), + databaseBackupPath: path.join(data.mockDir, 'database.db.backup'), + backupPath: path.join(data.mockDir, 'coordinator_backup.json'), + acceptJoiningDeviceHandler: expect.any(Function), + adapter: {concurrent: undefined, delay: undefined, disableLED: false, transmitPower: 14}, + serialPort: {baudRate: undefined, rtscts: undefined, path: '/dev/dummy'}, + }); + 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(mockLogger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); + expect(mockLogger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); + expect(mockMQTTConnectAsync).toHaveBeenCalledTimes(1); + expect(mockMQTTConnectAsync).toHaveBeenCalledWith('mqtt://localhost', { + will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + properties: {maximumPacketSize: 1048576}, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {retain: true, qos: 0}, + ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0}); + }); + + it('Start controller with specific MQTT settings', async () => { + const ca = tmp.fileSync().name; + fs.writeFileSync(ca, 'ca'); + const key = tmp.fileSync().name; + fs.writeFileSync(key, 'key'); + const cert = tmp.fileSync().name; + fs.writeFileSync(cert, 'cert'); + + const configuration = { + base_topic: 'zigbee2mqtt', + server: 'mqtt://localhost', + keepalive: 30, + ca, + cert, + key, + password: 'pass', + user: 'user1', + client_id: 'my_client_id', + reject_unauthorized: false, + version: 5, + maximum_packet_size: 20000, + }; + settings.set(['mqtt'], configuration); + await controller.start(); + await flushPromises(); + expect(mockMQTTConnectAsync).toHaveBeenCalledTimes(1); + const expected = { + 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]), + cert: Buffer.from([99, 101, 114, 116]), + password: 'pass', + username: 'user1', + clientId: 'my_client_id', + rejectUnauthorized: false, + protocolVersion: 5, + properties: {maximumPacketSize: 20000}, + }; + expect(mockMQTTConnectAsync).toHaveBeenCalledWith('mqtt://localhost', expected); + }); + + it('Should generate network_key, pan_id and ext_pan_id when set to GENERATE', async () => { + settings.set(['advanced', 'network_key'], 'GENERATE'); + settings.set(['advanced', 'pan_id'], 'GENERATE'); + settings.set(['advanced', 'ext_pan_id'], 'GENERATE'); + await controller.start(); + await flushPromises(); + 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)); + }); + + it('Start controller should publish cached states', async () => { + data.writeDefaultState(); + await controller.start(); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false}); + }); + + it('Start controller should not publish cached states when disabled', async () => { + settings.set(['advanced', 'cache_state_send_on_startup'], false); + data.writeDefaultState(); + await controller.start(); + await flushPromises(); + const publishedTopics = mockMQTT.publishAsync.mock.calls.map((m) => m[0]); + expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote'])); + }); + + it('Start controller should not publish cached states when cache_state is false', async () => { + settings.set(['advanced', 'cache_state'], false); + data.writeDefaultState(); + await controller.start(); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + `{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`, + {qos: 0, retain: true}, + ); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true}); + }); + + it('Log when MQTT client is unavailable', async () => { + await controller.start(); + await flushPromises(); + mockLogger.error.mockClear(); + // @ts-expect-error private + controller.mqtt.client.reconnecting = true; + jest.advanceTimersByTime(11 * 1000); + 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(); + 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', + brightness: 50, + color_temp: 370, + color: {r: 100, g: 50, b: 10}, + dummy: {1: 'yes', 2: 'no'}, + }); + await flushPromises(); + 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; + }); + + it('Load empty state when state file does not exist', async () => { + 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'], [devices.bulb_color.ieeeAddr]); + devices.bulb.removeFromNetwork.mockImplementationOnce(() => { + throw new Error('dummy'); + }); + await controller.start(); + await flushPromises(); + 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'], [devices.bulb_color.ieeeAddr]); + await controller.start(); + await flushPromises(); + expect(devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); + }); + + it('Start controller fails', async () => { + mockZHController.start.mockImplementationOnce(() => { + throw new Error('failed'); + }); + await controller.start(); + expect(mockExit).toHaveBeenCalledTimes(1); + }); + + it('Start controller fails due to MQTT connect error', async () => { + mockMQTTConnectAsync.mockImplementationOnce(() => { + throw new Error('addr not found'); + }); + await controller.start(); + await flushPromises(); + expect(mockLogger.error).toHaveBeenCalledWith('MQTT failed to connect, exiting... (addr not found)'); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1, false); + }); + + it('Start controller and stop with restart', async () => { + await controller.start(); + await controller.stop(true); + expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(0, true); + }); + + it('Start controller and stop', async () => { + mockZHController.stop.mockRejectedValueOnce('failed'); + await controller.start(); + await controller.stop(); + expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1, false); + }); + + it('Start controller adapter disconnects', async () => { + mockZHController.stop.mockRejectedValueOnce('failed'); + await controller.start(); + await mockZHEvents.adapterDisconnected(); + await flushPromises(); + expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1); + expect(mockZHController.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1, false); + }); + + it('Handles reconnecting to MQTT', async () => { + await controller.start(); + await flushPromises(); + + mockLogger.error.mockClear(); + mockLogger.info.mockClear(); + + mockMQTTEvents.error(new Error('ECONNRESET')); + mockMQTT.reconnecting = true; + expect(mockLogger.error).toHaveBeenCalledWith('MQTT error: ECONNRESET'); + + await jest.advanceTimersByTimeAsync(11000); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + + mockMQTT.reconnecting = false; + await mockMQTTEvents.connect(); + expect(mockLogger.info).toHaveBeenCalledWith('Connected to MQTT server'); + }); + + it('Handles reconnecting to MQTT after v5+ DISCONNECT', async () => { + settings.set(['mqtt', 'version'], 5); + await controller.start(); + await flushPromises(); + + mockLogger.error.mockClear(); + mockLogger.info.mockClear(); + mockMQTTEvents.disconnect({ + reasonCode: 149, + properties: {reasonString: 'Maximum packet size was exceeded'}, + }); + mockMQTT.disconnecting = true; + expect(mockLogger.error).toHaveBeenCalledWith('MQTT disconnect: reason 149 (Maximum packet size was exceeded)'); + + await jest.advanceTimersByTimeAsync(11000); + expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!'); + + mockMQTT.disconnecting = false; + await mockMQTTEvents.connect(); + expect(mockLogger.info).toHaveBeenCalledWith('Connected to MQTT server'); + }); + + it('Handles MQTT publish error', async () => { + await controller.start(); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + // fail on device_joined (has skipLog=false) + mockMQTT.publishAsync.mockImplementationOnce(mockMQTT.publishAsync.getMockImplementation()!).mockImplementationOnce(() => { + throw new Error('client disconnecting'); + }); + await mockZHEvents.deviceJoined({device: devices.bulb}); + await flushPromises(); + + expect(mockLogger.error).toHaveBeenCalledWith('MQTT server error: client disconnecting'); + }); + + it('Handle mqtt message', async () => { + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); + + await controller.start(); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('dummytopic', 'dummymessage'); + expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'dummytopic', message: 'dummymessage'}); + 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 () => { + // @ts-expect-error private + const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(); + + await controller.start(); + mockLogger.debug.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); + expect(spyEventbusEmitMQTTMessage).toHaveBeenCalledWith({topic: 'zigbee2mqtt/skip-this-topic', message: 'skipped'}); + mockLogger.debug.mockClear(); + // @ts-expect-error private + await controller.mqtt.publish('skip-this-topic', '', {}); + await mockMQTTEvents.message('zigbee2mqtt/skip-this-topic', 'skipped'); + expect(mockLogger.debug).toHaveBeenCalledTimes(0); + }); + + it('On zigbee event message', async () => { + await controller.start(); + const device = devices.bulb; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + 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', + ); + }); + + it('On zigbee event message with group ID', async () => { + await controller.start(); + const device = devices.bulb; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + groupID: 0, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + 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', + ); + }); + + it('Should add entities which are missing from configuration but are in database to configuration', async () => { + await controller.start(); + const device = devices.notInSettings; + expect(settings.getDevice(device.ieeeAddr)).not.toBeUndefined(); + }); + + it('On zigbee deviceJoined', async () => { + await controller.start(); + const device = devices.bulb; + const payload = {device}; + await mockZHEvents.deviceJoined(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('acceptJoiningDeviceHandler reject device on blocklist', async () => { + await controller.start(); + const device = devices.bulb; + settings.set(['blocklist'], [device.ieeeAddr]); + 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 = devices.bulb; + settings.set(['blocklist'], ['123']); + 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 = devices.bulb; + settings.set(['passlist'], [device.ieeeAddr]); + 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 = devices.bulb; + settings.set(['passlist'], ['123']); + 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 = devices.bulb; + settings.set(['passlist'], [device.ieeeAddr]); + settings.set(['blocklist'], [device.ieeeAddr]); + 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 = 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 = devices.bulb; + const payload = {device}; + mockZHEvents.deviceJoined(payload); + mockZHEvents.deviceJoined(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('On zigbee deviceInterview started', async () => { + await controller.start(); + const device = devices.bulb; + const payload = {device, status: 'started'}; + await mockZHEvents.deviceInterview(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('On zigbee deviceInterview failed', async () => { + await controller.start(); + const device = devices.bulb; + const payload = {device, status: 'failed'}; + await mockZHEvents.deviceInterview(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('On zigbee deviceInterview successful supported', async () => { + await controller.start(); + mockMQTT.publishAsync.mockClear(); + const device = devices.bulb; + const payload = {device, status: 'successful'}; + await mockZHEvents.deviceInterview(payload); + await flushPromises(); + expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publishAsync.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(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + }); + + it('On zigbee deviceInterview successful not supported', async () => { + await controller.start(); + mockMQTT.publishAsync.mockClear(); + const device = devices.unsupported; + const payload = {device, status: 'successful'}; + await mockZHEvents.deviceInterview(payload); + await flushPromises(); + expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event'); + const parsedMessage = JSON.parse(mockMQTT.publishAsync.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(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0}); + }); + + it('On zigbee event device announce', async () => { + await controller.start(); + const device = devices.bulb; + const payload = {device}; + await mockZHEvents.deviceAnnounce(payload); + await flushPromises(); + expect(mockLogger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('On zigbee event device leave (removed from database and settings)', async () => { + await controller.start(); + returnDevices.push('0x00124b00120144ae'); + settings.set(['devices'], {}); + mockMQTT.publishAsync.mockClear(); + const device = devices.bulb; + const payload = {ieeeAddr: device.ieeeAddr}; + await mockZHEvents.deviceLeave(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}), + {retain: false, qos: 0}, + ); + }); + + it('On zigbee event device leave (removed from database and NOT settings)', async () => { + await controller.start(); + returnDevices.push('0x00124b00120144ae'); + const device = devices.bulb; + mockMQTT.publishAsync.mockClear(); + const payload = {ieeeAddr: device.ieeeAddr}; + await mockZHEvents.deviceLeave(payload); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/event', + stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}), + {retain: false, qos: 0}, + ); + }); + + it('Publish entity state attribute output', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute'); + mockMQTT.publishAsync.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}, + state: 'ON', + test: undefined, + test1: null, + color_temp: 370, + brightness: 50, + }); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true}); + }); + + it('Publish entity state attribute_json output', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledTimes(5); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + ); + }); + + it('Publish entity state attribute_json output filtered', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}); + }); + + it('Publish entity state attribute_json output filtered (device_options)', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}); + }); + + it('Publish entity state attribute_json output filtered cache', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']); + mockMQTT.publishAsync.mockClear(); + + // @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(mockMQTT.publishAsync).toHaveBeenCalledTimes(5); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), + {qos: 0, retain: true}, + ); + }); + + it('Publish entity state attribute_json output filtered cache (device_options)', async () => { + await controller.start(); + settings.set(['advanced', 'output'], 'attribute_and_json'); + settings.set(['device_options', 'filtered_cache'], ['linkquality']); + mockMQTT.publishAsync.mockClear(); + + // @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(mockMQTT.publishAsync).toHaveBeenCalledTimes(5); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}), + {qos: 0, retain: true}, + ); + }); + + it('Publish entity state with device information', async () => { + await controller.start(); + settings.set(['mqtt', 'include_device_information'], true); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + let device = controller.zigbee.resolveEntity('bulb')!; + await controller.publishEntityState(device, {state: 'ON'}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({ + state: 'ON', + brightness: 50, + color_temp: 370, + linkquality: 99, + device: { + friendlyName: 'bulb', + model: 'LED1545G12', + ieeeAddr: '0x000b57fffec6a5b2', + networkAddress: 40369, + type: 'Router', + manufacturerID: 4476, + powerSource: 'Mains (single phase)', + }, + }), + {qos: 0, retain: true}, + ); + + // Unsupported device should have model "unknown" + // @ts-expect-error private + device = controller.zigbee.resolveEntity('unsupported2')!; + await controller.publishEntityState(device, {state: 'ON'}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/unsupported2', + stringify({ + state: 'ON', + device: { + friendlyName: 'unsupported2', + model: 'notSupportedModelID', + ieeeAddr: '0x0017880104e45529', + networkAddress: 6536, + type: 'EndDevice', + manufacturerID: 0, + powerSource: 'Battery', + }, + }), + {qos: 0, retain: false}, + ); + }); + + it('Should publish entity state without retain', async () => { + await controller.start(); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], false); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + await controller.publishEntityState(device, {state: 'ON'}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: false}, + ); + }); + + it('Should publish entity state with retain', async () => { + await controller.start(); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + await controller.publishEntityState(device, {state: 'ON'}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true}, + ); + }); + + it('Should publish entity state with expiring retention', async () => { + await controller.start(); + settings.set(['mqtt', 'version'], 5); + settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true); + settings.set(['devices', devices.bulb.ieeeAddr, 'retention'], 37); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + await controller.publishEntityState(device, {state: 'ON'}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}), + {qos: 0, retain: true, properties: {messageExpiryInterval: 37}}, + ); + }); + + it('Publish entity state no empty messages', async () => { + data.writeEmptyState(); + await controller.start(); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + const device = controller.zigbee.resolveEntity('bulb')!; + await controller.publishEntityState(device, {}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0); + }); + + it('Should allow to disable state persistency', async () => { + settings.set(['advanced', 'cache_state_persistent'], false); + data.removeState(); + await controller.start(); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true}); + await controller.stop(); + expect(data.stateExists()).toBeFalsy(); + }); + + it('Shouldnt crash when it cannot save state', async () => { + data.removeState(); + await controller.start(); + mockLogger.error.mockClear(); + // @ts-expect-error private + controller.state.file = '/'; + // @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(); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true}); + }); + + 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({}); + }); + + it('Start controller with force_disable_retain', async () => { + settings.set(['mqtt', 'force_disable_retain'], true); + await controller.start(); + await flushPromises(); + expect(mockMQTTConnectAsync).toHaveBeenCalledTimes(1); + const expected = { + will: {payload: Buffer.from('{"state":"offline"}'), retain: false, topic: 'zigbee2mqtt/bridge/state', qos: 1}, + properties: {maximumPacketSize: 1048576}, + }; + expect(mockMQTTConnectAsync).toHaveBeenCalledWith('mqtt://localhost', expected); + }); + + it('Should republish retained messages on MQTT initial connect', async () => { + await controller.start(); + await flushPromises(); + + const retainedMessages = Object.keys( + // @ts-expect-error private + controller.mqtt.retainedMessages, + ).length; + + mockMQTT.publishAsync.mockClear(); + await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers + + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(retainedMessages); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + }); + + it('Should not republish retained messages on MQTT initial connect when retained message are sent', async () => { + await controller.start(); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bridge/info', 'dummy'); + await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers + + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0); + }); + + 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(); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + await controller.mqtt.publish('fo', 'bar', {retain: true}); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0}); + }); + + it('Should publish last seen changes', async () => { + settings.set(['advanced', 'last_seen'], 'epoch'); + await controller.start(); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'deviceAnnounce'}); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255, last_seen: 1000}), { + qos: 0, + retain: true, + }); + }); + + it('Should not publish last seen changes when reason is messageEmitted', async () => { + settings.set(['advanced', 'last_seen'], 'epoch'); + await controller.start(); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + const device = devices.remote; + await mockZHEvents.lastSeenChanged({device, reason: 'messageEmitted'}); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0); + }); + + it('Ignore messages from coordinator', async () => { + // https://github.com/Koenkk/zigbee2mqtt/issues/9218 + await controller.start(); + const device = devices.coordinator; + const payload = { + device, + endpoint: device.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + cluster: 'genBasic', + data: {modelId: device.modelID}, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(mockLogger.log).toHaveBeenCalledWith( + 'debug', + `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(); + // @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({constructor: {name: 'Test'}}, callback); + eventbus.emitStateChange({}); + await flushPromises(); + expect(callback).toHaveBeenCalledTimes(1); + 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..88148f0b4c 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 'node: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 59% rename from test/availability.test.js rename to test/extensions/availability.test.ts index 118f1badcc..b89edae175 100644 --- a/test/availability.test.js +++ b/test/extensions/availability.test.ts @@ -1,48 +1,51 @@ -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 'node: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.publishAsync, 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, + devices.hue_twilight.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); + settings.set(['availability'], {enabled: true}); controller = new Controller(jest.fn(), jest.fn()); await controller.start(); await flushPromises(); @@ -52,10 +55,10 @@ describe('Availability', () => { jest.setSystemTime(utils.minutes(1)); data.writeDefaultConfiguration(); settings.reRead(); - settings.set(['availability'], true); + settings.set(['availability'], {enabled: 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,14 +71,15 @@ 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).not.toHaveBeenCalledWith( - 'zigbee2mqtt/bulb_color_2/availability', - 'online', - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'online'}), {retain: true, qos: 1}); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); }); it('Should ping on startup for enabled and unavailable devices', async () => { @@ -102,38 +106,44 @@ describe('Availability', () => { }); it('Should publish offline for active device when not seen for 10 minutes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), { + retain: true, + qos: 1, + }); }); 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.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}); }); it('Should reset ping timer when device last seen changes for active device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), { + retain: true, + qos: 1, + }); await setTimeAndAdvanceTimers(utils.minutes(7)); expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0); @@ -144,9 +154,9 @@ describe('Availability', () => { }); it('Should ping again when first ping fails', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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'); @@ -159,13 +169,13 @@ describe('Availability', () => { }); it('Should reset ping timer when device last seen changes for passive device', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.mockClear(); await setTimeAndAdvanceTimers(utils.hours(24)); 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)); + await mockZHEvents.lastSeenChanged({device: devices.remote}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}); await setTimeAndAdvanceTimers(utils.hours(25)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); @@ -175,15 +185,21 @@ describe('Availability', () => { }); it('Should immediately mark device as online when it lastSeen changes', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(15)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), { + retain: true, + qos: 1, + }); 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('zigbee2mqtt/bulb_color/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); }); it('Should allow to change availability timeout via device options', async () => { @@ -208,7 +224,7 @@ describe('Availability', () => { }); it('Should allow to change availability timeout via avaiability options', async () => { - settings.set(['availability'], {active: {timeout: 30}}); + settings.set(['availability', 'active', 'timeout'], 30); await resetExtension(); devices.bulb_color.ping.mockClear(); @@ -223,7 +239,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)); @@ -234,7 +250,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)); @@ -242,7 +258,7 @@ describe('Availability', () => { }); it('Should allow to be disabled', async () => { - settings.set(['availability'], false); + settings.set(['availability'], {enabled: false}); await resetExtension(); devices.bulb_color.ping.mockClear(); @@ -251,7 +267,7 @@ describe('Availability', () => { }); it('Should allow to enable availability for just one device', async () => { - settings.set(['availability'], false); + settings.set(['availability'], {enabled: false}); settings.set(['devices', devices.bulb_color.ieeeAddr, 'availability'], true); await resetExtension(); @@ -262,18 +278,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); @@ -283,7 +300,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(''); @@ -293,61 +310,30 @@ describe('Availability', () => { }); it('Should republish availability when device is renamed', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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('zigbee2mqtt/bulb_new_name/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); await setTimeAndAdvanceTimers(utils.hours(12)); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', 'offline', {retain: true, qos: 1}, expect.any(Function)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'offline'}), { + retain: true, + qos: 1, + }); }); 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(); + mockMQTT.publishAsync.mockClear(); await setTimeAndAdvanceTimers(utils.hours(26)); expect(devices.remote.ping).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/remote/availability', - stringify({state: 'offline'}), - {retain: true, qos: 1}, - expect.any(Function), - ); - }); - - 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); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1}); }); it('Should publish availability for groups', async () => { @@ -355,44 +341,42 @@ describe('Availability', () => { await resetExtension(); devices.bulb_color_2.ping.mockClear(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/group_tradfri_remote/availability', - 'online', - {retain: true, qos: 1}, - expect.any(Function), - ); - MQTT.publish.mockClear(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); + mockMQTT.publishAsync.mockClear(); await setTimeAndAdvanceTimers(utils.minutes(12)); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/group_tradfri_remote/availability', - 'offline', - {retain: true, qos: 1}, - expect.any(Function), - ); - MQTT.publish.mockClear(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'offline'}), { + retain: true, + qos: 1, + }); + mockMQTT.publishAsync.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( - 'zigbee2mqtt/group_tradfri_remote/availability', - 'online', - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), { + retain: true, + qos: 1, + }); }); 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(); @@ -400,8 +384,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/extensions/bind.test.ts b/test/extensions/bind.test.ts new file mode 100644 index 0000000000..13980129d8 --- /dev/null +++ b/test/extensions/bind.test.ts @@ -0,0 +1,774 @@ +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.publishAsync, + 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(); + endpoint.configureReporting.mockClear(); + endpoint.bind = jest.fn(); + endpoint.bind.mockClear(); + endpoint.unbind.mockClear(); + } + }; + + beforeAll(async () => { + jest.useFakeTimers(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + await flushPromises(); + }); + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + groups.group_1.members = []; + await resetExtension(); + mocksClear.forEach((m) => m.mockClear()); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + it('Should bind to device and configure reporting', async () => { + 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 originalTargetBinds = target.binds; + target.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); + mockClear(device); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); + + 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); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); + expect(target.configureReporting).toHaveBeenCalledTimes(3); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + transaction: '1234', + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + + // Teardown + target.binds = originalTargetBinds; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; + }); + + it('Should throw error on invalid payload', async () => { + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({fromz: 'remote', to: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + ); + }); + + it('Filters out unsupported clusters for reporting setup', async () => { + 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 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: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); + mockClear(device); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); + + 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); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); + expect(target.configureReporting).toHaveBeenCalledTimes(2); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + transaction: '1234', + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + + // Teardown + target.binds = originalTargetBinds; + target.inputClusters = originalTargetInputClusters; + target.outputClusters = originalTargetOutputClusters; + device.getEndpoint(1)!.inputClusters = originalDeviceInputClusters; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; + }); + + it('Filters out reporting setup based on bind status', async () => { + 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 originalTargetBinds = target.binds; + target.binds = [{cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}]; + target.getClusterAttributeValue.mockReturnValueOnce(undefined); + mockClear(device); + target.configureReporting.mockImplementationOnce(() => { + throw new Error('timeout'); + }); + const originalTargetCR = target.configuredReportings; + target.configuredReportings = [ + { + cluster: {name: 'genLevelCtrl'}, + attribute: {name: 'currentLevel', ID: 0}, + minimumReportInterval: 0, + maximumReportInterval: 3600, + reportableChange: 0, + }, + ]; + + 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); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', target); + expect(target.configureReporting).toHaveBeenCalledTimes(2); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + // expect(target.configureReporting).toHaveBeenCalledWith("genLevelCtrl",[{"attribute": "currentLevel", "maximumReportInterval": 3600, "minimumReportInterval": 5, "reportableChange": 1}]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1}, + ]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + transaction: '1234', + data: { + from: 'remote', + from_endpoint: 'default', + to: 'bulb_color', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'], + failed: [], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + + // Teardown + target.configuredReportings = originalTargetCR; + target.binds = originalTargetBinds; + device.getEndpoint(1)!.outputClusters = originalDeviceOutputClusters; + }); + + it('Should bind only specified clusters', async () => { + const device = devices.remote; + const target = devices.bulb_color.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Should log error when there is nothing to bind', async () => { + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + mockLogger.error.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'})); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: 'Nothing to bind'}), + {retain: false, qos: 0}, + ); + }); + + it('Should unbind', async () => { + 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 originalTargetBinds = target.binds; + target.binds = [ + {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)!; + mockClear(device); + 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); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target); + + // Disable reporting + expect(target.configureReporting).toHaveBeenCalledTimes(3); + expect(target.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 0xffff, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 0xffff, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [ + {attribute: 'colorTemperature', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + {attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + {attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, + ]); + expect(devices.bulb_color.meta.configured).toBe(332242049); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/unbind', + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + + // Teardown + target.binds = originalTargetBinds; + device.getEndpoint(1)!.binds = originalRemoteBinds; + }); + + it('Should unbind coordinator', async () => { + const device = devices.remote; + const target = devices.coordinator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + endpoint.unbind.mockClear(); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/unbind', + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Should bind to groups', async () => { + 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); + 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); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target); + expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target); + expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + + // Should configure reporting for device added to group + target1Member.configureReporting.mockClear(); + 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', [ + {attribute: 'onOff', maximumReportInterval: 3600, minimumReportInterval: 0, reportableChange: 0}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1}, + ]); + }); + + it('Should unbind from group', async () => { + 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); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/unbind', + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Should unbind from group with skip_disable_reporting=true', async () => { + 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. + const originalBinds = endpoint.binds; + endpoint.binds = []; + + target1Member.binds = [ + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, + ]; + target1Member.configureReporting.mockClear(); + mockClear(device); + 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 + expect(target1Member.configureReporting).toHaveBeenCalledTimes(0); + endpoint.binds = originalBinds; + }); + + it('Should unbind from group with skip_disable_reporting=false', async () => { + 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. + const originalBinds = endpoint.binds; + endpoint.binds = []; + + target1Member.binds = [ + {cluster: {name: 'genLevelCtrl'}, target: devices.coordinator.getEndpoint(1)!}, + {cluster: {name: 'genOnOff'}, target: devices.coordinator.getEndpoint(1)!}, + ]; + target1Member.configureReporting.mockClear(); + mockClear(device); + 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 + expect(target1Member.configureReporting).toHaveBeenCalledTimes(2); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [ + {attribute: 'currentLevel', maximumReportInterval: 65535, minimumReportInterval: 5, reportableChange: 1}, + ]); + expect(target1Member.configureReporting).toHaveBeenCalledWith('genOnOff', [ + {attribute: 'onOff', maximumReportInterval: 65535, minimumReportInterval: 0, reportableChange: 0}, + ]); + endpoint.binds = originalBinds; + }); + + it('Should bind to group by number', async () => { + const device = devices.remote; + const target = groups.group_1; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + data: {from: 'remote', from_endpoint: 'default', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Should log when bind fails', async () => { + mockLogger.error.mockClear(); + const device = devices.remote; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + endpoint.bind.mockImplementation(() => { + throw new Error('failed'); + }); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'})); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: 'Failed to bind'}), + {retain: false, qos: 0}, + ); + }); + + 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.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch_double', to_endpoint: 'right', clusters: ['genOnOff'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + 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', 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.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + data: {from: 'remote', from_endpoint: 2, to: 'wall_switch_double', to_endpoint: 3, clusters: ['genOnOff'], failed: []}, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Should bind server clusters to client clusters', async () => { + const device = devices.temperature_sensor; + const target = devices.heating_actuator.getEndpoint(1)!; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({ + data: { + from: 'temperature_sensor', + from_endpoint: 'default', + to: 'heating_actuator', + clusters: ['msTemperatureMeasurement'], + failed: [], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Should bind to default endpoint returned by endpoints()', async () => { + const device = devices.remote; + const target = devices.QBKG04LM.getEndpoint(2)!; + const endpoint = device.getEndpoint(2)!; + mockClear(device); + 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.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Should unbind from default_bind_group', async () => { + const device = devices.remote; + const target = 'default_bind_group'; + const endpoint = device.getEndpoint(1)!; + mockClear(device); + 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(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/unbind', + stringify({ + data: { + from: 'remote', + from_endpoint: 'default', + to: 'default_bind_group', + clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], + failed: [], + }, + status: 'ok', + }), + {retain: false, qos: 0}, + ); + }); + + it('Error bind fails when source device does not exist', async () => { + const device = devices.remote; + mockClear(device); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: "Source device 'remote_not_existing' does not exist"}), + {retain: false, qos: 0}, + ); + }); + + 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', from_endpoint: 'not_existing_endpoint', to: 'bulb_color'}), + ); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'"}), + {retain: false, qos: 0}, + ); + }); + + it('Error bind fails when target device or group 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'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: "Target device or group 'bulb_color_not_existing' does not exist"}), + {retain: false, qos: 0}, + ); + }); + + 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', to_endpoint: 'not_existing_endpoint'}), + ); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/bind', + stringify({data: {}, status: 'error', error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}), + {retain: false, qos: 0}, + ); + }); + + it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { + 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)!, + type: 'commandHueNotification', + linkquality: 10, + groupID: 0, + }; + await mockZHEvents.message(payload); + await flushPromises(); + 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 = devices.bj_scene_switch; + const data = {action: 'recall_2_row_1'}; + devices.bulb_color_2.getEndpoint(1)!.read.mockImplementationOnce(() => { + throw new Error('failed'); + }); + const payload = { + data, + cluster: 'genScenes', + device: remote, + endpoint: remote.getEndpoint(10)!, + type: 'commandRecall', + linkquality: 10, + groupID: 0, + }; + await mockZHEvents.message(payload); + await flushPromises(); + // Calls to three clusters are expected in this case + 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 () => { + 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)!, + type: 'commandStepWithOnOff', + linkquality: 10, + groupID: 15071, + }; + await mockZHEvents.message(payload); + await flushPromises(); + 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 mockZHEvents.message(payload); + await flushPromises(); + expect(mockDebounce).toHaveBeenCalledTimes(2); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(4); + + // Should only call Hue bulb, not e.g. tradfri + expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); + }); +}); diff --git a/test/bridge.test.js b/test/extensions/bridge.test.ts similarity index 75% rename from test/bridge.test.js rename to test/extensions/bridge.test.ts index b26d79b56c..e6332a3f0a 100644 --- a/test/bridge.test.js +++ b/test/extensions/bridge.test.ts @@ -1,78 +1,78 @@ -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 'node:assert'; +import fs from 'node:fs'; +import path from 'node: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.publishAsync, + 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'); }; beforeAll(async () => { jest.useFakeTimers(); mockRestart = jest.fn(); - settings.set(['advanced', 'legacy_api'], false); 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(); - settings.set(['advanced', 'legacy_api'], false); data.writeDefaultState(); - logger.info.mockClear(); - logger.warning.mockClear(); - logger.setTransportsEnabled(false); - MQTT.publish.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 () => { @@ -85,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.publishAsync.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')![1]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', stringify({ commit: version.commitHash, @@ -94,10 +94,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 +101,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,43 +113,83 @@ describe('Bridge', () => { log_syslog: {}, output: 'json', pan_id: 6754, - report: false, - soft_reset_timeout: 0, timestamp_format: 'YYYY-MM-DD HH:mm:ss', }, blocklist: [], device_options: {}, devices: { - '0x000b57fffec6a5b2': {description: 'this is my bulb', friendly_name: 'bulb', retain: true}, + '0x000b57cdfec6a5b3': {friendly_name: 'hue_twilight'}, + '0x000b57fffec6a5b2': { + description: 'this is my bulb', + friendly_name: 'bulb', + retain: true, + }, '0x000b57fffec6a5b3': {friendly_name: 'bulb_color', retain: false}, - '0x000b57fffec6a5b4': {friendly_name: 'bulb_color_2', retain: false}, + '0x000b57fffec6a5b4': { + friendly_name: 'bulb_color_2', + retain: false, + }, '0x000b57fffec6a5b7': {friendly_name: 'bulb_2', retain: false}, '0x0017880104a44559': {friendly_name: 'J1_cover'}, '0x0017880104e43559': {friendly_name: 'U202DST600ZB'}, '0x0017880104e44559': {friendly_name: '3157100_thermostat'}, '0x0017880104e45517': {friendly_name: 'remote', retain: true}, '0x0017880104e45520': {friendly_name: 'button', retain: false}, - '0x0017880104e45521': {friendly_name: 'button_double_key', retain: false}, - '0x0017880104e45522': {friendly_name: 'weather_sensor', qos: 1, retain: false}, - '0x0017880104e45523': {friendly_name: 'occupancy_sensor', retain: false}, + '0x0017880104e45521': { + friendly_name: 'button_double_key', + retain: false, + }, + '0x0017880104e45522': { + friendly_name: 'weather_sensor', + qos: 1, + retain: false, + }, + '0x0017880104e45523': { + friendly_name: 'occupancy_sensor', + retain: false, + }, '0x0017880104e45524': {friendly_name: 'power_plug', retain: false}, '0x0017880104e45526': {friendly_name: 'GL-S-007ZS'}, - '0x0017880104e45529': {friendly_name: 'unsupported2', retain: false}, - '0x0017880104e45530': {friendly_name: 'button_double_key_interviewing', retain: false}, + '0x0017880104e45529': { + friendly_name: 'unsupported2', + retain: false, + }, + '0x0017880104e45530': { + friendly_name: 'button_double_key_interviewing', + retain: false, + }, '0x0017880104e45540': {friendly_name: 'ikea_onoff'}, '0x0017880104e45541': {friendly_name: 'wall_switch', retain: false}, - '0x0017880104e45542': {friendly_name: 'wall_switch_double', retain: false}, - '0x0017880104e45543': {friendly_name: 'led_controller_1', retain: false}, - '0x0017880104e45544': {friendly_name: 'led_controller_2', retain: false}, - '0x0017880104e45545': {friendly_name: 'dimmer_wall_switch', retain: false}, + '0x0017880104e45542': { + friendly_name: 'wall_switch_double', + retain: false, + }, + '0x0017880104e45543': { + friendly_name: 'led_controller_1', + retain: false, + }, + '0x0017880104e45544': { + friendly_name: 'led_controller_2', + retain: false, + }, + '0x0017880104e45545': { + friendly_name: 'dimmer_wall_switch', + retain: false, + }, '0x0017880104e45547': {friendly_name: 'curtain', retain: false}, '0x0017880104e45548': {friendly_name: 'fan', retain: false}, '0x0017880104e45549': {friendly_name: 'siren', retain: false}, '0x0017880104e45550': {friendly_name: 'thermostat', retain: false}, '0x0017880104e45551': {friendly_name: 'smart vent', retain: false}, '0x0017880104e45552': {friendly_name: 'j1', retain: false}, - '0x0017880104e45553': {friendly_name: 'bulb_enddevice', retain: false}, - '0x0017880104e45559': {friendly_name: 'cc2530_router', retain: false}, + '0x0017880104e45553': { + friendly_name: 'bulb_enddevice', + retain: false, + }, + '0x0017880104e45559': { + friendly_name: 'cc2530_router', + retain: false, + }, '0x0017880104e45560': {friendly_name: 'livolo', retain: false}, '0x0017880104e45561': {friendly_name: 'temperature_sensor'}, '0x0017880104e45562': {friendly_name: 'heating_actuator'}, @@ -168,7 +202,10 @@ describe('Bridge', () => { '0x90fd9ffffe4b64aa': {friendly_name: 'SP600_OLD'}, '0x90fd9ffffe4b64ab': {friendly_name: 'SP600_NEW'}, '0x90fd9ffffe4b64ac': {friendly_name: 'MKS-CM-W5'}, - '0x90fd9ffffe4b64ae': {friendly_name: 'tradfri_remote', retain: false}, + '0x90fd9ffffe4b64ae': { + friendly_name: 'tradfri_remote', + retain: false, + }, '0x90fd9ffffe4b64af': {friendly_name: 'roller_shutter'}, '0x90fd9ffffe4b64ax': {friendly_name: 'ZNLDP12LM'}, '0xf4ce368a38be56a1': { @@ -189,71 +226,106 @@ describe('Bridge', () => { retain: false, }, }, - 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}, + 19: {friendly_name: 'hue_twilight_group'}, 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: { + enabled: false, + discovery_topic: 'homeassistant', + status_topic: 'homeassistant/status', + legacy_action_sensor: false, + experimental_event_entities: false, + }, + availability: { + enabled: false, + active: {timeout: 10}, + passive: {timeout: 1500}, + }, + frontend: { + enabled: false, + port: 8080, + base_url: '/', }, - homeassistant: false, map_options: { graphviz: { colors: { - fill: {coordinator: '#e04e5d', enddevice: '#fff8ce', router: '#4ea3e0'}, - font: {coordinator: '#ffffff', enddevice: '#000000', router: '#ffffff'}, + fill: { + coordinator: '#e04e5d', + enddevice: '#fff8ce', + router: '#4ea3e0', + }, + font: { + coordinator: '#ffffff', + enddevice: '#000000', + router: '#ffffff', + }, line: {active: '#009900', inactive: '#994444'}, }, }, }, - 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}, + mqtt: { + base_topic: 'zigbee2mqtt', + force_disable_retain: false, + include_device_information: false, + maximum_packet_size: 1048576, + server: 'mqtt://localhost', + }, + ota: { + default_maximum_data_size: 50, + disable_automatic_update_check: false, + image_block_response_delay: 250, + update_check_interval: 1440, + }, passlist: [], - permit_join: true, serial: {disable_led: false, port: '/dev/dummy'}, }, - config_schema: settings.schema, + config_schema: settings.schemaJson, 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: '0x64c5fd698daf0c00', pan_id: 5674}, permit_join: false, + permit_join_end: undefined, restart_required: false, version: version.version, zigbee_herdsman: zhVersion, zigbee_herdsman_converters: zhcVersion, }), {retain: true, qos: 0}, - expect.any(Function), ); }); 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.publishAsync).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: [ @@ -494,7 +566,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', }, @@ -694,7 +766,7 @@ describe('Bridge', () => { type: 'Router', }, { - date_code: null, + date_code: undefined, definition: { description: 'Hue dimmer switch', exposes: [ @@ -765,18 +837,7 @@ describe('Bridge', () => { { access: 2, description: - 'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).', - label: 'Legacy', - name: 'legacy', - property: 'legacy', - type: 'binary', - value_off: false, - value_on: true, - }, - { - access: 2, - description: - 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. Only works when legacy is false.', + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.', features: [ { access: 2, @@ -830,12 +891,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: [ @@ -877,7 +938,7 @@ describe('Bridge', () => { { access: 2, description: - 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ', + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.', features: [ { access: 2, @@ -925,12 +986,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: [ @@ -1008,17 +1069,6 @@ describe('Bridge', () => { property: 'device_temperature_calibration', type: 'numeric', }, - { - access: 2, - description: - 'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).', - label: 'Legacy', - name: 'legacy', - property: 'legacy', - type: 'binary', - value_off: false, - value_on: true, - }, ], supports_ota: false, vendor: 'Aqara', @@ -1047,12 +1097,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: [ @@ -1190,12 +1240,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: [ @@ -1339,12 +1389,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: [ @@ -1841,12 +1891,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: [ @@ -2075,185 +2125,199 @@ 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', }, ]), {retain: true, qos: 0}, - expect.any(Function), ); }); it('Should publish definitions on startup', async () => { await resetExtension(); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - {retain: true, qos: 0}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(CUSTOM_CLUSTERS)), { + retain: true, + qos: 0, + }); }); 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.publishAsync.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publishAsync).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.publishAsync).toHaveBeenCalledTimes(1); // Should not publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(0); + mockMQTT.publishAsync.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publishAsync).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.publishAsync.mockClear(); + mockLogger.info.mockClear(); + mockLogger.info('this is a test'); + mockLogger.info('this is a test'); // Should not publish dupes + expect(mockMQTT.publishAsync).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.publishAsync).toHaveBeenCalledTimes(1); // Should publish debug logging - MQTT.publish.mockClear(); - logger.debug('this is a test'); - expect(MQTT.publish).toHaveBeenCalledTimes(1); + mockMQTT.publishAsync.mockClear(); + mockLogger.debug('this is a test'); + expect(mockMQTT.publishAsync).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.publishAsync.mockClear(); + mockLogger.info.mockClear(); + mockLogger.error.mockClear(); + mockLogger.info('this is a test'); + expect(mockMQTT.publishAsync).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); + // console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/groups')); + expect(mockMQTT.publishAsync).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: []}, + { + friendly_name: 'hue_twilight_group', + id: 19, + members: [{endpoint: 11, ieee_address: '0x000b57cdfec6a5b3'}], + scenes: [], + }, ]), {retain: true, qos: 0}, - expect.any(Function), ); }); it('Should publish event when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publishAsync.mockClear(); + await mockZHEvents.deviceJoined({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should publish devices when device joined', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceNetworkAddressChanged({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); }); it('Should publish event when device announces', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceAnnounce({device: zigbeeHerdsman.devices.bulb}); + mockMQTT.publishAsync.mockClear(); + await mockZHEvents.deviceAnnounce({device: devices.bulb}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should publish event when device interview started', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'started'}); + mockMQTT.publishAsync.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'started'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: '0x000b57fffec6a5b2'}}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + await mockZHEvents.deviceInterview({device: devices.bulb, status: 'failed'}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(2); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledTimes(7); + // console.log(mockMQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event')); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2476,9 +2540,8 @@ describe('Bridge', () => { type: 'device_interview', }), {retain: false, qos: 0}, - expect.any(Function), ); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({ data: { @@ -2523,7 +2586,7 @@ describe('Bridge', () => { { access: 2, description: - 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ', + 'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.', features: [ { access: 2, @@ -2562,190 +2625,156 @@ describe('Bridge', () => { type: 'device_interview', }), {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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.any(String), {retain: true, qos: 0}); }); it('Should publish event and devices when device leaves', async () => { - MQTT.publish.mockClear(); - await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr}); + mockMQTT.publishAsync.mockClear(); + await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb.ieeeAddr}); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledTimes(3); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( // Defintitions should be updated on device event 'zigbee2mqtt/bridge/definitions', expect.any(String), {retain: true, qos: 0}, - expect.any(Function), ); }); - it('Should allow permit join', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', 'true'); - await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true, undefined, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: true}, 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 on all', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 1})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), { + retain: false, + qos: 0, + }); + }); - 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 () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, undefined); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(0, undefined); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 0}, status: 'ok'}), { + retain: false, + qos: 0, + }); + }); - // Invalid payload - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value_bla: false})); + it('Should allow permit join with number string (automatically on all)', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', '1'); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {}, status: 'error', error: 'Invalid payload'}), - {retain: false, qos: 0}, - expect.any(Function), - ); + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/permit_join', stringify({data: {time: 1}, status: 'ok'}), { + retain: false, + qos: 0, + }); }); - 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})); + it('Should not allow permit join with invalid payload', async () => { + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time_bla: false})); await flushPromises(); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); - expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false, undefined, 10); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(0); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', - stringify({data: {value: false, time: 10}, status: 'ok'}), + stringify({data: {}, status: 'error', error: 'Invalid payload'}), {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}); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); }); 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.publishAsync.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.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); }); 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'})); + const device = devices.bulb; + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.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(MQTT.publish).toHaveBeenCalledWith( + expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1); + expect(mockZHController.permitJoin).toHaveBeenCalledWith(123, device); + expect(mockMQTT.publishAsync).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(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/permit_join', stringify({value: true, device: 'bulb_not_existing_woeeee'})); + it('Should not allow permit join via non-existing device', async () => { + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing_woeeee' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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})); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0, transaction: 22})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).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), ); }); 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({value: false})); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/permit_join', stringify({time: 0})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/permit_join', stringify({data: {}, status: 'error', error: 'Failed to connect to adapter'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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})); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.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'}), + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/options', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/random', stringify({value: false})); + const device = devices.bulb; + await mockZHEvents.message({ data: {onOff: 1}, cluster: 'genOnOff', device, @@ -2757,205 +2786,168 @@ describe('Bridge', () => { }); it('Should allow a healthcheck', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/health_check', ''); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/health_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/health_check', stringify({data: {healthy: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockZHController.coordinatorCheck.mockReturnValueOnce({missingRouters: [mockZHController.getDeviceByIeeeAddr('0x000b57fffec6a5b2')]}); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/coordinator_check', ''); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/coordinator_check', stringify({data: {missing_routers: [{friendly_name: 'bulb', ieee_address: '0x000b57fffec6a5b2'}]}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow to remove device by string', async () => { - const device = zigbeeHerdsman.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', - ], - }, - }); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', 'bulb'); + const device = devices.bulb; + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, - 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(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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: false, force: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {id: 'bulb', block: true, force: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); expect(settings.get().blocklist).toStrictEqual(['0x000b57fffec6a5b2']); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/remove', stringify({data: {id: 'group_1', force: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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 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(); - MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/remove', stringify({id: 'non-existing-device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/remove', stringify({data: {}, status: 'error', error: "Device 'non-existing-device' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should throw error when remove device fails', async () => { - const device = zigbeeHerdsman.devices.bulb; - MQTT.publish.mockClear(); + const device = devices.bulb; + mockMQTT.publishAsync.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.publishAsync).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}, - expect.any(Function), ); }); 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.publishAsync.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({ @@ -2964,72 +2956,67 @@ 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', '', {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name', stringify({brightness: 50}), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'living_room/blinds#'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).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}, - expect.any(Function), ); }); 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.publishAsync.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(settings.getGroup('group_new_name')).toStrictEqual({ID: 1, friendly_name: 'group_new_name', retain: false}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/rename', stringify({data: {from: 'group_1', to: 'group_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from_bla: 'bulb', to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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({ @@ -3038,168 +3025,158 @@ 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {from: 'bulb', to: 'bulb_new_name', homeassistant_rename: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb', to: 'bulb_new_name/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).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}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({last: true, to: 'bulb_new_name'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/rename', stringify({data: {}, status: 'error', error: 'No device has joined since start'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: 'bulb'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); // 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {id: '0x000b57fffec6a5b2'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); // 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb_not_existing' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'bulb/1' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "Device 'group_1' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/interview', stringify({data: {}, status: 'error', error: "interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: 'non_existing_device'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({data: {}, error: "Device 'non_existing_device' does not exist", status: 'error'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({id: devices.ZNCZ02LM.ieeeAddr})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/generate_external_definition', stringify({ data: { @@ -3221,19 +3198,18 @@ describe('Bridge', () => { status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow change device options', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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', @@ -3242,7 +3218,7 @@ describe('Bridge', () => { transition: 1, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3254,12 +3230,11 @@ describe('Bridge', () => { status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow to remove device option', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.mockClear(); settings.set(['devices', '0x000b57fffec6a5b2', 'qos'], 1); expect(settings.getDevice('bulb')).toStrictEqual({ ID: '0x000b57fffec6a5b2', @@ -3268,7 +3243,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', @@ -3276,7 +3251,7 @@ describe('Bridge', () => { retain: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3288,19 +3263,18 @@ describe('Bridge', () => { status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow change device options with restart required', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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', @@ -3309,7 +3283,7 @@ describe('Bridge', () => { disabled: true, description: 'this is my bulb', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({ data: { @@ -3321,383 +3295,224 @@ describe('Bridge', () => { status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow change group options', async () => { - MQTT.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'})); + mockMQTT.publishAsync.mockClear(); + 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(MQTT.publish).toHaveBeenCalledWith( + expect(settings.getGroup('group_1')).toStrictEqual({ID: 1, friendly_name: 'group_1', retain: true, transition: 1}); + expect(mockMQTT.publishAsync).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}, - expect.any(Function), ); }); it('Should allow change group options with restart required', async () => { - MQTT.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'})); + mockMQTT.publishAsync.mockClear(); + 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', }); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).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'}, status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/options', stringify({options_: {retain: true, transition: 1}, id: 'bulb'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/options', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow to add group by string', async () => { - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/group/add', 'group_193'); + mockMQTT.publishAsync.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(settings.getGroup('group_193')).toStrictEqual({ID: 3, friendly_name: 'group_193'}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 3}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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(settings.getGroup('group_193')).toStrictEqual({ID: 92, friendly_name: 'group_193'}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {friendly_name: 'group_193', id: 92}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name: '', id: 9})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {}, status: 'error', error: 'friendly_name must be at least 1 char long'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/add', stringify({friendly_name9: 'group_193'})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/group/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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(); - zigbeeHerdsman.touchlinkFactoryResetFirst.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/request/touchlink/factory_reset', ''); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Add install code', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); // 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {value: 'my-code'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/install_code/add', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {ieee_address: '0x1239', channel: 12}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/identify', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/factory_reset', stringify({data: {}, status: 'error', error: 'Failed to factory reset device through Touchlink'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow to touchlink scan', async () => { - MQTT.publish.mockClear(); - zigbeeHerdsman.touchlinkScan.mockClear(); - zigbeeHerdsman.touchlinkScan.mockReturnValueOnce([ + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/touchlink/scan', stringify({ data: { @@ -3709,20 +3524,20 @@ describe('Bridge', () => { status: 'ok', }), {retain: false, qos: 0}, - expect.any(Function), ); }); - it('Should allow to configure reporting', async () => { - const device = zigbeeHerdsman.devices.bulb; - const endpoint = device.getEndpoint(1); + 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(); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publishAsync.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, @@ -3732,18 +3547,19 @@ 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({ data: { - id: '0x000b57fffec6a5b2/1', + id: '0x000b57fffec6a5b2', + endpoint: 1, cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, @@ -3753,23 +3569,68 @@ describe('Bridge', () => { 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)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + }); + + 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.publishAsync.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.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure_reporting', + stringify({ + data: { + 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(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); }); 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(); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message( '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, @@ -3777,24 +3638,23 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: 'Invalid payload'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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(); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message( 'zigbee2mqtt/bridge/request/device/configure_reporting', stringify({ id: 'non_existing_device', + endpoint: '1', cluster: 'genLevelCtrl', attribute: 'currentLevel', maximum_report_interval: 10, @@ -3804,24 +3664,23 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure_reporting', stringify({data: {}, status: 'error', error: "Device 'non_existing_device' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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(); - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message( + mockMQTT.publishAsync.mockClear(); + 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, @@ -3831,11 +3690,10 @@ describe('Bridge', () => { ); await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).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}, - expect.any(Function), ); }); @@ -3846,10 +3704,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.publishAsync.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)); @@ -3857,286 +3715,261 @@ 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/backup', stringify({data: {zip: 'THISISBASE64'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Should allow to restart', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/restart', ''); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/restart', ''); await flushPromises(); jest.runOnlyPendingTimers(); expect(mockRestart).toHaveBeenCalledTimes(1); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/restart', - stringify({data: {}, status: 'ok'}), - {retain: false, qos: 0}, - expect.any(Function), - ); - }); - - 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), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/restart', stringify({data: {}, status: 'ok'}), { + retain: false, + qos: 0, + }); }); 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.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {homeassistant: {enabled: 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Change options and apply - log_debug_namespace_ignore', async () => { - MQTT.publish.mockClear(); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: false}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).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 () => { - zigbeeHerdsman.permitJoin.mockClear(); settings.apply({serial: {port: '123'}}); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', stringify({options: {serial: {port: '/dev/newport'}}})); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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( + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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}}})); + mockMQTT.publishAsync.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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {restart_required: true}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); it('Change options invalid payload', async () => { - zigbeeHerdsman.permitJoin.mockClear(); - MQTT.publish.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', 'I am invalid'); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', stringify({data: {}, error: 'Invalid payload', status: 'error'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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'}})); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/options', stringify({options: {advanced: {log_level: 123}}})); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/options', - stringify({data: {}, error: 'permit_join must be boolean', status: 'error'}), + stringify({data: {}, error: 'advanced/log_level must be string', status: 'error'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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( - 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(5); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(CUSTOM_CLUSTERS)), { + retain: true, + qos: 0, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/event', stringify({data: {friendly_name: '0x000b57fffec6a5c2', ieee_address: '0x000b57fffec6a5c2'}, type: 'device_joined'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync.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( - 'zigbee2mqtt/bridge/definitions', - expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)), - {retain: true, qos: 0}, - expect.any(Function), - ); - expect(MQTT.publish).toHaveBeenCalledWith( - 'zigbee2mqtt/bridge/response/device/configure', - expect.any(String), - {retain: false, qos: 0}, - expect.any(Function), - ); + // console.log(mockMQTT.publish.mock.calls); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(4); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/definitions', expect.stringContaining(stringify(CUSTOM_CLUSTERS)), { + retain: true, + qos: 0, + }); + expect(mockMQTT.publishAsync).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 53% rename from test/configure.test.js rename to test/extensions/configure.test.ts index da7a0e2539..44c4c2e722 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'); +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'; -const mocksClear = [MQTT.publish, logger.warning, logger.debug]; +import stringify from 'json-stable-stringify-without-jsonify'; -describe('Configure', () => { - let controller; - let coordinatorEndpoint; +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; - const expectRemoteConfigured = () => { - const device = zigbeeHerdsman.devices.remote; - const endpoint1 = device.getEndpoint(1); +const mocksClear = [mockMQTT.publishAsync, 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,148 +128,134 @@ 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', stringify({data: {id: 'remote'}, status: 'ok'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({data: {id: 'not_existing_device'}, status: 'error', error: "Device 'not_existing_device' does not exist"}), + stringify({data: {}, status: 'error', error: "Device 'not_existing_device' does not exist"}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({data: {id: 'remote'}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}), + stringify({data: {}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}), {retain: false, qos: 0}, - expect.any(Function), ); }); 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.publishAsync).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/response/device/configure', - stringify({data: {id: '0x0017882104a44559'}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}), + stringify({data: {}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}), {retain: false, qos: 0}, - expect.any(Function), ); }); - 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'); + it('Handles invalid payload for configure via MQTT', async () => { + await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({idx: '0x0017882104a44559'})); 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.`); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + ); }); 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/extensions/externalConverters.test.ts b/test/extensions/externalConverters.test.ts new file mode 100644 index 0000000000..6322bc8336 --- /dev/null +++ b/test/extensions/externalConverters.test.ts @@ -0,0 +1,345 @@ +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 type Device from '../../lib/model/device'; + +import fs from 'node:fs'; +import path from 'node:path'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import * as zhc from 'zigbee-herdsman-converters'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const BASE_DIR = 'external_converters'; + +describe('Extension: ExternalConverters', () => { + const mockBasePath = path.join(data.mockDir, BASE_DIR); + let controller: Controller; + + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); + const readdirSyncSpy = jest.spyOn(fs, 'readdirSync'); + const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); + const rmSyncSpy = jest.spyOn(fs, 'rmSync'); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync'); + + const zhcAddDefinitionSpy = jest.spyOn(zhc, 'addDefinition'); + const zhcRemoveExternalDefinitionsSpy = jest.spyOn(zhc, 'removeExternalDefinitions'); + + const mocksClear = [ + mockMQTT.endAsync, + mockMQTT.publishAsync, + mockLogger.debug, + mockLogger.error, + mockZHController.stop, + devices.bulb.save, + existsSyncSpy, + readdirSyncSpy, + mkdirSyncSpy, + rmSyncSpy, + writeFileSyncSpy, + zhcAddDefinitionSpy, + zhcRemoveExternalDefinitionsSpy, + ]; + + const useAssets = (): void => { + fs.cpSync(path.join(__dirname, '..', 'assets', BASE_DIR), mockBasePath, {recursive: true}); + }; + + const getFileCode = (fileName: string): string => { + return fs.readFileSync(path.join(__dirname, '..', 'assets', BASE_DIR, fileName), 'utf8'); + }; + + const getZ2MDevice = (zhDevice: unknown): Device => { + // @ts-expect-error private + return controller.zigbee.resolveEntity(zhDevice)! as Device; + }; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + zhc.removeExternalDefinitions(); // remove all external converters + mocksClear.forEach((m) => m.mockClear()); + data.writeDefaultConfiguration(); + data.writeDefaultState(); + settings.reRead(); + returnDevices.push(devices.external_converter_device.ieeeAddr, devices.coordinator.ieeeAddr); + + controller = new Controller(jest.fn(), jest.fn()); + }); + + afterEach(async () => { + fs.rmSync(mockBasePath, {recursive: true, force: true}); + + await controller?.stop(); + }); + + it('loads nothing from folder', async () => { + await controller.start(); + await flushPromises(); + + expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath); + expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0}); + }); + + it('loads from folder', async () => { + useAssets(); + + await controller.start(); + await flushPromises(); + + expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({ + description: 'external', + model: 'external_converter_device', + vendor: 'external', + zigbeeModel: ['external_converter_device'], + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/converters', + stringify([ + {name: 'mock-external-converter-multiple.js', code: getFileCode('mock-external-converter-multiple.js')}, + {name: 'mock-external-converter.js', code: getFileCode('mock-external-converter.js')}, + ]), + {retain: true, qos: 0}, + ); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, 'mock-external-converter-multiple.js'); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, 'mock-external-converter.js'); + expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + mock: 1, + model: 'external_converters_device_1', + zigbeeModel: ['external_converter_device_1'], + vendor: 'external_1', + description: 'external_1', + }), + ); + expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + mock: 2, + model: 'external_converters_device_2', + zigbeeModel: ['external_converter_device_2'], + vendor: 'external_2', + description: 'external_2', + }), + ); + expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + mock: true, + zigbeeModel: ['external_converter_device'], + vendor: 'external', + model: 'external_converter_device', + description: 'external', + }), + ); + + const bridgeDevices = mockMQTT.publishAsync.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/devices'); + expect(bridgeDevices.length).toBe(1); + expect(JSON.parse(bridgeDevices[0][1])).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + model_id: 'external_converter_device', + supported: true, + definition: expect.objectContaining({ + description: 'external', + model: 'external_converter_device', + }), + }), + ]), + ); + }); + + it('saves and removes from MQTT', async () => { + const converterName = 'foo.js'; + const converterCode = getFileCode('mock-external-converter.js'); + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({ + description: 'Automatically generated definition', + model: 'external_converter_device', + vendor: '', + zigbeeModel: ['external_converter_device'], + }); + + //-- SAVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode})); + await flushPromises(); + + expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({ + description: 'external', + model: 'external_converter_device', + vendor: 'external', + zigbeeModel: ['external_converter_device'], + }); + expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true}); + expect(writeFileSyncSpy).toHaveBeenCalledWith(converterFilePath, converterCode, 'utf8'); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(1); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, converterName); + expect(zhcAddDefinitionSpy).toHaveBeenCalledWith( + expect.objectContaining({ + mock: true, + zigbeeModel: ['external_converter_device'], + vendor: 'external', + model: 'external_converter_device', + description: 'external', + }), + ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([{name: converterName, code: converterCode}]), { + retain: true, + qos: 0, + }); + + //-- REMOVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName})); + await flushPromises(); + + expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({ + description: 'Automatically generated definition', + model: 'external_converter_device', + vendor: '', + zigbeeModel: ['external_converter_device'], + }); + expect(rmSyncSpy).toHaveBeenCalledWith(converterFilePath, {force: true}); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2); + expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, converterName); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0}); + }); + + it('returns error on invalid code', async () => { + const converterName = 'foo.js'; + const converterCode = 'definetly not a correct javascript code'; + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/converter/save', + expect.stringContaining(`"error":"foo.js contains invalid code`), + {retain: false, qos: 0}, + ); + expect(writeFileSyncSpy).not.toHaveBeenCalledWith(converterFilePath, converterCode, 'utf8'); + }); + + it('returns error on invalid removal', async () => { + const converterName = 'invalid.js'; + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/converter/remove', + stringify({data: {}, status: 'error', error: `${converterName} (${converterFilePath}) doesn't exists`}), + {retain: false, qos: 0}, + ); + expect(rmSyncSpy).not.toHaveBeenCalledWith(converterFilePath, {force: true}); + }); + + it('returns error on invalid definition', async () => { + const converterName = 'foo.js'; + const converterCode = getFileCode('mock-external-converter.js'); + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + const errorMsg = `Invalid definition`; + + zhcAddDefinitionSpy.mockImplementationOnce(() => { + throw new Error(errorMsg); + }); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/converter/save', expect.stringContaining(errorMsg), { + retain: false, + qos: 0, + }); + expect(writeFileSyncSpy).not.toHaveBeenCalledWith(converterFilePath, converterCode, 'utf8'); + }); + + it('returns error on failed removal', async () => { + const converterName = 'foo.js'; + const converterCode = getFileCode('mock-external-converter.js'); + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + //-- SAVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode})); + await flushPromises(); + + const errorMsg = `Failed to remove definition`; + + zhcRemoveExternalDefinitionsSpy.mockImplementationOnce(() => { + throw new Error(errorMsg); + }); + + //-- REMOVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/converter/remove', + stringify({data: {}, status: 'error', error: errorMsg}), + {retain: false, qos: 0}, + ); + expect(rmSyncSpy).not.toHaveBeenCalledWith(converterFilePath, {force: true}); + }); + + it('handles invalid payloads', async () => { + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: 'test.js', transaction: 1 /* code */})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/converter/save', + stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 1}), + {retain: false, qos: 0}, + ); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({namex: 'test.js', transaction: 2})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/converter/remove', + stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 2}), + {retain: false, qos: 0}, + ); + }); +}); diff --git a/test/extensions/externalExtensions.test.ts b/test/extensions/externalExtensions.test.ts new file mode 100644 index 0000000000..0190208a6f --- /dev/null +++ b/test/extensions/externalExtensions.test.ts @@ -0,0 +1,193 @@ +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 'node:fs'; +import path from 'node:path'; + +import stringify from 'json-stable-stringify-without-jsonify'; + +import {Controller} from '../../lib/controller'; +import * as settings from '../../lib/util/settings'; + +const BASE_DIR = 'external_extensions'; + +describe('Extension: ExternalExtensions', () => { + let controller: Controller; + const mockBasePath = path.join(data.mockDir, BASE_DIR); + + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); + const readdirSyncSpy = jest.spyOn(fs, 'readdirSync'); + const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); + const rmSyncSpy = jest.spyOn(fs, 'rmSync'); + const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync'); + + const mocksClear = [ + mockMQTT.endAsync, + mockMQTT.publishAsync, + mockLogger.debug, + mockLogger.error, + mockZHController.stop, + devices.bulb.save, + existsSyncSpy, + readdirSyncSpy, + mkdirSyncSpy, + rmSyncSpy, + writeFileSyncSpy, + ]; + + const useAssets = (): void => { + fs.cpSync(path.join(__dirname, '..', 'assets', BASE_DIR), mockBasePath, {recursive: true}); + }; + + const getFileCode = (fileName: string): string => { + return fs.readFileSync(path.join(__dirname, '..', 'assets', BASE_DIR, fileName), 'utf8'); + }; + + beforeAll(async () => { + jest.useFakeTimers(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + mocksClear.forEach((m) => m.mockClear()); + data.writeDefaultConfiguration(); + data.writeDefaultState(); + settings.reRead(); + returnDevices.splice(0); + + controller = new Controller(jest.fn(), jest.fn()); + }); + + afterEach(() => { + fs.rmSync(mockBasePath, {recursive: true, force: true}); + }); + + it('loads nothing from folder', async () => { + await controller.start(); + await flushPromises(); + + expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath); + expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0}); + }); + + it('loads from folder', async () => { + useAssets(); + + await controller.start(); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from constructor', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from start', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/extensions', + stringify([ + {name: 'example2Extension.js', code: getFileCode('example2Extension.js')}, + {name: 'exampleExtension.js', code: getFileCode('exampleExtension.js')}, + ]), + {retain: true, qos: 0}, + ); + }); + + it('saves and removes from MQTT', async () => { + const extensionName = 'foo.js'; + const extensionCode = getFileCode('exampleExtension.js'); + const extensionFilePath = path.join(mockBasePath, extensionName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + //-- SAVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: extensionName, code: extensionCode})); + await flushPromises(); + + expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true}); + expect(writeFileSyncSpy).toHaveBeenCalledWith(extensionFilePath, extensionCode, 'utf8'); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{name: extensionName, code: extensionCode}]), { + retain: true, + qos: 0, + }); + + //-- REMOVE + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: extensionName})); + await flushPromises(); + + expect(rmSyncSpy).toHaveBeenCalledWith(extensionFilePath, {force: true}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from stop', {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0}); + }); + + it('returns error on invalid code', async () => { + const extensionName = 'foo.js'; + const extensionCode = 'definetly not a correct javascript code'; + const extensionFilePath = path.join(mockBasePath, extensionName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: extensionName, code: extensionCode})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/save', + expect.stringContaining(`"error":"${extensionName} contains invalid code`), + {retain: false, qos: 0}, + ); + expect(writeFileSyncSpy).not.toHaveBeenCalledWith(extensionFilePath, extensionCode, 'utf8'); + }); + + it('returns error on invalid removal', async () => { + const converterName = 'invalid.js'; + const converterFilePath = path.join(mockBasePath, converterName); + + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: converterName})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/remove', + stringify({data: {}, status: 'error', error: `${converterName} (${converterFilePath}) doesn't exists`}), + {retain: false, qos: 0}, + ); + expect(rmSyncSpy).not.toHaveBeenCalledWith(converterFilePath, {force: true}); + }); + + it('handles invalid payloads', async () => { + await controller.start(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'test.js', transaction: 1 /* code */})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/save', + stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 1}), + {retain: false, qos: 0}, + ); + + mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({namex: 'test.js', transaction: 2})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/remove', + stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 2}), + {retain: false, qos: 0}, + ); + }); +}); diff --git a/test/extensions/frontend.test.ts b/test/extensions/frontend.test.ts new file mode 100644 index 0000000000..15ed616dca --- /dev/null +++ b/test/extensions/frontend.test.ts @@ -0,0 +1,395 @@ +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 'node: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('node:http', () => ({ + createServer: jest.fn().mockImplementation((onRequest) => { + mockHTTPOnRequest = onRequest; + return mockHTTP; + }), + Agent: jest.fn(), +})); + +jest.mock('node: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'], {enabled: true, port: 8081, host: '127.0.0.1'}); + settings.set(['homeassistant'], {enabled: 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'], {enabled: true, 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', + effect: null, + 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', + effect: null, + 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/extensions/groups.test.ts b/test/extensions/groups.test.ts new file mode 100644 index 0000000000..7db66bcd05 --- /dev/null +++ b/test/extensions/groups.test.ts @@ -0,0 +1,773 @@ +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, resetGroupMembers, 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; + + beforeAll(async () => { + jest.useFakeTimers(); + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + await flushPromises(); + }); + + afterAll(async () => { + jest.useRealTimers(); + }); + + beforeEach(() => { + resetGroupMembers(); + data.writeDefaultConfiguration(); + settings.reRead(); + mockMQTT.publishAsync.mockClear(); + groups.gledopto_group.command.mockClear(); + zhcToZigbee.__clearStore__(); + // @ts-expect-error private + controller.state.state = {}; + }); + + 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); + + mockMQTT.publishAsync.mockClear(); + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; + await mockZHEvents.message(payload); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should not republish identical optimistic group states', async () => { + const device1 = devices.bulb_2; + const device2 = devices.bulb_color_2; + + mockMQTT.publishAsync.mockClear(); + await mockZHEvents.message({ + data: {onOff: 1}, + cluster: 'genOnOff', + device: device1, + endpoint: device1.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); + await mockZHEvents.message({ + data: {onOff: 1}, + cluster: 'genOnOff', + device: device2, + endpoint: device2.getEndpoint(1), + type: 'attributeReport', + linkquality: 10, + }); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(6); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_with_tradfri', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/ha_discovery_group', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should publish state change of all members 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); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should not publish state change when group changes state and device is disabled', async () => { + 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); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should publish state change for group when members state change', async () => { + // Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725 + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; + group.members.push(endpoint); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should publish state of device with endpoint name', async () => { + const group = groups.gledopto_group; + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {}); + }); + + it('Should publish state of group when specific state of specific endpoint is changed', async () => { + const group = groups.gledopto_group; + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(group.command).toHaveBeenCalledTimes(0); + }); + + it('Should publish state change of all members when a group changes its state, filtered', 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, filtered_attributes: ['brightness']}}); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 100}), { + retain: false, + qos: 0, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Shouldnt publish group state change when a group is not optimistic', 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', optimistic: false, retain: false}}); + + mockMQTT.publishAsync.mockClear(); + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10}; + await mockZHEvents.message(payload); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + 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)!; + groups.group_1.members.push(endpoint); + groups.group_2.members.push(endpoint); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0}); + }); + + it('Should not publish state change off if any lights within are still on when changed via device', async () => { + 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); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + 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 = 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', retain: false, off_state: 'last_member_state'}, + }); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Should not publish state change off if any lights within with non default-ep are still on when changed via device', async () => { + const device_1 = devices.bulb_color; + const device_2 = devices.QBKG03LM; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(2)!; + const group = groups.group_1; + group.members.push(endpoint_1); + group.members.push(endpoint_2); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Should not publish state change off if any lights within are still on when changed via device with non default-ep', async () => { + const device_1 = devices.bulb_color; + const device_2 = devices.QBKG03LM; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(2)!; + const endpoint_3 = device_2.getEndpoint(3)!; + endpoint_3.removeFromGroup(groups.ha_discovery_group); + const group = groups.group_1; + group.members.push(endpoint_1); + group.members.push(endpoint_2); + group.members.push(endpoint_3); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_left: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF', state_right: 'ON'}), { + retain: false, + qos: 0, + }); + }); + + it('Should publish state change off if all lights within turn off with non default-ep', async () => { + const device_1 = devices.bulb_color; + const device_2 = devices.QBKG03LM; + const endpoint_1 = device_1.getEndpoint(1)!; + const endpoint_2 = device_2.getEndpoint(2)!; + const group = groups.group_1; + group.members.push(endpoint_1); + group.members.push(endpoint_2); + settings.set(['groups'], { + 1: {friendly_name: 'group_1', retain: false}, + }); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_left: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Should not publish state change off if any lights within are still on when changed via shared group', async () => { + 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)!; + 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(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Should publish state change off if all lights within turn off', async () => { + 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', retain: false}, + }); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Should only update group state with changed properties', async () => { + 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', retain: false}, + }); + mockMQTT.publishAsync.mockClear(); + + 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(); + mockMQTT.publishAsync.mockClear(); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({color_mode: 'color_temp', color_temp: 300, state: 'OFF'}), + {retain: false, qos: 0}, + ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), { + retain: true, + qos: 0, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), + {retain: false, qos: 0}, + ); + }); + + it('Should publish state change off even when missing current state', async () => { + 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', retain: false}, + }); + + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'})); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + // @ts-expect-error private + controller.state.state = {}; + + await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'})); + await flushPromises(); + + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0}); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0}); + }); + + it('Add to group via MQTT', async () => { + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = groups.group_1; + expect(group.members.length).toBe(0); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, transaction: '123', status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Add to group via MQTT fails', async () => { + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; + expect(group.members.length).toBe(0); + endpoint.addToGroup.mockImplementationOnce(() => { + throw new Error('timeout'); + }); + await flushPromises(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'})); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: 'Failed to add from group (timeout)'}), + {retain: false, qos: 0}, + ); + }); + + it('Add to group with slashes via MQTT', async () => { + 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}}); + expect(group.members.length).toBe(0); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group/with/slashes'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Add to group via MQTT with postfix', async () => { + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; + expect(group.members.length).toBe(0); + mockMQTT.publishAsync.mockClear(); + 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Add to group via MQTT with postfix shouldnt add it twice', async () => { + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; + expect(group.members.length).toBe(0); + mockMQTT.publishAsync.mockClear(); + 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', endpoint: '3'}), + ); + await flushPromises(); + expect(group.members).toStrictEqual([endpoint]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group via MQTT', async () => { + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; + group.members.push(endpoint); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'})); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group via MQTT keeping device reporting', async () => { + const device = devices.bulb_color; + const endpoint = device.getEndpoint(1)!; + const group = groups.group_1; + group.members.push(endpoint); + mockMQTT.publishAsync.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(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group via MQTT with postfix variant 1', async () => { + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; + group.members.push(endpoint); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: '3'}), + ); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {device: '0x0017880104e45542', endpoint: '3', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group via MQTT with postfix variant 2', async () => { + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; + group.members.push(endpoint); + mockMQTT.publishAsync.mockClear(); + 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.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {device: 'wall_switch_double', endpoint: '3', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group via MQTT with postfix variant 3', async () => { + const device = devices.QBKG03LM; + const endpoint = device.getEndpoint(3)!; + const group = groups.group_1; + group.members.push(endpoint); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/remove', + stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: 'right'}), + ); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {device: '0x0017880104e45542', endpoint: 'right', group: 'group_1'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Remove from group all', async () => { + const group = groups.group_1; + groups.group_1.members.push(devices.QBKG03LM.endpoints[2]); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542', endpoint: 'right'})); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove_all', + stringify({data: {device: '0x0017880104e45542', endpoint: 'right'}, status: 'ok'}), + {retain: false, qos: 0}, + ); + }); + + it('Error when adding to non-existing group', async () => { + mockLogger.error.mockClear(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/remove', + stringify({data: {}, status: 'error', error: "Group 'group_1_not_existing' does not exist"}), + {retain: false, qos: 0}, + ); + }); + + it('Error when adding a non-existing device', async () => { + mockLogger.error.mockClear(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'})); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: "Device 'bulb_color_not_existing' does not exist"}), + {retain: false, qos: 0}, + ); + }); + + it('Error when adding a non-existing endpoint', async () => { + mockLogger.error.mockClear(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message( + 'zigbee2mqtt/bridge/request/group/members/add', + stringify({group: 'group_1', device: 'bulb_color', endpoint: 'not_existing_endpoint'}), + ); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).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}, + ); + }); + + it('Error when invalid payload', async () => { + mockLogger.error.mockClear(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', devicez: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + ); + }); + + it('Error when add/remove with invalid payload', async () => { + mockLogger.error.mockClear(); + mockMQTT.publishAsync.mockClear(); + mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({groupz: 'group_1', device: 'bulb_color'})); + await flushPromises(); + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object)); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/group/members/add', + stringify({data: {}, status: 'error', error: 'Invalid payload'}), + {retain: false, qos: 0}, + ); + }); + + it('Should only include relevant properties when publishing member states', async () => { + 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)!); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 50}), { + retain: false, + qos: 0, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 50}), { + retain: false, + qos: 0, + }); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 50}), { + retain: true, + qos: 0, + }); + + mockMQTT.publishAsync.mockClear(); + await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}})); + await flushPromises(); + expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color', + stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), + {retain: false, qos: 0}, + ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( + 'zigbee2mqtt/group_1', + stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}), + {retain: false, qos: 0}, + ); + expect(mockMQTT.publishAsync).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 63% rename from test/homeassistant.test.js rename to test/extensions/homeassistant.test.ts index 8475290db8..1834f0d783 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 'node: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.publishAsync, 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); + settings.set(['homeassistant'], {enabled: true}); data.writeDefaultConfiguration(); settings.reRead(); data.writeEmptyState(); - MQTT.publish.mockClear(); - sleep.mock(); - controller = new Controller(false); + mockMQTT.publishAsync.mockClear(); + mockSleep.mock(); + controller = new Controller(jest.fn(), jest.fn()); await controller.start(); }); afterAll(async () => { jest.useRealTimers(); - sleep.restore(); + mockSleep.restore(); }); - 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: []}}; + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings.reRead(); + settings.set(['homeassistant'], {enabled: 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', 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); } }); }); @@ -89,13 +110,13 @@ describe('HomeAssistant extension', () => { }); it('Should discover devices and groups', async () => { - settings.set(['homeassistant'], {experimental_event_entities: true}); + settings.set(['homeassistant', 'experimental_event_entities'], true); await resetExtension(); 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', @@ -109,7 +130,6 @@ describe('HomeAssistant extension', () => { }, max_mireds: 454, min_mireds: 250, - json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', name: null, schema: 'json', state_topic: 'zigbee2mqtt/ha_discovery_group', @@ -132,15 +152,13 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/light/1221051039810110150109113116116_9/light/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/light/1221051039810110150109113116116_9/light/config', stringify(payload), { + retain: true, + qos: 1, + }); 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'], @@ -150,7 +168,6 @@ describe('HomeAssistant extension', () => { manufacturer: 'Zigbee2MQTT', via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae', }, - json_attributes_topic: 'zigbee2mqtt/ha_discovery_group', name: null, payload_off: 'OFF', payload_on: 'ON', @@ -161,11 +178,10 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( + expect(mockMQTT.publishAsync).toHaveBeenCalledWith( 'homeassistant/switch/1221051039810110150109113116116_9/switch/config', stringify(payload), {retain: true, qos: 1}, - expect.any(Function), ); payload = { @@ -174,28 +190,24 @@ describe('HomeAssistant extension', () => { state_class: 'measurement', value_template: '{{ value_json.temperature }}', state_topic: 'zigbee2mqtt/weather_sensor', - json_attributes_topic: 'zigbee2mqtt/weather_sensor', object_id: 'weather_sensor_temperature', unique_id: '0x0017880104e45522_temperature_zigbee2mqtt', origin: origin, device: { identifiers: ['zigbee2mqtt_0x0017880104e45522'], name: 'weather_sensor', - sw_version: null, model: 'Temperature and humidity sensor (WSDCGQ11LM)', 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, }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/sensor/0x0017880104e45522/temperature/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45522/temperature/config', stringify(payload), { + retain: true, + qos: 1, + }); payload = { unit_of_measurement: '%', @@ -203,7 +215,6 @@ describe('HomeAssistant extension', () => { state_class: 'measurement', value_template: '{{ value_json.humidity }}', state_topic: 'zigbee2mqtt/weather_sensor', - json_attributes_topic: 'zigbee2mqtt/weather_sensor', object_id: 'weather_sensor_humidity', unique_id: '0x0017880104e45522_humidity_zigbee2mqtt', origin: origin, @@ -211,20 +222,17 @@ 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', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/sensor/0x0017880104e45522/humidity/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45522/humidity/config', stringify(payload), { + retain: true, + qos: 1, + }); payload = { unit_of_measurement: 'hPa', @@ -232,7 +240,6 @@ describe('HomeAssistant extension', () => { state_class: 'measurement', value_template: '{{ value_json.pressure }}', state_topic: 'zigbee2mqtt/weather_sensor', - json_attributes_topic: 'zigbee2mqtt/weather_sensor', object_id: 'weather_sensor_pressure', unique_id: '0x0017880104e45522_pressure_zigbee2mqtt', origin: origin, @@ -240,20 +247,17 @@ 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', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/sensor/0x0017880104e45522/pressure/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45522/pressure/config', stringify(payload), { + retain: true, + qos: 1, + }); payload = { unit_of_measurement: '%', @@ -261,7 +265,6 @@ describe('HomeAssistant extension', () => { state_class: 'measurement', value_template: '{{ value_json.battery }}', state_topic: 'zigbee2mqtt/weather_sensor', - json_attributes_topic: 'zigbee2mqtt/weather_sensor', object_id: 'weather_sensor_battery', unique_id: '0x0017880104e45522_battery_zigbee2mqtt', origin: origin, @@ -270,20 +273,17 @@ 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', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/sensor/0x0017880104e45522/battery/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45522/battery/config', stringify(payload), { + retain: true, + qos: 1, + }); payload = { icon: 'mdi:signal', @@ -293,7 +293,6 @@ describe('HomeAssistant extension', () => { state_class: 'measurement', value_template: '{{ value_json.linkquality }}', state_topic: 'zigbee2mqtt/weather_sensor', - json_attributes_topic: 'zigbee2mqtt/weather_sensor', name: 'Linkquality', object_id: 'weather_sensor_linkquality', unique_id: '0x0017880104e45522_linkquality_zigbee2mqtt', @@ -301,33 +300,28 @@ 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', }, - availability: [{topic: 'zigbee2mqtt/bridge/state'}], + availability: [{topic: 'zigbee2mqtt/bridge/state', value_template: '{{ value_json.state }}'}], }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/sensor/0x0017880104e45522/linkquality/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45522/linkquality/config', stringify(payload), { + retain: true, + qos: 1, + }); 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'], 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', name: 'Left', payload_off: 'OFF', payload_on: 'ON', @@ -338,25 +332,21 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_left }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/switch/0x0017880104e45542/switch_left/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/switch/0x0017880104e45542/switch_left/config', stringify(payload), { + retain: true, + qos: 1, + }); 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'], 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', name: 'Right', payload_off: 'OFF', payload_on: 'ON', @@ -367,15 +357,13 @@ describe('HomeAssistant extension', () => { value_template: '{{ value_json.state_right }}', }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/switch/0x0017880104e45542/switch_right/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/switch/0x0017880104e45542/switch_right/config', stringify(payload), { + retain: true, + qos: 1, + }); 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'], @@ -387,12 +375,10 @@ 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, effect_list: ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect'], - json_attributes_topic: 'zigbee2mqtt/bulb', name: null, schema: 'json', state_topic: 'zigbee2mqtt/bulb', @@ -401,42 +387,42 @@ describe('HomeAssistant extension', () => { origin: origin, }; - expect(MQTT.publish).toHaveBeenCalledWith( - 'homeassistant/light/0x000b57fffec6a5b2/light/config', - stringify(payload), - {retain: true, qos: 1}, - expect.any(Function), - ); + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/light/0x000b57fffec6a5b2/light/config', stringify(payload), { + retain: true, + qos: 1, + }); 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'], icon: 'mdi:gesture-double-tap', - 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. value_template: - "{% set patterns = [\n{\"pattern\": '^(?P