From cebfb0ad9bed3cdf72be01fa76cc9ec8138296e4 Mon Sep 17 00:00:00 2001 From: Ben <43026681+bwp91@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:10:44 +0100 Subject: [PATCH] convert repo to esm --- .eslintrc | 37 - .gitattributes | 2 - .github/FUNDING.yml | 2 +- .github/labeler.yml | 6 +- .github/node-persist-ignore.js | 18 +- .github/release.yml | 18 +- .github/workflows/alpha-release.yml | 12 +- .github/workflows/beta-release.yml | 14 +- .github/workflows/codeql-analysis.yml | 6 +- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/release.yml | 8 +- .../workflows/wiki-change-notification.yml | 2 +- @types/bonjour-hap.d.ts | 103 +- @types/simple-plist.d.ts | 4 - README.md | 35 +- __mocks__/bonjour-hap.ts | 22 +- __mocks__/node-persist.ts | 18 +- docs/functions/IsKnownHAPStatusError.html | 2 +- docs/modules.html | 2 +- eslint.config.js | 48 + jest.config.json | 12 - package-lock.json | 7606 +++++++++-------- package.json | 85 +- src/accessories/AirConditioner_accessory.ts | 175 +- src/accessories/AppleTVRemote_accessory.ts | 195 +- src/accessories/Camera_accessory.ts | 755 +- src/accessories/Fan_accessory.ts | 61 +- src/accessories/GarageDoorOpener_accessory.ts | 90 +- .../Light-AdaptiveLighting_accessory.ts | 128 +- src/accessories/Light_accessory.ts | 168 +- src/accessories/Lock_accessory.ts | 69 +- src/accessories/MotionSensor_accessory.ts | 51 +- src/accessories/Outlet_accessory.ts | 76 +- src/accessories/SmartSpeaker_accessory.ts | 62 +- src/accessories/Sprinkler_accessory.ts | 125 +- src/accessories/TV_accessory.ts | 104 +- .../TemperatureSensor_accessory.ts | 41 +- src/accessories/Wi-FiRouter_accessory.ts | 23 +- src/accessories/Wi-FiSatellite_accessory.ts | 25 +- src/accessories/gstreamer-audioProducer.ts | 198 +- src/accessories/types.ts | 168 +- src/index.spec.ts | 14 +- src/index.ts | 75 +- src/internal-types.ts | 46 +- src/lib/Accessory.spec.ts | 2635 +++--- src/lib/Accessory.ts | 1699 ++-- src/lib/Advertiser.spec.ts | 42 +- src/lib/Advertiser.ts | 538 +- src/lib/Bridge.ts | 6 +- src/lib/Characteristic.spec.ts | 2446 +++--- src/lib/Characteristic.ts | 1835 ++-- src/lib/HAPServer.spec.ts | 712 +- src/lib/HAPServer.ts | 959 ++- src/lib/Service.spec.ts | 246 +- src/lib/Service.ts | 566 +- src/lib/camera/RTPProxy.ts | 353 +- src/lib/camera/RTPStreamManagement.ts | 1434 ++-- src/lib/camera/RecordingManagement.spec.ts | 65 +- src/lib/camera/RecordingManagement.ts | 917 +- src/lib/camera/index.ts | 6 +- .../controller/AdaptiveLightingController.ts | 885 +- src/lib/controller/CameraController.spec.ts | 306 +- src/lib/controller/CameraController.ts | 661 +- src/lib/controller/Controller.ts | 255 +- src/lib/controller/DoorbellController.spec.ts | 30 +- src/lib/controller/DoorbellController.ts | 84 +- src/lib/controller/RemoteController.ts | 1350 +-- src/lib/controller/index.ts | 10 +- src/lib/datastream/DataStreamManagement.ts | 159 +- src/lib/datastream/DataStreamParser.ts | 822 +- src/lib/datastream/DataStreamServer.ts | 920 +- src/lib/datastream/index.ts | 6 +- src/lib/dbus/align.ts | 12 + src/lib/dbus/bus.ts | 241 + src/lib/dbus/constants.ts | 54 + src/lib/dbus/dbus-buffer.ts | 192 + src/lib/dbus/handshake.ts | 152 + src/lib/dbus/index.ts | 148 + src/lib/dbus/introspect.ts | 205 + src/lib/dbus/marshall.ts | 110 + src/lib/dbus/marshallers.ts | 342 + src/lib/dbus/message.ts | 126 + src/lib/dbus/put.ts | 89 + src/lib/dbus/readline.ts | 27 + src/lib/dbus/signature.ts | 64 + src/lib/dbus/stdifaces.ts | 205 + .../CharacteristicDefinitions.spec.ts | 2880 +++---- .../definitions/CharacteristicDefinitions.ts | 3062 +++---- .../definitions/ServiceDefinitions.spec.ts | 3137 +++---- src/lib/definitions/ServiceDefinitions.ts | 1298 ++- src/lib/definitions/generate-definitions.ts | 852 +- .../definitions/generator-configuration.ts | 410 +- src/lib/definitions/index.ts | 4 +- src/lib/gen/HomeKit.ts | 6 - src/lib/model/AccessoryInfo.spec.ts | 55 +- src/lib/model/AccessoryInfo.ts | 277 +- src/lib/model/ControllerStorage.ts | 233 +- src/lib/model/HAPStorage.spec.ts | 80 +- src/lib/model/HAPStorage.ts | 30 +- src/lib/model/IdentifierCache.spec.ts | 239 +- src/lib/model/IdentifierCache.ts | 115 +- src/lib/tv/AccessControlManagement.ts | 98 +- src/lib/util/checkName.spec.ts | 76 +- src/lib/util/checkName.ts | 15 +- src/lib/util/clone.ts | 13 +- src/lib/util/color-utils.ts | 24 +- src/lib/util/eventedhttp.spec.ts | 376 +- src/lib/util/eventedhttp.ts | 711 +- src/lib/util/hapCrypto.spec.ts | 256 +- src/lib/util/hapCrypto.ts | 153 +- src/lib/util/hapStatusError.spec.ts | 30 +- src/lib/util/hapStatusError.ts | 14 +- src/lib/util/net-utils.spec.ts | 172 +- src/lib/util/net-utils.ts | 50 +- src/lib/util/once.spec.ts | 36 +- src/lib/util/once.ts | 13 +- src/lib/util/promise-utils.ts | 34 +- src/lib/util/request-util.spec.ts | 84 +- src/lib/util/request-util.ts | 125 +- src/lib/util/time.ts | 17 +- src/lib/util/tlv.spec.ts | 440 +- src/lib/util/tlv.ts | 234 +- src/lib/util/uuid.spec.ts | 122 +- src/lib/util/uuid.ts | 83 +- src/test-utils/HAPHTTPClient.ts | 386 +- src/test-utils/HAPHTTPError.ts | 14 +- src/test-utils/PairSetupClient.ts | 226 +- src/test-utils/PairVerifyClient.ts | 205 +- src/test-utils/tlvError.ts | 10 +- src/types.ts | 194 +- src/types/dbus-native.d.ts | 38 +- src/types/node-persist.d.ts | 46 +- tsconfig.json | 26 +- vitest.config.js | 11 + 135 files changed, 26270 insertions(+), 23369 deletions(-) delete mode 100644 .eslintrc delete mode 100644 .gitattributes delete mode 100644 @types/simple-plist.d.ts create mode 100644 eslint.config.js delete mode 100644 jest.config.json create mode 100644 src/lib/dbus/align.ts create mode 100644 src/lib/dbus/bus.ts create mode 100644 src/lib/dbus/constants.ts create mode 100644 src/lib/dbus/dbus-buffer.ts create mode 100644 src/lib/dbus/handshake.ts create mode 100644 src/lib/dbus/index.ts create mode 100644 src/lib/dbus/introspect.ts create mode 100644 src/lib/dbus/marshall.ts create mode 100644 src/lib/dbus/marshallers.ts create mode 100644 src/lib/dbus/message.ts create mode 100644 src/lib/dbus/put.ts create mode 100644 src/lib/dbus/readline.ts create mode 100644 src/lib/dbus/signature.ts create mode 100644 src/lib/dbus/stdifaces.ts delete mode 100644 src/lib/gen/HomeKit.ts create mode 100644 vitest.config.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index d19d44c97..000000000 --- a/.eslintrc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin - ], - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - }, - "ignorePatterns": [ - "dist/" - ], - "rules": { - "quotes": ["error", "double"], - "indent": ["error", 2, { "SwitchCase": 0 }], - "linebreak-style": ["error", "unix"], - "semi": ["error", "always"], - - "comma-dangle": ["error", "always-multiline"], - "dot-notation": "error", - "eqeqeq": ["error", "smart"], - "curly": ["error", "all"], - "brace-style": ["error"], - "prefer-arrow-callback": "warn", - "max-len": ["warn", 160], - "object-curly-spacing": ["error", "always"], - - "no-use-before-define": "off", - "@typescript-eslint/no-use-before-define": ["error", { "classes": false, "enums": false }], - "@typescript-eslint/no-unused-vars": ["error", { "caughtErrors": "none" }], - - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/explicit-module-boundary-types": "error" - } -} diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 2453a72f1..000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore all differences in line endings -* -crlf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 656805420..58887e082 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ github: Supereg -custom: "https://paypal.me/Supereg" +custom: 'https://paypal.me/Supereg' diff --git a/.github/labeler.yml b/.github/labeler.yml index 78331e16c..552382aa2 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,11 +1,11 @@ # Add 'beta' label to any PR where the base branch name starts with `beta` or has a `beta` section in the name beta: - - base-branch: ['^beta', 'beta', 'beta*'] + - base-branch: [^beta, beta, 'beta*'] # Add 'beta' label to any PR where the base branch name starts with `beta` or has a `beta` section in the name alpha: - - base-branch: ['^alpha', 'alpha', 'alpha*'] + - base-branch: [^alpha, alpha, 'alpha*'] # Add 'latest' label to any PR where the base branch name starts with `latest` or has a `latest` section in the name latest: - - base-branch: ['^latest', 'latest', 'latest*'] \ No newline at end of file + - base-branch: [^latest, latest, 'latest*'] diff --git a/.github/node-persist-ignore.js b/.github/node-persist-ignore.js index 86d73aa40..c16e43364 100644 --- a/.github/node-persist-ignore.js +++ b/.github/node-persist-ignore.js @@ -2,17 +2,17 @@ * This script tried to solve the problem of having types collisions of node-persist types. */ -const path = require("path"); -const fs = require("fs"); +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' -const storageDefinition = "./dist/lib/model/HAPStorage.d.ts"; -const resolved = path.resolve(storageDefinition); +const storageDefinition = './dist/lib/model/HAPStorage.d.ts' +const resolved = resolve(storageDefinition) -if (!fs.existsSync(resolved)) { - throw new Error("Tried to update definition but could not find HAPStorage.d.ts!"); +if (!existsSync(resolved)) { + throw new Error('Tried to update definition but could not find HAPStorage.d.ts!') } -const rows = fs.readFileSync(resolved, "utf8").split("\n"); -rows.unshift("// @ts-ignore"); +const rows = readFileSync(resolved, 'utf8').split('\n') +rows.unshift('// @ts-expect-error') -fs.writeFileSync(resolved, rows.join("\n")); +writeFileSync(resolved, rows.join('\n')) diff --git a/.github/release.yml b/.github/release.yml index 0dee4400c..53f7d6df7 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -4,18 +4,18 @@ changelog: categories: - title: Breaking Changes 🛠 labels: - - 'breaking change' + - breaking change - title: Featured Changes ✨ labels: - - 'feature' - - 'enhancement' + - feature + - enhancement - title: Bug Fixes 🐛 labels: - - 'fix' - - 'bugfix' - - 'bug' + - fix + - bugfix + - bug - title: Other Changes labels: - - "chore" - - "housekeeping" - - "*" + - chore + - housekeeping + - '*' diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 9c7b9a31e..be5dce68e 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -8,12 +8,12 @@ on: jobs: publish: if: ${{ github.repository == 'homebridge/HAP-NodeJS' }} - uses: homebridge/.github/.github/workflows/npm-publish.yml@latest + uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest with: - tag: 'alpha' + tag: alpha dynamically_adjust_version: true - npm_version_command: 'pre' - pre_id: 'alpha' + npm_version_command: pre + pre_id: alpha secrets: npm_auth_token: ${{ secrets.npm_token }} @@ -22,9 +22,9 @@ jobs: needs: [publish] uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest with: - title: "HAP-NodeJS Alpha Release" + title: HAP-NodeJS Alpha Release description: | Version `v${{ needs.publish.outputs.NPM_VERSION }}` - url: "https://github.com/homebridge/homebridge/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" + url: 'https://github.com/homebridge/homebridge/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}' secrets: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index f72f4aed8..6cdd958c2 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -21,23 +21,23 @@ jobs: if: ${{ github.repository == 'homebridge/HAP-NodeJS' }} - uses: homebridge/.github/.github/workflows/npm-publish.yml@latest + uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest with: - tag: 'beta' + tag: beta dynamically_adjust_version: true - npm_version_command: 'pre' - pre_id: 'beta' + npm_version_command: pre + pre_id: beta secrets: npm_auth_token: ${{ secrets.npm_token }} github-releases-to-discord: name: Discord Webhooks - needs: [build_and_test,publish] + needs: [build_and_test, publish] uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest with: - title: "HAP-NodeJS Beta Release" + title: HAP-NodeJS Beta Release description: | Version `v${{ needs.publish.outputs.NPM_VERSION }}` - url: "https://github.com/homebridge/homebridge/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" + url: 'https://github.com/homebridge/homebridge/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}' secrets: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8b4a56e14..1dd177666 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,10 +1,10 @@ -name: "CodeQL" +name: CodeQL on: push: - branches: [ master, beta* ] + branches: [master, beta*] pull_request: - branches: [ master, beta* ] + branches: [master, beta*] schedule: - cron: '44 16 * * 5' diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index df2d34624..7b5503659 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -9,4 +9,4 @@ jobs: stale: uses: homebridge/.github/.github/workflows/pr-labeler.yml@latest secrets: - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 25177f97d..5c50042cf 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -12,4 +12,4 @@ jobs: stale: uses: homebridge/.github/.github/workflows/release-drafter.yml@latest secrets: - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ef9aac9b..2294fc761 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,18 +19,18 @@ jobs: if: ${{ github.repository == 'homebridge/HAP-NodeJS' }} - uses: homebridge/.github/.github/workflows/npm-publish.yml@latest + uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest secrets: npm_auth_token: ${{ secrets.npm_token }} github-releases-to-discord: name: Discord Webhooks - needs: [build_and_test,publish] + needs: [build_and_test, publish] uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest with: - title: "HAP-NodeJS Release" + title: HAP-NodeJS Release description: | Version `v${{ needs.publish.outputs.NPM_VERSION }}` - url: "https://github.com/homebridge/homebridge-config-ui-x/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}" + url: 'https://github.com/homebridge/homebridge-config-ui-x/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}' secrets: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} diff --git a/.github/workflows/wiki-change-notification.yml b/.github/workflows/wiki-change-notification.yml index 1089f163b..aaf9b8d70 100644 --- a/.github/workflows/wiki-change-notification.yml +++ b/.github/workflows/wiki-change-notification.yml @@ -7,7 +7,7 @@ jobs: notify: runs-on: ubuntu-latest steps: - - uses: 'oznu/gh-wiki-edit-discord-notification@main' + - uses: oznu/gh-wiki-edit-discord-notification@main with: discord-webhook-url: ${{ secrets.DISCORD_WEBHOOK_WIKI_EDIT }} ignore-collaborators: true diff --git a/@types/bonjour-hap.d.ts b/@types/bonjour-hap.d.ts index 9f152f845..9b22d4389 100644 --- a/@types/bonjour-hap.d.ts +++ b/@types/bonjour-hap.d.ts @@ -1,66 +1,63 @@ -declare module "bonjour-hap" { - +declare module 'bonjour-hap' { export const enum Protocols { - TCP = "tcp", - UDP = "udp", + TCP = 'tcp', + UDP = 'udp', } - export type Nullable = T | null; - export type TxtRecord = Record; + export type Nullable = T | null + export type TxtRecord = Record export class BonjourHAPService { - name: string; - type: string; - subtypes: Nullable; - protocol: Protocols; - host: string; - port: number; - fqdn: string; - txt: Nullable>; - published: boolean; - - start(): void; - stop(callback?: () => void): void; - destroy(): void; - updateTxt(txt: TxtRecord, silent?: boolean): void; + name: string + type: string + subtypes: Nullable + protocol: Protocols + host: string + port: number + fqdn: string + txt: Nullable> + published: boolean + + start(): void + stop(callback?: () => void): void + destroy(): void + updateTxt(txt: TxtRecord, silent?: boolean): void } - export type PublishOptions = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - category?: any, - host?: string; - name?: string; - pincode?: string; - port: number; - protocol?: Protocols; - subtypes?: string[]; - txt?: Record; - type?: string; - username?: string; - - addUnsafeServiceEnumerationRecord?: boolean, - - restrictedAddresses?: string[]; - disabledIpv6?: boolean; - }; + export interface PublishOptions { + category?: any + host?: string + name?: string + pincode?: string + port: number + protocol?: Protocols + subtypes?: string[] + txt?: Record + type?: string + username?: string + + addUnsafeServiceEnumerationRecord?: boolean + + restrictedAddresses?: string[] + disabledIpv6?: boolean + } export class BonjourHAP { - publish(options: PublishOptions): BonjourHAPService; - unpublishAll(callback: () => void): void; - destroy(): void; + publish(options: PublishOptions): BonjourHAPService + unpublishAll(callback: () => void): void + destroy(): void } + export interface MulticastOptions { + multicast?: boolean + interface?: string + port?: number + ip?: string + ttl?: number + loopback?: boolean + reuseAddr?: boolean + } + function createWithOptions(options?: MulticastOptions): BonjourHAP - export type MulticastOptions = { - multicast?: boolean; - interface?: string; - port?: number; - ip?: string; - ttl?: number; - loopback?: boolean; - reuseAddr?: boolean; - }; - function createWithOptions(options?: MulticastOptions): BonjourHAP; - - export default createWithOptions; + export default createWithOptions } diff --git a/@types/simple-plist.d.ts b/@types/simple-plist.d.ts deleted file mode 100644 index f71beace7..000000000 --- a/@types/simple-plist.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "simple-plist" { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export function readFileSync(path: string): Record; -} diff --git a/README.md b/README.md index f95061315..4bf40c084 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@

- + # HAP-NodeJS - - - -
- - - Coverage Status + + + +
+ + +Coverage Status
@@ -18,9 +18,9 @@ HAP-NodeJS is an implementation of the HomeKit Accessory Server as specified in which is defined by Apple as part of the HomeKit Framework. HAP-NodeJS is intended to be used as a library to easily create your own HomeKit Accessory on a Raspberry Pi, -Intel Edison, or any other platform that can run Node.js :) +Intel Edison, or any other platform that can run Node.js :) If you are searching for a pluggable HomeKit bridge with over a thousand community driven plugins to bring HomeKit -support to devices which do not support HomeKit out of the box, you may want to look at the +support to devices which do not support HomeKit out of the box, you may want to look at the [homebridge][project-homebridge] project (which also uses HAP-NodeJS internally). The implementation tries to follow the HAP specification as close as it can, but may differ in some cases. @@ -28,7 +28,7 @@ HAP-NodeJS is not an Apple certified HAP implementation, as this is only availab ## Getting started -You may start by having a look at our [Wiki][wiki], especially have a look at the +You may start by having a look at our [Wiki][wiki], especially have a look at the [Important HomeKit Terminology][hk-terminology] used in this project. There is also a pretty detailed guide on [how to start developing with HAP-NodeJS][dev-guide]. @@ -43,11 +43,11 @@ If you wish to do a contribution please read through our [CONTRIBUTING][contribu ## Projects based on HAP-NodeJS -- [Homebridge][project-homebridge] - HomeKit support for the impatient - Pluggable HomeKit Bridge. - Plugins available for e.g. Pilight, Telldus TDtool, Savant, Netatmo, Open Pixel Control, HomeWizard, Fritz!Box, - LG WebOS TV, Home Assistant, HomeMatic and many more. -- [OpenHAB-HomeKit-Bridge][project-openhab-homekit-bridge] - OpenHAB HomeKit Bridge bridges openHAB items to - Apples HomeKit Accessory Protocol. +- [Homebridge][project-homebridge] - HomeKit support for the impatient - Pluggable HomeKit Bridge. + Plugins available for e.g. Pilight, Telldus TDtool, Savant, Netatmo, Open Pixel Control, HomeWizard, Fritz!Box, + LG WebOS TV, Home Assistant, HomeMatic and many more. +- [OpenHAB-HomeKit-Bridge][project-openhab-homekit-bridge] - OpenHAB HomeKit Bridge bridges openHAB items to + Apples HomeKit Accessory Protocol. - [homekit2mqtt][project-homekit2mqtt] - HomeKit to MQTT bridge. - [pimatic-hap][project-pimatic-hap] - Pimatic homekit bridge. - [node-red-contrib-homekit][project-node-red-contrib-homekit] - Node-RED nodes to simulate Apple HomeKit devices. @@ -71,10 +71,8 @@ If you are interested in HAP over BTLE, you might want to check [this][link-hap- [dev-guide]: https://github.com/homebridge/HAP-NodeJS/wiki/Using-HAP-NodeJS-as-a-library [faq-debug]: https://github.com/homebridge/HAP-NodeJS/wiki/FAQ#debug-mode [contributing]: https://github.com/homebridge/HAP-NodeJS/blob/master/CONTRIBUTING.md - [examples-repo]: https://github.com/homebridge/HAP-NodeJS-examples [example-accessories]: https://github.com/homebridge/HAP-NodeJS/tree/master/src/accessories - [project-homebridge]: https://github.com/homebridge/homebridge [project-openhab-homekit-bridge]: https://github.com/htreu/OpenHAB-HomeKit-Bridge [project-homekit2mqtt]: https://github.com/hobbyquaker/homekit2mqtt @@ -82,7 +80,6 @@ If you are interested in HAP over BTLE, you might want to check [this][link-hap- [project-node-red-contrib-homekit]: https://github.com/NRCHKB/node-red-contrib-homekit-bridged [project-ioBroker-homekit]: https://github.com/ioBroker/ioBroker.homekit2 [project-accessoryserver]: https://github.com/Appyx/AccessoryServer - [link-alex-skalozub]: https://twitter.com/pieceofsummer [link-homekit-research]: https://gist.github.com/pieceofsummer/13272bf76ac1d6b58a30 [link-apple-dmca]: https://github.com/github/dmca/blob/master/2014/2014-11-04-Apple.md diff --git a/__mocks__/bonjour-hap.ts b/__mocks__/bonjour-hap.ts index f0e33f3f3..ca98ee1c6 100644 --- a/__mocks__/bonjour-hap.ts +++ b/__mocks__/bonjour-hap.ts @@ -1,16 +1,20 @@ +import { vi } from 'vitest' + class Advertisement { - updateTxt = jest.fn(); - stop = jest.fn(); - destroy = jest.fn(); + updateTxt = vi.fn() + stop = vi.fn() + destroy = vi.fn() +} + +function publishFn() { + return new Advertisement() } class BonjourService { - publish = jest.fn(() => { - return new Advertisement(); - }); - destroy = jest.fn(); + publish = vi.fn(publishFn) + destroy = vi.fn() } -export default (opts: any) => { - return new BonjourService(); +export default () => { + return new BonjourService() } diff --git a/__mocks__/node-persist.ts b/__mocks__/node-persist.ts index b5e9456d5..686eeedcf 100644 --- a/__mocks__/node-persist.ts +++ b/__mocks__/node-persist.ts @@ -1,10 +1,14 @@ +import { vi } from 'vitest' + +const createFn = vi.fn().mockImplementation(() => new Storage()) + class Storage { - getItem = jest.fn(); - setItemSync = jest.fn(); - persistSync = jest.fn(); - removeItemSync = jest.fn(); - initSync = jest.fn(); - create = jest.fn().mockImplementation(() => new Storage()); + getItem = vi.fn() + setItemSync = vi.fn() + persistSync = vi.fn() + removeItemSync = vi.fn() + initSync = vi.fn() + create = createFn } -export default new Storage(); +export default new Storage() diff --git a/docs/functions/IsKnownHAPStatusError.html b/docs/functions/IsKnownHAPStatusError.html index b123de706..69133481d 100644 --- a/docs/functions/IsKnownHAPStatusError.html +++ b/docs/functions/IsKnownHAPStatusError.html @@ -1,2 +1,2 @@ -IsKnownHAPStatusError | hap-nodejs

Function IsKnownHAPStatusError

HomeKit Data Streams (HDS)

DataFormatTags DataStreamConnectionEvent DataStreamServerEvent diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..6a1295db9 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,48 @@ +import antfu from '@antfu/eslint-config' + +export default antfu( + { + ignores: ['dist', 'docs'], + jsx: false, + typescript: true, + formatters: { + markdown: true, + }, + rules: { + 'curly': ['error', 'multi-line'], + 'import/extensions': ['error', 'ignorePackages'], + 'import/order': 0, + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-line-alignment': 'error', + 'no-undef': 'error', + 'perfectionist/sort-exports': 'error', + 'perfectionist/sort-imports': [ + 'error', + { + groups: [ + 'builtin-type', + 'external-type', + 'internal-type', + ['parent-type', 'sibling-type', 'index-type'], + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + ], + order: 'asc', + type: 'natural', + }, + ], + 'perfectionist/sort-named-exports': 'error', + 'perfectionist/sort-named-imports': 'error', + 'sort-imports': 0, + 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'style/quote-props': ['error', 'consistent-as-needed'], + 'test/no-only-tests': 'error', + 'unicorn/no-useless-spread': 'error', + 'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }], + }, + }, +) diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index 7fa817d3a..000000000 --- a/jest.config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "preset": "ts-jest", - "testEnvironment": "node", - "coverageReporters": ["lcov"], - "collectCoverageFrom": [ - "src/**", - "!src/accessories/**", - "!src/lib/definitions/generate-definitions.ts", - "!src/lib/definitions/generator-configuration.ts", - "!src/test-utils" - ] -} diff --git a/package-lock.json b/package-lock.json index 75214df48..5dfe5c6cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,34 +15,35 @@ "debug": "^4.3.6", "fast-srp-hap": "^2.0.4", "futoin-hkdf": "^1.5.3", + "long": "^5.2.3", "node-persist": "^0.0.12", "source-map-support": "^0.5.21", - "tslib": "^2.6.3", - "tweetnacl": "^1.0.3" + "tweetnacl": "^1.0.3", + "xml2js": "^0.6.2" }, "devDependencies": { + "@antfu/eslint-config": "^3.0.0", "@types/debug": "^4.1.12", "@types/escape-html": "^1.0.4", - "@types/jest": "^29.5.12", - "@types/node": "^22.5.0", + "@types/node": "^22.5.1", "@types/plist": "^3.0.5", "@types/semver": "^7.3.7", "@types/source-map-support": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^8.2.0", - "@typescript-eslint/parser": "^8.2.0", + "@types/xml2js": "^0.4.14", + "@vitest/coverage-v8": "^2.0.5", "axios": "^1.7.5", "commander": "^12.1.0", "escape-html": "^1.0.3", - "eslint": "^8.57.0", + "eslint": "^9.9.1", + "eslint-plugin-format": "^0.1.2", "http-parser-js": "^0.5.8", - "jest": "^29.7.0", "rimraf": "^6.0.1", "semver": "^7.6.3", "simple-plist": "^1.4.0", - "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typedoc": "^0.26.6", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^2.0.5" }, "engines": { "node": "^18.4.0 || ^20 || ^22" @@ -62,166 +63,146 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", - "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" + "node_modules/@antfu/eslint-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-3.0.0.tgz", + "integrity": "sha512-3HC35LrsW5kvHyVY2U6yat3Uz20/9Re5137LAKqAtl2tKictef2CmdYk5z+qK4UsaY32MMfg98MhuBbvAvZF1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^0.4.1", + "@clack/prompts": "^0.7.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", + "@stylistic/eslint-plugin": "^2.6.5", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitest/eslint-plugin": "^1.1.0", + "eslint-config-flat-gitignore": "^0.3.0", + "eslint-flat-config-utils": "^0.3.1", + "eslint-merge-processors": "^0.1.0", + "eslint-plugin-antfu": "^2.3.6", + "eslint-plugin-command": "^0.2.3", + "eslint-plugin-import-x": "^4.1.0", + "eslint-plugin-jsdoc": "^50.2.2", + "eslint-plugin-jsonc": "^2.16.0", + "eslint-plugin-markdown": "^5.1.0", + "eslint-plugin-n": "^17.10.2", + "eslint-plugin-no-only-tests": "^3.3.0", + "eslint-plugin-perfectionist": "^3.3.0", + "eslint-plugin-regexp": "^2.6.0", + "eslint-plugin-toml": "^0.11.1", + "eslint-plugin-unicorn": "^55.0.0", + "eslint-plugin-unused-imports": "^4.1.3", + "eslint-plugin-vue": "^9.27.0", + "eslint-plugin-yml": "^1.14.0", + "eslint-processor-vue-blocks": "^0.1.2", + "globals": "^15.9.0", + "jsonc-eslint-parser": "^2.4.0", + "local-pkg": "^0.5.0", + "parse-gitignore": "^2.0.0", + "picocolors": "^1.0.1", + "toml-eslint-parser": "^0.10.0", + "vue-eslint-parser": "^9.4.3", + "yaml-eslint-parser": "^1.2.3", + "yargs": "^17.7.2" }, - "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.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.4", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "eslint-config": "bin/index.js" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "funding": { + "url": "https://github.com/sponsors/antfu" }, - "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-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "peerDependencies": { + "@eslint-react/eslint-plugin": "^1.5.8", + "@prettier/plugin-xml": "^3.4.1", + "@unocss/eslint-plugin": ">=0.50.0", + "astro-eslint-parser": "^1.0.2", + "eslint": "^9.5.0", + "eslint-plugin-astro": "^1.2.0", + "eslint-plugin-format": ">=0.1.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "eslint-plugin-solid": "^0.13.2", + "eslint-plugin-svelte": ">=2.35.1", + "prettier-plugin-astro": "^0.13.0", + "prettier-plugin-slidev": "^1.0.5", + "svelte-eslint-parser": ">=0.37.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependenciesMeta": { + "@eslint-react/eslint-plugin": { + "optional": true + }, + "@prettier/plugin-xml": { + "optional": true + }, + "@unocss/eslint-plugin": { + "optional": true + }, + "astro-eslint-parser": { + "optional": true + }, + "eslint-plugin-astro": { + "optional": true + }, + "eslint-plugin-format": { + "optional": true + }, + "eslint-plugin-react-hooks": { + "optional": true + }, + "eslint-plugin-react-refresh": { + "optional": true + }, + "eslint-plugin-solid": { + "optional": true + }, + "eslint-plugin-svelte": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-slidev": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "node_modules/@antfu/install-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.4.1.tgz", + "integrity": "sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "package-manager-detector": "^0.2.0", + "tinyexec": "^0.3.0" }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@babel/helper-simple-access": { + "node_modules/@babel/code-frame": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -247,30 +228,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/highlight": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", @@ -366,13 +323,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.4" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -381,333 +338,564 @@ "node": ">=6.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==", + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.9.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==", + "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/@clack/core": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz", + "integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" } }, - "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==", + "node_modules/@clack/prompts": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz", + "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==", + "bundleDependencies": [ + "is-unicode-supported" + ], "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@clack/core": "^0.3.3", + "is-unicode-supported": "*", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" } }, - "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==", + "node_modules/@clack/prompts/node_modules/is-unicode-supported": { + "version": "1.3.0", "dev": true, + "inBundle": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "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==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "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==", + "node_modules/@dprint/formatter": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@dprint/formatter/-/formatter-0.3.0.tgz", + "integrity": "sha512-N9fxCxbaBOrDkteSOzaCqwWjso5iAe+WJPsHC021JfHNj2ThInPNEF13ORDKta3llq5D1TlclODCvOvipH7bWQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "license": "MIT" }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "node_modules/@dprint/markdown": { + "version": "0.17.8", + "resolved": "https://registry.npmjs.org/@dprint/markdown/-/markdown-0.17.8.tgz", + "integrity": "sha512-ukHFOg+RpG284aPdIg7iPrCYmMs3Dqy43S1ejybnwlJoFiW02b+6Bbr5cfZKFRYNP3dKGM86BqHEnMzBOyLvvA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "license": "MIT" }, - "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==", + "node_modules/@dprint/toml": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@dprint/toml/-/toml-0.6.2.tgz", + "integrity": "sha512-Mk5unEANsL/L+WHYU3NpDXt1ARU5bNU5k5OZELxaJodDycKG6RoRnSlZXpW6+7UN2PSnETAFVUdKrh937ZwtHA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "license": "MIT" }, - "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==", + "node_modules/@es-joy/jsdoccomment": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.43.1.tgz", + "integrity": "sha512-I238eDtOolvCuvtxrnqtlBaw0BwdQuYqK7eA6XIonicMdOOOb75mqdIzkGDUbS04+1Di007rgm9snFRNeVrOog==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@types/eslint": "^8.56.5", + "@types/estree": "^1.0.5", + "@typescript-eslint/types": "^7.2.0", + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=16" } }, - "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==", + "node_modules/@es-joy/jsdoccomment/node_modules/@types/eslint": { + "version": "8.56.12", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", + "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@types/estree": "*", + "@types/json-schema": "*" } }, - "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==", + "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "engines": { + "node": "^18.18.0 || >=20.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", - "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=12" } }, - "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=12" } }, - "node_modules/@babel/traverse/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==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.4.0.tgz", + "integrity": "sha512-yljsWl5Qv3IkIRmJ38h3NrHXFCm4EUl55M8doGTF6hvzvFF8kRpextgSrg2dwHev9lzBZyafCr9RelGIyQm6fw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "escape-string-regexp": "^4.0.0", + "ignore": "^5.2.4" + }, + "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 || ^9.0.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -726,6 +914,19 @@ "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.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", @@ -736,31 +937,32 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/compat": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.1.1.tgz", + "integrity": "sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.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" + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "node_modules/@eslint/config-array/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==", @@ -771,7 +973,7 @@ "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -784,19 +986,90 @@ "node": "*" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@homebridge/ciao": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.1.tgz", + "node_modules/@eslint/eslintrc/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/@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/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/@eslint/js": { + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", + "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/@homebridge/ciao": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.1.tgz", "integrity": "sha512-87tQCBNNnTymlbg8pKlQjRsk7a5uuqhWBpCbUriVYUebz3voJkLbbTmp0TQg7Sa6Jnpk/Uo6LA8zAOy2sbK9bw==", "license": "MIT", "dependencies": { @@ -845,46 +1118,6 @@ "node": ">=0.3.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/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/@humanwhocodes/config-array/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/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -899,13 +1132,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -938,44 +1177,6 @@ "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==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "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", @@ -992,540 +1193,360 @@ "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==", + "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", - "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": ">=8" } }, - "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==", + "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": "ISC", + "license": "MIT", "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" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "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==", + "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", - "dependencies": { - "sprintf-js": "~1.0.2" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "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", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=6.0.0" } }, - "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==", + "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": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "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": { - "p-locate": "^4.1.0" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "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", - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 8" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "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": { - "p-limit": "^2.2.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">=8" + "node": ">=14" } }, - "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==", + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", + "cpu": [ + "arm" + ], "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==", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", + "cpu": [ + "arm64" + ], "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/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", + "cpu": [ + "arm64" + ], "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==", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", + "cpu": [ + "x64" + ], "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==", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", + "cpu": [ + "arm" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", + "cpu": [ + "arm" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", + "cpu": [ + "arm64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", + "cpu": [ + "arm64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", + "cpu": [ + "ppc64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", + "cpu": [ + "riscv64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", + "cpu": [ + "s390x" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", + "cpu": [ + "x64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", + "cpu": [ + "x64" + ], "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==", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", + "cpu": [ + "arm64" + ], "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==", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" - } + "os": [ + "win32" + ] }, "node_modules/@shikijs/core": { "version": "1.14.1", @@ -1537,31 +1558,25 @@ "@types/hast": "^3.0.4" } }, - "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==", + "node_modules/@stylistic/eslint-plugin": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.7.1.tgz", + "integrity": "sha512-JqnHom8CP14oOgPhwTPbn0QgsBJwgNySQSe00V9GQQDlY1tEqZUlK4jM2DIOJ5nE+oXoy51vZWHnHkfZ6rEruw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@types/eslint": "^9.6.1", + "@typescript-eslint/utils": "^8.3.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" } }, "node_modules/@tsconfig/node10": { @@ -1592,51 +1607,6 @@ "dev": true, "license": "MIT" }, - "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/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1654,16 +1624,24 @@ "dev": true, "license": "MIT" }, - "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==", + "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/node": "*" + "@types/estree": "*", + "@types/json-schema": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1674,42 +1652,21 @@ "@types/unist": "*" } }, - "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==", + "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/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.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@types/unist": "^2" } }, "node_modules/@types/ms": { @@ -1720,15 +1677,22 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -1757,49 +1721,35 @@ "source-map": "^0.6.0" } }, - "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/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "dev": true, "license": "MIT" }, - "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==", + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@types/node": "*" } }, - "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.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", - "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/type-utils": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1823,16 +1773,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", - "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { @@ -1852,14 +1802,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", - "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1870,14 +1820,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", - "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1895,9 +1845,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", - "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "license": "MIT", "engines": { @@ -1909,16 +1859,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", - "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -1938,16 +1888,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", - "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1961,13 +1911,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", - "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -1978,12 +1928,232 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "node_modules/@typescript-eslint/visitor-keys/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": "ISC" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.5", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.0.5" + } + }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.0.tgz", + "integrity": "sha512-Ur80Y27Wbw8gFHJ3cv6vypcjXmrx6QHfw+q435h6Q2L+tf+h4Xf5pJTCL4YU/Jps9EVeggQxS85OcUZU7sdXRw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": ">= 8.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz", + "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.38", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz", + "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz", + "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.38", + "@vue/compiler-dom": "3.4.38", + "@vue/compiler-ssr": "3.4.38", + "@vue/shared": "3.4.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.40", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz", + "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz", + "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", @@ -2048,35 +2218,6 @@ "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-escapes/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/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2103,18 +2244,14 @@ "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==", + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=14" } }, "node_modules/arg": { @@ -2153,23 +2290,16 @@ "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", "license": "MIT" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2204,132 +2334,6 @@ "proxy-from-env": "^1.1.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-istanbul/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/babel-plugin-istanbul/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-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-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", @@ -2380,6 +2384,13 @@ "multicast-dns-service-types": "^1.1.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.1.tgz", @@ -2459,35 +2470,35 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "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/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, "engines": { - "node": ">= 6" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "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/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2517,16 +2528,6 @@ "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.30001653", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", @@ -2548,6 +2549,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2565,20 +2583,53 @@ "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==", + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 16" } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", "dev": true, "funding": [ { @@ -2591,12 +2642,28 @@ "node": ">=8" } }, - "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } }, "node_modules/cliui": { "version": "8.0.1", @@ -2613,23 +2680,45 @@ "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==", + "node_modules/cliui/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/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==", "dev": true, "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" + "node": ">=8" } }, - "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==", + "node_modules/cliui/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" + "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/color-convert": { "version": "2.0.1", @@ -2674,6 +2763,16 @@ "node": ">=18" } }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2681,33 +2780,25 @@ "dev": true, "license": "MIT" }, - "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==", + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", "dev": true, "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==", + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "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" + "browserslist": "^4.23.3" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, "node_modules/create-require": { @@ -2732,6 +2823,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -2749,19 +2853,14 @@ } } }, - "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "engines": { + "node": ">=6" } }, "node_modules/deep-equal": { @@ -2803,16 +2902,6 @@ "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/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2857,16 +2946,6 @@ "node": ">=0.4.0" } }, - "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": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2877,29 +2956,6 @@ "node": ">=0.3.1" } }, - "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/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -2938,49 +2994,34 @@ "dev": true, "license": "MIT" }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.13", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "dev": true, - "license": "ISC" + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT" }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" + "engines": { + "node": ">=10.13.0" } }, - "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==", - "dev": true, - "license": "MIT" - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3045,6 +3086,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3076,42 +3163,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.1", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -3125,578 +3208,591 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "url": "https://eslint.org/donate" }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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" + "peerDependencies": { + "jiti": "*" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/eslint/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==", + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/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/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.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" + "semver": "^7.5.4" }, "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" + "node": ">=12" }, - "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/event-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", - "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" + "peerDependencies": { + "eslint": ">=6.0.0" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/eslint-config-flat-gitignore": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-flat-gitignore/-/eslint-config-flat-gitignore-0.3.0.tgz", + "integrity": "sha512-0Ndxo4qGhcewjTzw52TK06Mc00aDtHNTdeeW2JfONgDcLkRO/n/BteMRzNVpLQYxdCC/dFEilfM9fjjpGIJ9Og==", "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" + "@eslint/compat": "^1.1.1", + "find-up-simple": "^1.0.0" }, "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" + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "eslint": "^9.5.0" } }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "node_modules/eslint-flat-config-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/eslint-flat-config-utils/-/eslint-flat-config-utils-0.3.1.tgz", + "integrity": "sha512-eFT3EaoJN1hlN97xw4FIEX//h0TiFUobgl2l5uLkIwhVN9ahGq95Pbs+i1/B5UACA78LO3rco3JzuvxLdTUOPA==", "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" + "@types/eslint": "^9.6.0", + "pathe": "^1.1.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "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-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/eslint-formatting-reporter": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/eslint-formatting-reporter/-/eslint-formatting-reporter-0.0.0.tgz", + "integrity": "sha512-k9RdyTqxqN/wNYVaTk/ds5B5rA8lgoAmvceYN7bcZMBwU7TuXx5ntewJv81eF3pIL/CiJE+pJZm36llG8yhyyw==", "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" + "prettier-linter-helpers": "^1.0.0" }, - "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" + "funding": { + "url": "https://github.com/sponsors/antfu" }, - "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-srp-hap": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", - "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", - "license": "MIT", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.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==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "bser": "2.1.1" + "ms": "^2.1.1" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/eslint-merge-processors": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/eslint-merge-processors/-/eslint-merge-processors-0.1.0.tgz", + "integrity": "sha512-IvRXXtEajLeyssvW4wJcZ2etxkR9mUf4zpNwgI+m/Uac9RfXHskuJefkHUcawVzePnd6xp24enp5jfgdHzjRdQ==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" + "funding": { + "url": "https://github.com/sponsors/antfu" }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "peerDependencies": { + "eslint": "*" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "node_modules/eslint-parser-plain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/eslint-parser-plain/-/eslint-parser-plain-0.1.0.tgz", + "integrity": "sha512-oOeA6FWU0UJT/Rxc3XF5Cq0nbIZbylm7j8+plqq0CZoE6m4u32OXJrR+9iy4srGMmF6v6pmgvP1zPxSRIGh3sg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/eslint-plugin-antfu": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-antfu/-/eslint-plugin-antfu-2.3.6.tgz", + "integrity": "sha512-31VwbU1Yd4BFNUUPQEazKyP79f3c+ohJtq5iZIuw38JjkRQdQAcF/31Kjr0DOKZXVDkeeNPrttKidrr3xhnhOA==", + "dev": true, + "license": "MIT", "dependencies": { - "minimatch": "^5.0.1" + "@antfu/utils": "^0.7.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "eslint": "*" } }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/eslint-plugin-command": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-command/-/eslint-plugin-command-0.2.3.tgz", + "integrity": "sha512-1bBYNfjZg60N2ZpLV5ATYSYyueIJ+zl5yKrTs0UFDdnyu07dNSZ7Xplnc+Wb6SXTdc1sIaoIrnuyhvztcltX6A==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@es-joy/jsdoccomment": "^0.43.0" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "eslint": "*" } }, - "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==", + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" }, "engines": { - "node": ">=8" + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" } }, - "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==", + "node_modules/eslint-plugin-format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-format/-/eslint-plugin-format-0.1.2.tgz", + "integrity": "sha512-ZrcO3aiumgJ6ENAv65IWkPjtW77ML/5mp0YrRK0jdvvaZJb+4kKWbaQTMr/XbJo6CtELRmCApAziEKh7L2NbdQ==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" + "@dprint/formatter": "^0.3.0", + "@dprint/markdown": "^0.17.1", + "@dprint/toml": "^0.6.2", + "eslint-formatting-reporter": "^0.0.0", + "eslint-parser-plain": "^0.1.0", + "prettier": "^3.3.2", + "synckit": "^0.9.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "eslint": "^8.40.0 || ^9.0.0" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "node_modules/eslint-plugin-import-x": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.1.1.tgz", + "integrity": "sha512-dBEM8fACIFNt4H7GoOaRmnH6evJW6JSTJTYYgmRd3vI4geBTjgDM/JyUDKUwIw0HDSyI+u7Vs3vFRXUo/BOAtA==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "@typescript-eslint/typescript-estree": "^8.1.0", + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/eslint-plugin-jsdoc": { + "version": "50.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.2.tgz", + "integrity": "sha512-i0ZMWA199DG7sjxlzXn5AeYZxpRfMJjDPUl7lL9eJJX8TPRoIaxJU4ys/joP5faM5AXE1eqW/dslCj3uj4Nqpg==", "dev": true, - "license": "ISC", + "license": "BSD-3-Clause", "dependencies": { - "glob": "^7.1.3" + "@es-joy/jsdoccomment": "~0.48.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "node_modules/eslint-plugin-jsdoc/node_modules/@es-joy/jsdoccomment": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz", + "integrity": "sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + }, + "engines": { + "node": ">=16" + } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "node_modules/eslint-plugin-jsdoc/node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=12.0.0" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/eslint-plugin-jsonc": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.16.0.tgz", + "integrity": "sha512-Af/ZL5mgfb8FFNleH6KlO4/VdmDuTqmM+SPnWcdoWywTetv7kq+vQe99UyQb9XO3b0OWLVuTH7H0d/PXYCMdSg==", + "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "@eslint-community/eslint-utils": "^4.2.0", + "eslint-compat-utils": "^0.5.0", + "espree": "^9.6.1", + "graphemer": "^1.4.0", + "jsonc-eslint-parser": "^2.0.4", + "natural-compare": "^1.4.0", + "synckit": "^0.6.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" } }, - "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==", + "node_modules/eslint-plugin-jsonc/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": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">=14" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/eslint-plugin-jsonc/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, "engines": { - "node": ">=14" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/eslint" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "node_modules/eslint-plugin-jsonc/node_modules/synckit": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.6.2.tgz", + "integrity": "sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==", "dev": true, "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "tslib": "^2.3.1" }, "engines": { - "node": ">= 6" + "node": ">=12.20" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "license": "MIT" + "node_modules/eslint-plugin-markdown": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-5.1.0.tgz", + "integrity": "sha512-SJeyKko1K6GwI0AN6xeCDToXDkfKZfXcexA6B+O2Wr2btUS9GrC+YgwSyVli5DJnctUHjFXcQ2cqTaAmVoLi2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^0.8.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8" + } }, - "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==", + "node_modules/eslint-plugin-n": { + "version": "17.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.2.tgz", + "integrity": "sha512-e+s4eAf5NtJaxPhTNu3qMO0Iz40WANS93w9LQgYcvuljgvDmWi/a3rh+OrNyMHeng6aOWGJO0rCg5lH4zi8yTw==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "enhanced-resolve": "^5.17.0", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^15.8.0", + "ignore": "^5.2.4", + "minimatch": "^9.0.5", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz", + "integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=5.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==", + "node_modules/eslint-plugin-perfectionist": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-3.3.0.tgz", + "integrity": "sha512-sGgShkEqDBqIZ3WlenGHwLe1cl3vHKTfeh9b1XXAamaxSC7AY4Os0jdNCXnGJW4l0TlpismT5t2r7CXY7sfKlw==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.3.0", + "@typescript-eslint/utils": "^8.3.0", + "minimatch": "^10.0.1", + "natural-compare-lite": "^1.4.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "astro-eslint-parser": "^1.0.2", + "eslint": ">=8.0.0", + "svelte": ">=3.0.0", + "svelte-eslint-parser": "^0.41.0", + "vue-eslint-parser": ">=9.0.0" + }, + "peerDependenciesMeta": { + "astro-eslint-parser": { + "optional": true + }, + "svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + }, + "vue-eslint-parser": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-perfectionist/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/eslint-plugin-regexp": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.6.0.tgz", + "integrity": "sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.9.1", + "comment-parser": "^1.4.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "refa": "^0.12.1", + "regexp-ast-analysis": "^0.7.1", + "scslre": "^0.3.0" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "eslint": ">=8.44.0" } }, - "node_modules/futoin-hkdf": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", - "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", - "license": "Apache-2.0", + "node_modules/eslint-plugin-toml": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-toml/-/eslint-plugin-toml-0.11.1.tgz", + "integrity": "sha512-Y1WuMSzfZpeMIrmlP1nUh3kT8p96mThIq4NnHrYUhg10IKQgGfBZjAWnrg9fBqguiX4iFps/x/3Hb5TxBisfdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "eslint-compat-utils": "^0.5.0", + "lodash": "^4.17.19", + "toml-eslint-parser": "^0.10.0" + }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" } }, - "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==", + "node_modules/eslint-plugin-unicorn": { + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "globals": "^15.7.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.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==", + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", + "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/eslint-plugin-vue": { + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz", + "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.17.0 || >=16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "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==", + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=8.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/eslint-plugin-vue/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -3704,42 +3800,74 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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", + "node_modules/eslint-plugin-yml": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.14.0.tgz", + "integrity": "sha512-ESUpgYPOcAYQO9czugcX5OqRvn/ydDVwGCPXY4YjPqc09rHaUVUA6IE6HLQys4rXk/S+qx3EwTd1wHCwam/OWQ==", "dev": true, - "license": "ISC", + "license": "MIT", "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" + "debug": "^4.3.2", + "eslint-compat-utils": "^0.5.0", + "lodash": "^4.17.21", + "natural-compare": "^1.4.0", + "yaml-eslint-parser": "^1.2.1" }, "engines": { - "node": "*" + "node": "^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" } }, - "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==", + "node_modules/eslint-processor-vue-blocks": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-processor-vue-blocks/-/eslint-processor-vue-blocks-0.1.2.tgz", + "integrity": "sha512-PfpJ4uKHnqeL/fXUnzYkOax3aIenlwewXRX8jFinA1a2yCFnLgMuiH3xvCgvHHUlV2xJWQHbCTdiJWGwb3NqpQ==", "dev": true, - "license": "ISC", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/compiler-sfc": "^3.3.0", + "eslint": "^8.50.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "is-glob": "^4.0.3" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10.13.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/glob/node_modules/brace-expansion": { + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "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/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -3750,7 +3878,7 @@ "concat-map": "0.0.1" } }, - "node_modules/glob/node_modules/minimatch": { + "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", @@ -3763,435 +3891,412 @@ "node": "*" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "type-fest": "^0.20.2" + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "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": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", + "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": { - "get-intrinsic": "^1.1.3" + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4.0" } }, - "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==", + "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": "ISC" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "peer": true }, - "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==", + "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": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=16.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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": { - "function-bind": "^1.1.2" + "@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": ">= 0.4" + "node": ">=8.6.0" } }, - "node_modules/hexy": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz", - "integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==", - "license": "MIT", - "bin": { - "hexy": "bin/hexy_cmd.js" + "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": ">=10.4" + "node": ">= 6" } }, - "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==", + "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/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "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/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", + "node_modules/fast-srp-hap": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", + "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", + "license": "MIT", "engines": { "node": ">=10.17.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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/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": ">= 4" + "node": ">=16.0.0" } }, - "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==", + "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": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "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==", + "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": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "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==", + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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.", + "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": "ISC", + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true, "license": "ISC" }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, "engines": { - "node": ">= 0.4" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "is-callable": "^1.1.3" } }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "license": "MIT", + "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==", + "dev": true, + "license": "ISC", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "license": "MIT" + }, + "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": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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, + "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==", "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "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", + "node_modules/futoin-hkdf": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "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==", + "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": "MIT", + "license": "ISC", "engines": { - "node": ">=8" + "node": "6.* || 8.* || >= 10.*" } }, - "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==", + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": "*" } }, - "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, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4199,105 +4304,160 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "node_modules/get-tsconfig": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", + "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", + "dev": true, "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "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": ">= 0.4" + "node": "20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "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": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "license": "MIT", + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": "20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" + "get-intrinsic": "^1.1.3" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "es-define-property": "^1.0.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4305,14 +4465,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4320,11 +4477,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -4332,760 +4492,574 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" + } + }, + "node_modules/hexy": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz", + "integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==", + "license": "MIT", + "bin": { + "hexy": "bin/hexy_cmd.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=10.4" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "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/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "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==", + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">=16.17.0" } }, - "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==", + "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": "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" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 4" } }, - "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==", + "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": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=0.8.19" } }, - "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==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" }, "engines": { - "node": "20 || >=22" - }, + "node": ">= 0.4" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/jake/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, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jake/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", + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" }, "engines": { - "node": "*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "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-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "has-bigints": "^1.0.1" }, - "bin": { - "jest": "bin/jest.js" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.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" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" + "builtin-modules": "^3.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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": { - "@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" + "hasown": "^2.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "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" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "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" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "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==", + "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", - "dependencies": { - "detect-newline": "^3.0.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.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==", + "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==", "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": ">=8" } }, - "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==", + "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": { - "@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" + "is-extglob": "^2.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.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==", + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "dev": true, "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "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, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "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" + "node": ">= 0.4" }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "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", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.12.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, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "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": ">=8" } }, - "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, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "license": "MIT", "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "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": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "license": "MIT", "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "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" + "has-symbols": "^1.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "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" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "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" - } + "license": "ISC" }, - "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==", + "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": "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" - }, + "license": "BSD-3-Clause", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "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==", + "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": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "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==", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "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" + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" } }, - "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==", + "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": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "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==", + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "has-flag": "^4.0.0" + "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=10" + "node": "20 || >=22" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/js-tokens": { @@ -5108,17 +5082,27 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "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": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -5149,47 +5133,64 @@ "dev": true, "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==", + "node_modules/jsonc-eslint-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz", + "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==", "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" }, "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" } }, - "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==", + "node_modules/jsonc-eslint-parser/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": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "node_modules/jsonc-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, "engines": { - "node": ">=6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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==", + "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", - "engines": { - "node": ">=6" + "dependencies": { + "json-buffer": "3.0.1" } }, "node_modules/levn": { @@ -5223,6 +5224,23 @@ "uc.micro": "^2.0.0" } }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5239,10 +5257,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, "license": "MIT" }, @@ -5253,14 +5271,30 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "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==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", "dev": true, "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "engines": { + "node": "20 || >=22" } }, "node_modules/lunr": { @@ -5270,6 +5304,28 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5293,16 +5349,6 @@ "dev": true, "license": "ISC" }, - "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/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -5327,6 +5373,35 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -5351,6 +5426,27 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5365,6 +5461,19 @@ "node": ">=8.6" } }, + "node_modules/micromatch/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/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5389,13 +5498,26 @@ } }, "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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/minimatch": { @@ -5445,6 +5567,19 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5470,6 +5605,25 @@ "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5477,10 +5631,10 @@ "dev": true, "license": "MIT" }, - "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==", + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true, "license": "MIT" }, @@ -5501,27 +5655,69 @@ "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==", + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" } }, "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==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.0.0" + "path-key": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, "node_modules/object-inspect": { @@ -5579,27 +5775,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-fn": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5672,6 +5858,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz", + "integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5679,10 +5872,53 @@ "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-gitignore": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz", + "integrity": "sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/parse-imports": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.1.1.tgz", + "integrity": "sha512-TDT4HqzUiTMO1wJRwg/t/hYk8Wdp3iF/ToMIlAoVQfL1Xs/sTxq1dKWSMjMbQmIarfWKymOyly40+zmPHXMqCA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" }, "engines": { - "node": ">=6" + "node": ">= 18" } }, "node_modules/parse-json": { @@ -5714,16 +5950,6 @@ "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", @@ -5758,24 +5984,21 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", - "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } + "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 14.16" } }, "node_modules/pause-stream": { @@ -5798,119 +6021,105 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "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==", + "node_modules/pkg-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", + "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" } }, - "node_modules/pkg-dir/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==", + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=8" + "node": ">=10.4.0" } }, - "node_modules/pkg-dir/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==", + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/pkg-dir/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, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/pkg-dir/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==", + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node": ">=4" } }, "node_modules/prelude-ls": { @@ -5923,46 +6132,33 @@ "node": ">= 0.8.0" } }, - "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==", + "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", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "bin": { + "prettier": "bin/prettier.cjs" }, "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" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "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==", + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, "license": "MIT", "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" + "fast-diff": "^1.1.2" }, "engines": { - "node": ">= 6" + "node": ">=6.0.0" } }, "node_modules/proxy-from-env": { @@ -5992,23 +6188,6 @@ "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/q": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz", @@ -6041,12 +6220,142 @@ ], "license": "MIT" }, - "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==", + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/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/read-pkg-up/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/read-pkg-up/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/read-pkg-up/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/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -6062,8 +6371,30 @@ "engines": { "node": ">= 0.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" } }, "node_modules/require-directory": { @@ -6094,29 +6425,6 @@ "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-cwd/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-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6127,14 +6435,14 @@ "node": ">=4" } }, - "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==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/reusify": { @@ -6168,44 +6476,40 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/rollup": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, - "license": "ISC", + "license": "MIT", "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" + "@types/estree": "1.0.5" }, "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==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "20 || >=22" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", + "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { @@ -6258,6 +6562,21 @@ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -6355,13 +6674,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-plist": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.4.0.tgz", @@ -6381,15 +6713,12 @@ "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==", + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/source-map": { "version": "0.6.1", @@ -6400,6 +6729,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "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", @@ -6410,6 +6749,53 @@ "source-map": "^0.6.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -6422,35 +6808,26 @@ "node": "*" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT" }, - "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==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "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==", + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", @@ -6484,21 +6861,26 @@ "through": "~2.3.4" } }, - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width": { + "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==", @@ -6513,20 +6895,40 @@ "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==", + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/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==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi": { @@ -6556,24 +6958,30 @@ "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==", + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -6615,43 +7023,107 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "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==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/test-exclude/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==", + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/test-exclude/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/test-exclude/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -6673,12 +7145,49 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -6703,66 +7212,46 @@ "node": ">=8.0" } }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "node_modules/toml-eslint-parser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.10.0.tgz", + "integrity": "sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==", "dev": true, "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0" + }, "engines": { - "node": ">=16" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "typescript": ">=4.2.0" + "funding": { + "url": "https://github.com/sponsors/ota-meshi" } }, - "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "node_modules/toml-eslint-parser/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": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.6.3", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + "node": ">=16" }, "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } + "typescript": ">=4.2.0" } }, "node_modules/ts-node": { @@ -6834,27 +7323,14 @@ "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.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/typedoc": { @@ -6871,114 +7347,360 @@ "yaml": "^2.4.5" }, "bin": { - "typedoc": "bin/typedoc" + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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.1.2", + "picocolors": "^1.0.1" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", + "execa": "^8.0.1", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">= 18" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x" - } - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", + "happy-dom": "*", + "jsdom": "*" }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "@types/node": { + "optional": true }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true } - ], + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" }, - "bin": { - "update-browserslist-db": "cli.js" + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "eslint": ">=6.0.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==", + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "punycode": "^2.1.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "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==", + "node_modules/vue-eslint-parser/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": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10.12.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "Apache-2.0", + "license": "BSD-2-Clause", "dependencies": { - "makeerror": "1.0.12" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/which": { @@ -7050,6 +7772,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7061,18 +7800,18 @@ } }, "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==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7097,25 +7836,78 @@ "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==", + "node_modules/wrap-ansi-cjs/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==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "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==", + "node_modules/wrap-ansi-cjs/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==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=12" } }, "node_modules/xml2js": { @@ -7160,13 +7952,6 @@ "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/yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", @@ -7180,6 +7965,37 @@ "node": ">= 14" } }, + "node_modules/yaml-eslint-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.2.3.tgz", + "integrity": "sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "lodash": "^4.17.21", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/yaml-eslint-parser/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/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -7209,6 +8025,28 @@ "node": ">=12" } }, + "node_modules/yargs/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/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==", + "dev": true, + "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/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 387879c56..a34a8ce8c 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,17 @@ { "name": "hap-nodejs", + "type": "module", "version": "1.1.0", "description": "HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "maintainers": [ - "Andreas Bauer " - ], "author": "Khaos Tian (https://tz.is/)", - "homepage": "https://github.com/homebridge/HAP-NodeJS", "license": "Apache-2.0", - "scripts": { - "check": "npm install && npm outdated", - "clean": "rimraf dist && rimraf coverage", - "lint": "eslint 'src/**/*.{js,ts,json}'", - "build": "rimraf dist && tsc && node .github/node-persist-ignore.js", - "prepublishOnly": "npm run build", - "postpublish": "npm run clean", - "test": "jest", - "test-coverage": "jest --coverage", - "start": "node dist/BridgedCore.js", - "docs": "typedoc", - "lint-docs": "typedoc --emit none --treatWarningsAsErrors" + "homepage": "https://github.com/homebridge/HAP-NodeJS", + "repository": { + "type": "git", + "url": "git+https://github.com/homebridge/HAP-NodeJS.git" + }, + "bugs": { + "url": "https://github.com/homebridge/HAP-NodeJS/issues" }, "keywords": [ "hap-nodejs", @@ -36,22 +26,34 @@ "homekit-support", "siri" ], - "repository": { - "type": "git", - "url": "git+https://github.com/homebridge/HAP-NodeJS.git" - }, - "bugs": { - "url": "https://github.com/homebridge/HAP-NodeJS/issues" - }, - "engines": { - "node": "^18.4.0 || ^20 || ^22" - }, + "exports": "./dist/index.js", + "types": "dist/index.d.ts", + "maintainers": [ + "Andreas Bauer " + ], "files": [ - "README.md", + "@types", "LICENSE", - "dist", - "@types" + "README.md", + "dist" ], + "engines": { + "node": "^18.4.0 || ^20 || ^22" + }, + "scripts": { + "build": "rimraf dist && tsc && node .github/node-persist-ignore.js", + "check": "npm install && npm outdated", + "clean": "rimraf dist && rimraf coverage", + "docs": "typedoc", + "generate-types": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/lib/definitions/generate-definitions.ts", + "lint": "eslint .", + "lint:fix": "npm run lint -- --fix", + "lint-docs": "typedoc --emit none --treatWarningsAsErrors", + "prepublishOnly": "npm run build", + "postpublish": "npm run clean", + "test": "vitest run", + "test-coverage": "npm run test -- --coverage" + }, "dependencies": { "@homebridge/ciao": "^1.3.1", "@homebridge/dbus-native": "^0.6.0", @@ -59,33 +61,34 @@ "debug": "^4.3.6", "fast-srp-hap": "^2.0.4", "futoin-hkdf": "^1.5.3", + "long": "^5.2.3", "node-persist": "^0.0.12", "source-map-support": "^0.5.21", - "tslib": "^2.6.3", - "tweetnacl": "^1.0.3" + "tweetnacl": "^1.0.3", + "xml2js": "^0.6.2" }, "devDependencies": { + "@antfu/eslint-config": "^3.0.0", "@types/debug": "^4.1.12", "@types/escape-html": "^1.0.4", - "@types/jest": "^29.5.12", - "@types/node": "^22.5.0", + "@types/node": "^22.5.1", "@types/plist": "^3.0.5", "@types/semver": "^7.3.7", "@types/source-map-support": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^8.2.0", - "@typescript-eslint/parser": "^8.2.0", + "@types/xml2js": "^0.4.14", + "@vitest/coverage-v8": "^2.0.5", "axios": "^1.7.5", "commander": "^12.1.0", "escape-html": "^1.0.3", - "eslint": "^8.57.0", + "eslint": "^9.9.1", + "eslint-plugin-format": "^0.1.2", "http-parser-js": "^0.5.8", - "jest": "^29.7.0", "rimraf": "^6.0.1", "semver": "^7.6.3", "simple-plist": "^1.4.0", - "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typedoc": "^0.26.6", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^2.0.5" } } diff --git a/src/accessories/AirConditioner_accessory.ts b/src/accessories/AirConditioner_accessory.ts index 6ccb53c7c..fb6c139f1 100644 --- a/src/accessories/AirConditioner_accessory.ts +++ b/src/accessories/AirConditioner_accessory.ts @@ -1,22 +1,22 @@ +/* eslint-disable no-console */ // In This example we create an air conditioner Accessory that Has a Thermostat linked to a Fan Service. // For example, I've also put a Light Service that should be hidden to represent a light in the closet that is part of the AC. // It is to show how to hide services. // The linking and Hiding does NOT appear to be reflected in Home // here's a fake hardware device that we'll expose to HomeKit +import type { CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue } from '../index.js' +import type { VoidCallback } from '../types' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicGetCallback, - CharacteristicSetCallback, - CharacteristicValue, Service, uuid, -} from ".."; -import { VoidCallback } from "../types"; +} from '../index.js' const ACTest_data: Record = { fanPowerOn: false, @@ -27,133 +27,130 @@ const ACTest_data: Record = { TargetTemperature: 32, TemperatureDisplayUnits: 1, LightOn: false, -}; +} // This is the Accessory that we'll return to HAP-NodeJS that represents our fake fan. -const ACTest = exports.accessory = new Accessory("Air Conditioner", uuid.generate("hap-nodejs:accessories:airconditioner")); +const ACTest = exports.accessory = new Accessory('Air Conditioner', uuid.generate('hap-nodejs:accessories:airconditioner')) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -ACTest.username = "1A:2B:3C:4D:5E:FF"; +ACTest.username = '1A:2B:3C:4D:5E:FF' // @ts-expect-error: Core/BridgeCore API -ACTest.pincode = "031-45-154"; -ACTest.category = Categories.THERMOSTAT; +ACTest.pincode = '031-45-154' +ACTest.category = Categories.THERMOSTAT // set some basic properties (these values are arbitrary and setting them is optional) ACTest .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Sample Company"); + .setCharacteristic(Characteristic.Manufacturer, 'Sample Company') // listen for the "identify" event for this Accessory ACTest.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - console.log("Fan Identified!"); - callback(); // success -}); + console.log('Fan Identified!') + callback() // success +}) // Add the actual Fan Service and listen for change events from iOS. -const FanService = ACTest.addService(Service.Fan, "Blower"); // services exposed to the user should have "names" like "Fake Light" for us +const FanService = ACTest.addService(Service.Fan, 'Blower') // services exposed to the user should have "names" like "Fake Light" for us FanService.getCharacteristic(Characteristic.On)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("Fan Power Changed To "+value); - ACTest_data.fanPowerOn=value; - callback(); // Our fake Fan is synchronous - this value has been successfully set - }); + console.log(`Fan Power Changed To ${value}`) + ACTest_data.fanPowerOn = value + callback() // Our fake Fan is synchronous - this value has been successfully set + }) // We want to intercept requests for our current power state so we can query the hardware itself instead of // allowing HAP-NodeJS to return the cached Characteristic.value. FanService.getCharacteristic(Characteristic.On)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - // this event is emitted when you ask Siri directly whether your fan is on or not. you might query // the fan hardware itself to find this out, then call the callback. But if you take longer than a // few seconds to respond, Siri will give up. - const err = null; // in case there were any problems + const err = null // in case there were any problems if (ACTest_data.fanPowerOn) { - callback(err, true); + callback(err, true) } else { - callback(err, false); + callback(err, false) } - }); - + }) // also add an "optional" Characteristic for speed FanService.addCharacteristic(Characteristic.RotationSpeed) .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.rSpeed); + callback(null, ACTest_data.rSpeed) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("Setting fan rSpeed to %s", value); - ACTest_data.rSpeed=value; - callback(); - }); + console.log('Setting fan rSpeed to %s', value) + ACTest_data.rSpeed = value + callback() + }) -const ThermostatService = ACTest.addService(Service.Thermostat, "Thermostat"); -ThermostatService.addLinkedService(FanService); -ThermostatService.setPrimaryService(); +const ThermostatService = ACTest.addService(Service.Thermostat, 'Thermostat') +ThermostatService.addLinkedService(FanService) +ThermostatService.setPrimaryService() ThermostatService.getCharacteristic(Characteristic.CurrentHeatingCoolingState)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.CurrentHeatingCoolingState); - }) - .on(CharacteristicEventTypes.SET,(value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.CurrentHeatingCoolingState=value; - console.log( "Characteristic CurrentHeatingCoolingState changed to %s",value); - callback(); - }); - - ThermostatService.getCharacteristic(Characteristic.TargetHeatingCoolingState)! - .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.TargetHeatingCoolingState); - }) - .on(CharacteristicEventTypes.SET,(value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.TargetHeatingCoolingState=value; - console.log( "Characteristic TargetHeatingCoolingState changed to %s",value); - callback(); - }); - - ThermostatService.getCharacteristic(Characteristic.CurrentTemperature)! - .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.CurrentTemperature); - }) - .on(CharacteristicEventTypes.SET,(value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.CurrentTemperature=value; - console.log( "Characteristic CurrentTemperature changed to %s",value); - callback(); - }); - - ThermostatService.getCharacteristic(Characteristic.TargetTemperature)! - .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.TargetTemperature); - }) - .on(CharacteristicEventTypes.SET,(value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.TargetTemperature=value; - console.log( "Characteristic TargetTemperature changed to %s",value); - callback(); - }); - - ThermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)! - .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.TemperatureDisplayUnits); - }) - .on(CharacteristicEventTypes.SET,(value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.TemperatureDisplayUnits=value; - console.log( "Characteristic TemperatureDisplayUnits changed to %s",value); - callback(); - }); - - -const LightService = ACTest.addService(Service.Lightbulb, "AC Light"); + callback(null, ACTest_data.CurrentHeatingCoolingState) + }) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + ACTest_data.CurrentHeatingCoolingState = value + console.log('Characteristic CurrentHeatingCoolingState changed to %s', value) + callback() + }) + +ThermostatService.getCharacteristic(Characteristic.TargetHeatingCoolingState)! + .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { + callback(null, ACTest_data.TargetHeatingCoolingState) + }) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + ACTest_data.TargetHeatingCoolingState = value + console.log('Characteristic TargetHeatingCoolingState changed to %s', value) + callback() + }) + +ThermostatService.getCharacteristic(Characteristic.CurrentTemperature)! + .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { + callback(null, ACTest_data.CurrentTemperature) + }) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + ACTest_data.CurrentTemperature = value + console.log('Characteristic CurrentTemperature changed to %s', value) + callback() + }) + +ThermostatService.getCharacteristic(Characteristic.TargetTemperature)! + .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { + callback(null, ACTest_data.TargetTemperature) + }) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + ACTest_data.TargetTemperature = value + console.log('Characteristic TargetTemperature changed to %s', value) + callback() + }) + +ThermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)! + .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { + callback(null, ACTest_data.TemperatureDisplayUnits) + }) + .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { + ACTest_data.TemperatureDisplayUnits = value + console.log('Characteristic TemperatureDisplayUnits changed to %s', value) + callback() + }) + +const LightService = ACTest.addService(Service.Lightbulb, 'AC Light') LightService.getCharacteristic(Characteristic.On)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(null, ACTest_data.LightOn); + callback(null, ACTest_data.LightOn) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - ACTest_data.LightOn=value; - console.log( "Characteristic Light On changed to %s",value); - callback(); - }); -LightService.setHiddenService(); + ACTest_data.LightOn = value + console.log('Characteristic Light On changed to %s', value) + callback() + }) +LightService.setHiddenService() diff --git a/src/accessories/AppleTVRemote_accessory.ts b/src/accessories/AppleTVRemote_accessory.ts index 2014ae4b4..ac174a908 100644 --- a/src/accessories/AppleTVRemote_accessory.ts +++ b/src/accessories/AppleTVRemote_accessory.ts @@ -1,167 +1,162 @@ -import escapeHTML from "escape-html"; -import { Accessory, ButtonState, ButtonType, Categories, RemoteController, uuid } from ".."; -import * as http from "http"; -import url, { UrlWithParsedQuery } from "url"; -import { GStreamerAudioProducer, GStreamerOptions } from "./gstreamer-audioProducer"; +import type { GStreamerOptions } from './gstreamer-audioProducer' -const remoteUUID = uuid.generate("hap-nodejs:accessories:remote"); -const remote = exports.accessory = new Accessory("Remote", remoteUUID); +import { createServer } from 'node:http' + +import escapeHTML from 'escape-html' + +import { Accessory, ButtonState, ButtonType, Categories, RemoteController, uuid } from '../index.js' +import { GStreamerAudioProducer } from './gstreamer-audioProducer.js' + +const remoteUUID = uuid.generate('hap-nodejs:accessories:remote') +const remote = exports.accessory = new Accessory('Remote', remoteUUID) // @ts-expect-error: Core/BridgeCore API -remote.username = "DB:AF:E0:5C:69:76"; +remote.username = 'DB:AF:E0:5C:69:76' // @ts-expect-error: Core/BridgeCore API -remote.pincode = "874-23-897"; -remote.category = Categories.TARGET_CONTROLLER; +remote.pincode = '874-23-897' +remote.category = Categories.TARGET_CONTROLLER // ----------------- for siri support ----------------- // CHANGE this to enable siri support. Read docs in 'gstreamer-audioProducer.ts' for necessary package dependencies -const siriSupport = false; +const siriSupport = false const gstreamerOptions: Partial = { // any configuration regarding the producer can be made here -}; +} // ---------------------------------------------------- const controller = siriSupport ? new RemoteController(GStreamerAudioProducer, gstreamerOptions) - : new RemoteController(); -remote.configureController(controller); + : new RemoteController() +remote.configureController(controller) /* - This example plugin exposes an simple http api to interact with the remote and play around. + This example plugin exposes a simple http api to interact with the remote and play around. The supported routes are listed below. The http server runs on port 8080 as default. This example should not be used except for testing as the http server is unsecured. /listTargets - list all currently configured apple tvs and their respective configuration /getActiveTarget - return the current target id of the controlled device - /getActive - get the value of the active characteristic (active means the apple tv for the activeTarget is listening) + /getActive - get the value of the active characteristic (active means the Apple TV for the activeTarget is listening) /press?button=&time= - presses a given button for a given time. Time is optional and defaults to 200ms /button?button=&state= - send a single button event - /getTargetId?name= - get the target identifier for the given name of the apple tv - /setActiveTarget?identifier= - set currently controlled apple tv + /getTargetId?name= - get the target identifier for the given name of the Apple TV + /setActiveTarget?identifier= - set currently controlled Apple TV */ -http.createServer((request, response) => { - if (request.method !== "GET") { - response.writeHead(405, { "Content-Type": "text/html" }); - response.end("Method Not Allowed"); - return; +createServer((request, response) => { + if (request.method !== 'GET') { + response.writeHead(405, { 'Content-Type': 'text/html' }) + response.end('Method Not Allowed') + return } - const parsedPath: UrlWithParsedQuery = url.parse(request.url!, true); - const pathname = parsedPath.pathname!.substring(1, parsedPath.pathname!.length); - const query = parsedPath.query; + const parsedUrl = new URL(request.url!, `http://${request.headers.host}`) + const pathname = parsedUrl.pathname.substring(1) + const query = Object.fromEntries(parsedUrl.searchParams.entries()) - if (pathname === "setActiveTarget") { + if (pathname === 'setActiveTarget') { if (query === undefined || query.identifier === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Must include 'identifier' in query string!"); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end('Bad request. Must include \'identifier\' in query string!') + return } - const targetIdentifier = parseInt(query.identifier as string, 10); + const targetIdentifier = Number.parseInt(query.identifier as string, 10) if (!controller.isConfigured(targetIdentifier)) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. No target found for given identifier " + targetIdentifier); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end(`Bad request. No target found for given identifier ${targetIdentifier}`) + return } - controller.setActiveIdentifier(targetIdentifier); - response.writeHead(200, { "Content-Type": "text/html" }); - response.end("OK"); - return; - } else if (pathname === "getActiveTarget") { - response.writeHead(200, { "Content-Type": "text/html" }); - response.end(controller.activeIdentifier + ""); - return; - } else if (pathname === "getTargetId") { + controller.setActiveIdentifier(targetIdentifier) + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end('OK') + } else if (pathname === 'getActiveTarget') { + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end(`${controller.activeIdentifier}`) + } else if (pathname === 'getTargetId') { if (query === undefined || query.name === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Must include 'name' in query string!"); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end('Bad request. Must include \'name\' in query string!') + return } - const targetIdentifier = controller.getTargetIdentifierByName(query.name as string); + const targetIdentifier = controller.getTargetIdentifierByName(query.name as string) if (targetIdentifier === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. No target found for given name " + escapeHTML(query.name + "")); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end(`Bad request. No target found for given name ${escapeHTML(`${query.name}`)}`) + return } - response.writeHead(200, { "Content-Type": "text/html" }); - response.end("" + targetIdentifier); - return; - } else if (pathname === "button") { + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end(`${targetIdentifier}`) + } else if (pathname === 'button') { if (query === undefined || query.state === undefined || query.button === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Must include 'state' and 'button' in query string!"); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end('Bad request. Must include \'state\' and \'button\' in query string!') + return } - const buttonState = parseInt(query.state as string, 10); - const button = parseInt(query.button as string, 10); + const buttonState = Number.parseInt(query.state as string, 10) + const button = Number.parseInt(query.button as string, 10) // @ts-expect-error: forceConsistentCasingInFileNames compiler option if (ButtonState[buttonState] === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Unknown button state " + escapeHTML(query.state + "")); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end(`Bad request. Unknown button state ${escapeHTML(`${query.state}`)}`) + return } // @ts-expect-error: forceConsistentCasingInFileNames compiler option if (ButtonType[button] === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Unknown button " + escapeHTML(query.button + "")); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end(`Bad request. Unknown button ${escapeHTML(`${query.button}`)}`) + return } if (buttonState === ButtonState.UP) { - controller.releaseButton(button); + controller.releaseButton(button) } else if (buttonState === ButtonState.DOWN) { - controller.pushButton(button); + controller.pushButton(button) } - response.writeHead(200, { "Content-Type": "text/html" }); - response.end("OK"); - return; - } else if (pathname === "press") { + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end('OK') + } else if (pathname === 'press') { if (query === undefined || query.button === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Must include 'button' in query string!"); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end('Bad request. Must include \'button\' in query string!') + return } - let time = 200; + let time = 200 if (query.time !== undefined) { - const parsedTime = parseInt(query.time as string, 10); + const parsedTime = Number.parseInt(query.time as string, 10) if (parsedTime) { - time = parsedTime; + time = parsedTime } } - const button = parseInt(query.button as string, 10); + const button = Number.parseInt(query.button as string, 10) // @ts-expect-error: forceConsistentCasingInFileNames compiler option if (ButtonType[button] === undefined) { - response.writeHead(400, { "Content-Type": "text/html" }); - response.end("Bad request. Unknown button " + escapeHTML(query.button + "")); - return; + response.writeHead(400, { 'Content-Type': 'text/html' }) + response.end(`Bad request. Unknown button ${escapeHTML(`${query.button}`)}`) + return } - controller.pushAndReleaseButton(button, time); - - response.writeHead(200, { "Content-Type": "text/html" }); - response.end("OK"); - return; - } else if (pathname === "listTargets") { - const targets = controller.targetConfigurations; - - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify(targets, undefined, 4)); - return; - } else if (pathname === "getActive") { - response.writeHead(200, { "Content-Type": "text/html" }); - response.end(controller.isActive()? "true": "false"); - return; + controller.pushAndReleaseButton(button, time) + + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end('OK') + } else if (pathname === 'listTargets') { + const targets = controller.targetConfigurations + + response.writeHead(200, { 'Content-Type': 'application/json' }) + response.end(JSON.stringify(targets, undefined, 4)) + } else if (pathname === 'getActive') { + response.writeHead(200, { 'Content-Type': 'text/html' }) + response.end(controller.isActive() ? 'true' : 'false') } else { - response.writeHead(404, { "Content-Type": "text/html" }); - response.end("Not Found. No path found for " + escapeHTML(pathname)); - return; + response.writeHead(404, { 'Content-Type': 'text/html' }) + response.end(`Not Found. No path found for ${escapeHTML(pathname)}`) } -}).listen(8080); +}).listen(8080) diff --git a/src/accessories/Camera_accessory.ts b/src/accessories/Camera_accessory.ts index 369dc1dab..898c2411f 100644 --- a/src/accessories/Camera_accessory.ts +++ b/src/accessories/Camera_accessory.ts @@ -1,63 +1,73 @@ -import assert from "assert"; -import { ChildProcess, spawn } from "child_process"; -import { once } from "events"; -import { AddressInfo, createServer, Server, Socket } from "net"; +/* eslint-disable no-console */ +import type { ChildProcess } from 'node:child_process' +import type { AddressInfo, Server, Socket } from 'node:net' + +import type { + CameraRecordingConfiguration, + CameraRecordingDelegate, + CameraStreamingDelegate, + HDSProtocolSpecificErrorReason, + PrepareStreamCallback, + PrepareStreamRequest, + PrepareStreamResponse, + RecordingPacket, + SnapshotRequest, + SnapshotRequestCallback, + StreamingRequest, + StreamRequestCallback, + StreamSessionIdentifier, + VideoInfo, +} from '../index.js' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { createServer } from 'node:net' +import process from 'node:process' + import { Accessory, AudioBitrate, AudioRecordingCodecType, AudioRecordingSamplerate, CameraController, - CameraRecordingConfiguration, - CameraRecordingDelegate, - CameraStreamingDelegate, Categories, Characteristic, H264Level, H264Profile, - HDSProtocolSpecificErrorReason, MediaContainerType, - PrepareStreamCallback, - PrepareStreamRequest, - PrepareStreamResponse, - RecordingPacket, Service, - SnapshotRequest, - SnapshotRequestCallback, SRTPCryptoSuites, - StreamingRequest, - StreamRequestCallback, StreamRequestTypes, - StreamSessionIdentifier, uuid, VideoCodecType, - VideoInfo, -} from ".."; +} from '../index.js' interface MP4Atom { - header: Buffer; - length: number; - type: string; - data: Buffer; + header: Buffer + length: number + type: string + data: Buffer } -const cameraUUID = uuid.generate("hap-nodejs:accessories:ip-camera"); -const camera = exports.accessory = new Accessory("IPCamera", cameraUUID); +const cameraUUID = uuid.generate('hap-nodejs:accessories:ip-camera') +const camera = exports.accessory = new Accessory('IPCamera', cameraUUID) // @ts-expect-error: Core/BridgeCore API -camera.username = "9F:B2:46:0C:40:DB"; +camera.username = '9F:B2:46:0C:40:DB' // @ts-expect-error: Core/BridgeCore API -camera.pincode = "948-23-459"; -camera.category = Categories.IP_CAMERA; +camera.pincode = '948-23-459' +camera.category = Categories.IP_CAMERA -type SessionInfo = { - address: string, // address of the HAP controller +interface SessionInfo { + address: string // address of the HAP controller - videoPort: number, // port of the controller - localVideoPort: number, - videoCryptoSuite: SRTPCryptoSuites, // should be saved if multiple suites are supported - videoSRTP: Buffer, // key and salt concatenated - videoSSRC: number, // rtp synchronisation source + videoPort: number // port of the controller + localVideoPort: number + videoCryptoSuite: SRTPCryptoSuites // should be saved if multiple suites are supported + videoSRTP: Buffer // key and salt concatenated + videoSSRC: number // rtp synchronisation source /* Won't be saved as audio is not supported by this example audioPort: number, @@ -67,98 +77,98 @@ type SessionInfo = { */ } -type OngoingSession = { - localVideoPort: number, - process: ChildProcess, +interface OngoingSession { + localVideoPort: number + process: ChildProcess } const FFMPEGH264ProfileNames = [ - "baseline", - "main", - "high", -]; + 'baseline', + 'main', + 'high', +] const FFMPEGH264LevelNames = [ - "3.1", - "3.2", - "4.0", -]; + '3.1', + '3.2', + '4.0', +] -const ports = new Set(); +const ports = new Set() function getPort(): number { - for (let i = 5011;; i++) { + for (let i = 5011; ; i++) { if (!ports.has(i)) { - ports.add(i); - return i; + ports.add(i) + return i } } } class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate { - private ffmpegDebugOutput = false; + private ffmpegDebugOutput = false - controller?: CameraController; + controller?: CameraController // keep track of sessions - pendingSessions: Record = {}; - ongoingSessions: Record = {}; + pendingSessions: Record = {} + ongoingSessions: Record = {} // minimal secure video properties. - configuration?: CameraRecordingConfiguration; - handlingStreamingRequest = false; - server?: MP4StreamingServer; + configuration?: CameraRecordingConfiguration + handlingStreamingRequest = false + server?: MP4StreamingServer handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void { - const ffmpegCommand = `-f lavfi -i testsrc=s=${request.width}x${request.height} -vframes 1 -f mjpeg -`; - const ffmpeg = spawn("ffmpeg", ffmpegCommand.split(" "), { env: process.env }); + const ffmpegCommand = `-f lavfi -i testsrc=s=${request.width}x${request.height} -vframes 1 -f mjpeg -` + const ffmpeg = spawn('ffmpeg', ffmpegCommand.split(' '), { env: process.env }) - const snapshotBuffers: Buffer[] = []; + const snapshotBuffers: Buffer[] = [] - ffmpeg.stdout.on("data", data => snapshotBuffers.push(data)); - ffmpeg.stderr.on("data", data => { + ffmpeg.stdout.on('data', data => snapshotBuffers.push(data)) + ffmpeg.stderr.on('data', (data) => { if (this.ffmpegDebugOutput) { - console.log("SNAPSHOT: " + String(data)); + console.log(`SNAPSHOT: ${String(data)}`) } - }); + }) - ffmpeg.on("exit", (code, signal) => { + ffmpeg.on('exit', (code, signal) => { if (signal) { - console.log("Snapshot process was killed with signal: " + signal); - callback(new Error("killed with signal " + signal)); + console.log(`Snapshot process was killed with signal: ${signal}`) + callback(new Error(`killed with signal ${signal}`)) } else if (code === 0) { - console.log(`Successfully captured snapshot at ${request.width}x${request.height}`); - callback(undefined, Buffer.concat(snapshotBuffers)); + console.log(`Successfully captured snapshot at ${request.width}x${request.height}`) + callback(undefined, Buffer.concat(snapshotBuffers)) } else { - console.log("Snapshot process exited with code " + code); - callback(new Error("Snapshot process exited with code " + code)); + console.log(`Snapshot process exited with code ${code}`) + callback(new Error(`Snapshot process exited with code ${code}`)) } - }); + }) } // called when iOS request rtp setup prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void { - const sessionId: StreamSessionIdentifier = request.sessionID; - const targetAddress = request.targetAddress; + const sessionId: StreamSessionIdentifier = request.sessionID + const targetAddress = request.targetAddress - const video = request.video; + const video = request.video - const videoCryptoSuite = video.srtpCryptoSuite; // could be used to support multiple crypto suite (or support no suite for debugging) - const videoSrtpKey = video.srtp_key; - const videoSrtpSalt = video.srtp_salt; + const videoCryptoSuite = video.srtpCryptoSuite // could be used to support multiple crypto suite (or support no suite for debugging) + const videoSrtpKey = video.srtp_key + const videoSrtpSalt = video.srtp_salt - const videoSSRC = CameraController.generateSynchronisationSource(); + const videoSSRC = CameraController.generateSynchronisationSource() - const localPort = getPort(); + const localPort = getPort() const sessionInfo: SessionInfo = { address: targetAddress, videoPort: video.port, localVideoPort: localPort, - videoCryptoSuite: videoCryptoSuite, + videoCryptoSuite, videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]), - videoSSRC: videoSSRC, - }; + videoSSRC, + } const response: PrepareStreamResponse = { video: { @@ -169,150 +179,151 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate srtp_salt: videoSrtpSalt, }, // audio is omitted as we do not support audio in this example - }; + } - this.pendingSessions[sessionId] = sessionInfo; - callback(undefined, response); + this.pendingSessions[sessionId] = sessionInfo + callback(undefined, response) } // called when iOS device asks stream to start/stop/reconfigure handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void { - const sessionId = request.sessionID; + const sessionId = request.sessionID switch (request.type) { - case StreamRequestTypes.START: { - const sessionInfo = this.pendingSessions[sessionId]; - - const video: VideoInfo = request.video; - - const profile = FFMPEGH264ProfileNames[video.profile]; - const level = FFMPEGH264LevelNames[video.level]; - const width = video.width; - const height = video.height; - const fps = video.fps; - - const payloadType = video.pt; - const maxBitrate = video.max_bit_rate; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const rtcpInterval = video.rtcp_interval; // usually 0.5 - const mtu = video.mtu; // maximum transmission unit - - const address = sessionInfo.address; - const videoPort = sessionInfo.videoPort; - const localVideoPort = sessionInfo.localVideoPort; - const ssrc = sessionInfo.videoSSRC; - const cryptoSuite = sessionInfo.videoCryptoSuite; - const videoSRTP = sessionInfo.videoSRTP.toString("base64"); - - console.log(`Starting video stream (${width}x${height}, ${fps} fps, ${maxBitrate} kbps, ${mtu} mtu)...`); - - let videoffmpegCommand = `-re -f lavfi -i testsrc=s=${width}x${height}:r=${fps} -map 0:0 ` + - `-c:v h264 -pix_fmt yuv420p -r ${fps} -an -sn -dn -b:v ${maxBitrate}k ` + - `-profile:v ${profile} -level:v ${level} ` + - `-payload_type ${payloadType} -ssrc ${ssrc} -f rtp `; - - if (cryptoSuite !== SRTPCryptoSuites.NONE) { - let suite: string; - switch (cryptoSuite) { - case SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80: // actually ffmpeg just supports AES_CM_128_HMAC_SHA1_80 - suite = "AES_CM_128_HMAC_SHA1_80"; - break; - case SRTPCryptoSuites.AES_CM_256_HMAC_SHA1_80: - suite = "AES_CM_256_HMAC_SHA1_80"; - break; - } - - videoffmpegCommand += `-srtp_out_suite ${suite} -srtp_out_params ${videoSRTP} s`; - } - - videoffmpegCommand += `rtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${localVideoPort}&pkt_size=${mtu}`; - - if (this.ffmpegDebugOutput) { - console.log("FFMPEG command: ffmpeg " + videoffmpegCommand); - } - - const ffmpegVideo = spawn("ffmpeg", videoffmpegCommand.split(" "), { env: process.env }); - - let started = false; - ffmpegVideo.stderr.on("data", (data: Buffer) => { - console.log(data.toString("utf8")); - if (!started) { - started = true; - console.log("FFMPEG: received first frame"); + case StreamRequestTypes.START: { + const sessionInfo = this.pendingSessions[sessionId] + + const video: VideoInfo = request.video + + const profile = FFMPEGH264ProfileNames[video.profile] + const level = FFMPEGH264LevelNames[video.level] + const width = video.width + const height = video.height + const fps = video.fps + + const payloadType = video.pt + const maxBitrate = video.max_bit_rate + + // eslint-disable-next-line unused-imports/no-unused-vars + const rtcpInterval = video.rtcp_interval // usually 0.5 + const mtu = video.mtu // maximum transmission unit + + const address = sessionInfo.address + const videoPort = sessionInfo.videoPort + const localVideoPort = sessionInfo.localVideoPort + const ssrc = sessionInfo.videoSSRC + const cryptoSuite = sessionInfo.videoCryptoSuite + const videoSRTP = sessionInfo.videoSRTP.toString('base64') + + console.log(`Starting video stream (${width}x${height}, ${fps} fps, ${maxBitrate} kbps, ${mtu} mtu)...`) + + let videoffmpegCommand = `-re -f lavfi -i testsrc=s=${width}x${height}:r=${fps} -map 0:0 ` + + `-c:v h264 -pix_fmt yuv420p -r ${fps} -an -sn -dn -b:v ${maxBitrate}k ` + + `-profile:v ${profile} -level:v ${level} ` + + `-payload_type ${payloadType} -ssrc ${ssrc} -f rtp ` + + if (cryptoSuite !== SRTPCryptoSuites.NONE) { + let suite: string + switch (cryptoSuite) { + case SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80: // actually ffmpeg just supports AES_CM_128_HMAC_SHA1_80 + suite = 'AES_CM_128_HMAC_SHA1_80' + break + case SRTPCryptoSuites.AES_CM_256_HMAC_SHA1_80: + suite = 'AES_CM_256_HMAC_SHA1_80' + break + } - callback(); // do not forget to execute callback once set up + videoffmpegCommand += `-srtp_out_suite ${suite} -srtp_out_params ${videoSRTP} s` } + videoffmpegCommand += `rtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${localVideoPort}&pkt_size=${mtu}` + if (this.ffmpegDebugOutput) { - console.log("VIDEO: " + String(data)); + console.log(`FFMPEG command: ffmpeg ${videoffmpegCommand}`) } - }); - ffmpegVideo.on("error", error => { - console.log("[Video] Failed to start video stream: " + error.message); - callback(new Error("ffmpeg process creation failed!")); - }); - ffmpegVideo.on("exit", (code, signal) => { - const message = "[Video] ffmpeg exited with code: " + code + " and signal: " + signal; - - if (code == null || code === 255) { - console.log(message + " (Video stream stopped!)"); - } else { - console.log(message + " (error)"); + const ffmpegVideo = spawn('ffmpeg', videoffmpegCommand.split(' '), { env: process.env }) + + let started = false + ffmpegVideo.stderr.on('data', (data: Buffer) => { + console.log(data.toString('utf8')) if (!started) { - callback(new Error(message)); + started = true + console.log('FFMPEG: received first frame') + + callback() // do not forget to execute callback once set up + } + + if (this.ffmpegDebugOutput) { + console.log(`VIDEO: ${String(data)}`) + } + }) + ffmpegVideo.on('error', (error) => { + console.log(`[Video] Failed to start video stream: ${error.message}`) + callback(new Error('ffmpeg process creation failed!')) + }) + ffmpegVideo.on('exit', (code, signal) => { + const message = `[Video] ffmpeg exited with code: ${code} and signal: ${signal}` + + if (code == null || code === 255) { + console.log(`${message} (Video stream stopped!)`) } else { - this.controller!.forceStopStreamingSession(sessionId); + console.log(`${message} (error)`) + + if (!started) { + callback(new Error(message)) + } else { + this.controller!.forceStopStreamingSession(sessionId) + } } - } - }); + }) - this.ongoingSessions[sessionId] = { - localVideoPort: localVideoPort, - process: ffmpegVideo, - }; - delete this.pendingSessions[sessionId]; + this.ongoingSessions[sessionId] = { + localVideoPort, + process: ffmpegVideo, + } + delete this.pendingSessions[sessionId] - break; - } - case StreamRequestTypes.RECONFIGURE: - // not supported by this example - console.log("Received (unsupported) request to reconfigure to: " + JSON.stringify(request.video)); - callback(); - break; - case StreamRequestTypes.STOP: { - const ongoingSession = this.ongoingSessions[sessionId]; - if (!ongoingSession) { - callback(); - break; + break } + case StreamRequestTypes.RECONFIGURE: + // not supported by this example + console.log(`Received (unsupported) request to reconfigure to: ${JSON.stringify(request.video)}`) + callback() + break + case StreamRequestTypes.STOP: { + const ongoingSession = this.ongoingSessions[sessionId] + if (!ongoingSession) { + callback() + break + } - ports.delete(ongoingSession.localVideoPort); + ports.delete(ongoingSession.localVideoPort) - try { - ongoingSession.process.kill("SIGKILL"); - } catch (e) { - console.log("Error occurred terminating the video process!"); - console.log(e); - } + try { + ongoingSession.process.kill('SIGKILL') + } catch (e) { + console.log('Error occurred terminating the video process!') + console.log(e) + } - delete this.ongoingSessions[sessionId]; + delete this.ongoingSessions[sessionId] - console.log("Stopped streaming session!"); - callback(); - break; - } + console.log('Stopped streaming session!') + callback() + break + } } } updateRecordingActive(active: boolean): void { // we haven't implemented a prebuffer - console.log("Recording active set to " + active); + console.log(`Recording active set to ${active}`) } updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void { - this.configuration = configuration; - console.log(configuration); + this.configuration = configuration + console.log(configuration) } /** @@ -320,13 +331,13 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate * CameraController supporting HomeKit Secure Video. * * An ideal implementation would diverge from this in the following ways: - * * It would implement a prebuffer and respect the recording `active` characteristic for that. - * * It would start to immediately record after a trigger event occurred and not just + * It would implement a prebuffer and respect the recording `active` characteristic for that. + * It would start to immediately record after a trigger event occurred and not just * when the HomeKit Controller requests it (see the documentation of `CameraRecordingDelegate`). */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars async *handleRecordingStreamRequest(streamId: number): AsyncGenerator { - assert(!!this.configuration); + assert(!!this.configuration) /** * With this flag you can control how the generator reacts to a reset to the motion trigger. @@ -335,208 +346,217 @@ class ExampleCamera implements CameraStreamingDelegate, CameraRecordingDelegate * * Note: In a real implementation you would most likely introduce a bit of a delay. */ - const STOP_AFTER_MOTION_STOP = false; + const STOP_AFTER_MOTION_STOP = false - this.handlingStreamingRequest = true; + this.handlingStreamingRequest = true - assert(this.configuration.videoCodec.type === VideoCodecType.H264); + assert(this.configuration.videoCodec.type === VideoCodecType.H264) - const profile = this.configuration.videoCodec.parameters.profile === H264Profile.HIGH ? "high" - : this.configuration.videoCodec.parameters.profile === H264Profile.MAIN ? "main" : "baseline"; + const profile = this.configuration.videoCodec.parameters.profile === H264Profile.HIGH + ? 'high' + : this.configuration.videoCodec.parameters.profile === H264Profile.MAIN ? 'main' : 'baseline' - const level = this.configuration.videoCodec.parameters.level === H264Level.LEVEL4_0 ? "4.0" - : this.configuration.videoCodec.parameters.level === H264Level.LEVEL3_2 ? "3.2" : "3.1"; + const level = this.configuration.videoCodec.parameters.level === H264Level.LEVEL4_0 + ? '4.0' + : this.configuration.videoCodec.parameters.level === H264Level.LEVEL3_2 ? '3.2' : '3.1' const videoArgs: Array = [ - "-an", - "-sn", - "-dn", - "-codec:v", - "libx264", - "-pix_fmt", - "yuv420p", - - "-profile:v", profile, - "-level:v", level, - "-b:v", `${this.configuration.videoCodec.parameters.bitRate}k`, - "-force_key_frames", `expr:eq(t,n_forced*${this.configuration.videoCodec.parameters.iFrameInterval / 1000})`, - "-r", this.configuration.videoCodec.resolution[2].toString(), - ]; - - let samplerate: string; + '-an', + '-sn', + '-dn', + '-codec:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + + '-profile:v', + profile, + '-level:v', + level, + '-b:v', + `${this.configuration.videoCodec.parameters.bitRate}k`, + '-force_key_frames', + `expr:eq(t,n_forced*${this.configuration.videoCodec.parameters.iFrameInterval / 1000})`, + '-r', + this.configuration.videoCodec.resolution[2].toString(), + ] + + let samplerate: string switch (this.configuration.audioCodec.samplerate) { - case AudioRecordingSamplerate.KHZ_8: - samplerate = "8"; - break; - case AudioRecordingSamplerate.KHZ_16: - samplerate = "16"; - break; - case AudioRecordingSamplerate.KHZ_24: - samplerate = "24"; - break; - case AudioRecordingSamplerate.KHZ_32: - samplerate = "32"; - break; - case AudioRecordingSamplerate.KHZ_44_1: - samplerate = "44.1"; - break; - case AudioRecordingSamplerate.KHZ_48: - samplerate = "48"; - break; - default: - throw new Error("Unsupported audio samplerate: " + this.configuration.audioCodec.samplerate); + case AudioRecordingSamplerate.KHZ_8: + samplerate = '8' + break + case AudioRecordingSamplerate.KHZ_16: + samplerate = '16' + break + case AudioRecordingSamplerate.KHZ_24: + samplerate = '24' + break + case AudioRecordingSamplerate.KHZ_32: + samplerate = '32' + break + case AudioRecordingSamplerate.KHZ_44_1: + samplerate = '44.1' + break + case AudioRecordingSamplerate.KHZ_48: + samplerate = '48' + break + default: + throw new Error(`Unsupported audio samplerate: ${this.configuration.audioCodec.samplerate}`) } const audioArgs: Array = this.controller?.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive) ? [ - "-acodec", "libfdk_aac", - ...(this.configuration.audioCodec.type === AudioRecordingCodecType.AAC_LC ? - ["-profile:a", "aac_low"] : - ["-profile:a", "aac_eld"]), - "-ar", `${samplerate}k`, - "-b:a", `${this.configuration.audioCodec.bitrate}k`, - "-ac", `${this.configuration.audioCodec.audioChannels}`, - ] - : []; + '-acodec', + 'libfdk_aac', + ...(this.configuration.audioCodec.type === AudioRecordingCodecType.AAC_LC + ? ['-profile:a', 'aac_low'] + : ['-profile:a', 'aac_eld']), + '-ar', + `${samplerate}k`, + '-b:a', + `${this.configuration.audioCodec.bitrate}k`, + '-ac', + `${this.configuration.audioCodec.audioChannels}`, + ] + : [] this.server = new MP4StreamingServer( - "ffmpeg", + 'ffmpeg', `-f lavfi -i \ testsrc=s=${this.configuration.videoCodec.resolution[0]}x${this.configuration.videoCodec.resolution[1]}:r=${this.configuration.videoCodec.resolution[2]}` - .split(/ /g), + .split(/ /), audioArgs, videoArgs, - ); + ) - await this.server.start(); + await this.server.start() if (!this.server || this.server.destroyed) { - return; // early exit + return // early exit } - const pending: Array = []; + const pending: Array = [] try { for await (const box of this.server.generator()) { - pending.push(box.header, box.data); + pending.push(box.header, box.data) - const motionDetected = camera.getService(Service.MotionSensor)?.getCharacteristic(Characteristic.MotionDetected).value; + const motionDetected = camera.getService(Service.MotionSensor)?.getCharacteristic(Characteristic.MotionDetected).value - console.log("mp4 box type " + box.type + " and length " + box.length); - if (box.type === "moov" || box.type === "mdat") { - const fragment = Buffer.concat(pending); - pending.splice(0, pending.length); + console.log(`mp4 box type ${box.type} and length ${box.length}`) + if (box.type === 'moov' || box.type === 'mdat') { + const fragment = Buffer.concat(pending) + pending.splice(0, pending.length) - const isLast = STOP_AFTER_MOTION_STOP && !motionDetected; + const isLast = STOP_AFTER_MOTION_STOP && !motionDetected yield { data: fragment, - isLast: isLast, - }; + isLast, + } if (isLast) { - console.log("Ending session due to motion stopped!"); - break; + console.log('Ending session due to motion stopped!') + break } } } } catch (error) { - if (!error.message.startsWith("FFMPEG")) { // cheap way of identifying our own emitted errors - console.error("Encountered unexpected error on generator " + error.stack); + if (!error.message.startsWith('FFMPEG')) { // cheap way of identifying our own emitted errors + console.error(`Encountered unexpected error on generator ${error.stack}`) } } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars closeRecordingStream(streamId: number, reason?: HDSProtocolSpecificErrorReason): void { if (this.server) { - this.server.destroy(); - this.server = undefined; + this.server.destroy() + this.server = undefined } - this.handlingStreamingRequest = false; + this.handlingStreamingRequest = false } acknowledgeStream(streamId: number): void { - this.closeRecordingStream(streamId); + this.closeRecordingStream(streamId) } } class MP4StreamingServer { - readonly server: Server; + readonly server: Server /** * This can be configured to output ffmpeg debug output! */ - readonly debugMode: boolean = false; + readonly debugMode: boolean = false - readonly ffmpegPath: string; - readonly args: string[]; + readonly ffmpegPath: string + readonly args: string[] - socket?: Socket; - childProcess?: ChildProcess; - destroyed = false; + socket?: Socket + childProcess?: ChildProcess + destroyed = false - connectPromise: Promise; - connectResolve?: () => void; + connectPromise: Promise + connectResolve?: () => void constructor(ffmpegPath: string, ffmpegInput: Array, audioOutputArgs: Array, videoOutputArgs: Array) { - this.connectPromise = new Promise(resolve => this.connectResolve = resolve); + this.connectPromise = new Promise(resolve => this.connectResolve = resolve) - this.server = createServer(this.handleConnection.bind(this)); - this.ffmpegPath = ffmpegPath; - this.args = []; + this.server = createServer(this.handleConnection.bind(this)) + this.ffmpegPath = ffmpegPath + this.args = [] - this.args.push(...ffmpegInput); + this.args.push(...ffmpegInput) - this.args.push(...audioOutputArgs); + this.args.push(...audioOutputArgs) - this.args.push("-f", "mp4"); - this.args.push(...videoOutputArgs); - this.args.push("-fflags", - "+genpts", - "-reset_timestamps", - "1"); + this.args.push('-f', 'mp4') + this.args.push(...videoOutputArgs) + this.args.push('-fflags', '+genpts', '-reset_timestamps', '1') this.args.push( - "-movflags", "frag_keyframe+empty_moov+default_base_moof", - ); + '-movflags', + 'frag_keyframe+empty_moov+default_base_moof', + ) } async start() { - const promise = once(this.server, "listening"); - this.server.listen(); // listen on random port - await promise; + const promise = once(this.server, 'listening') + this.server.listen() // listen on random port + await promise if (this.destroyed) { - return; + return } - const port = (this.server.address() as AddressInfo).port; - this.args.push("tcp://127.0.0.1:" + port); + const port = (this.server.address() as AddressInfo).port + this.args.push(`tcp://127.0.0.1:${port}`) - console.log(this.ffmpegPath + " " + this.args.join(" ")); + console.log(`${this.ffmpegPath} ${this.args.join(' ')}`) - this.childProcess = spawn(this.ffmpegPath, this.args, { env: process.env, stdio: this.debugMode? "pipe": "ignore" }); - if (!this.childProcess ||!this.childProcess.stdout || !this.childProcess.stderr) { - throw new Error("ChildProcess or its streams is undefined directly after the init!"); + this.childProcess = spawn(this.ffmpegPath, this.args, { env: process.env, stdio: this.debugMode ? 'pipe' : 'ignore' }) + if (!this.childProcess || !this.childProcess.stdout || !this.childProcess.stderr) { + throw new Error('ChildProcess or its streams is undefined directly after the init!') } - if(this.debugMode) { - this.childProcess.stdout?.on("data", data => console.log(data.toString())); - this.childProcess.stderr?.on("data", data => console.log(data.toString())); + if (this.debugMode) { + this.childProcess.stdout?.on('data', data => console.log(data.toString())) + this.childProcess.stderr?.on('data', data => console.log(data.toString())) } } destroy() { - this.socket?.destroy(); - this.childProcess?.kill(); + this.socket?.destroy() + this.childProcess?.kill() - this.socket = undefined; - this.childProcess = undefined; - this.destroyed = true; + this.socket = undefined + this.childProcess = undefined + this.destroyed = true } handleConnection(socket: Socket): void { - this.server.close(); // don't accept any further clients - this.socket = socket; - this.connectResolve?.(); + this.server.close() // don't accept any further clients + this.socket = socket + this.connectResolve?.() } /** @@ -544,74 +564,77 @@ class MP4StreamingServer { * Throws error to signal EOF when socket is closed. */ async* generator(): AsyncGenerator { - await this.connectPromise; + await this.connectPromise if (!this.socket || !this.childProcess) { - console.log("Socket undefined " + !!this.socket + " childProcess undefined " + !!this.childProcess); - throw new Error("Unexpected state!"); + console.log(`Socket undefined ${!!this.socket} childProcess undefined ${!!this.childProcess}`) + throw new Error('Unexpected state!') } while (true) { - const header = await this.read(8); - const length = header.readInt32BE(0) - 8; - const type = header.slice(4).toString(); - const data = await this.read(length); + const header = await this.read(8) + const length = header.readInt32BE(0) - 8 + const type = header.subarray(4).toString() + const data = await this.read(length) yield { - header: header, - length: length, - type: type, - data: data, - }; + header, + length, + type, + data, + } } } async read(length: number): Promise { if (!this.socket) { - throw Error("FFMPEG tried reading from closed socket!"); + throw new Error('FFMPEG tried reading from closed socket!') } if (!length) { - return Buffer.alloc(0); + return Buffer.alloc(0) } - const value = this.socket.read(length); + const value = this.socket.read(length) if (value) { - return value; + return value } return new Promise((resolve, reject) => { - const readHandler = () => { - const value = this.socket!.read(length); - if (value) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - cleanup(); - resolve(value); - } - }; - - const endHandler = () => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - cleanup(); - reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`)); - }; - const cleanup = () => { - this.socket?.removeListener("readable", readHandler); - this.socket?.removeListener("close", endHandler); - }; + this.socket?.removeListener('readable', () => { + const value = this.socket!.read(length) + if (value) { + cleanup() + resolve(value) + } + }) + this.socket?.removeListener('close', () => { + cleanup() + reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`)) + }) + } if (!this.socket) { - throw new Error("FFMPEG socket is closed now!"); + throw new Error('FFMPEG socket is closed now!') } - this.socket.on("readable", readHandler); - this.socket.on("close", endHandler); - }); + this.socket.on('readable', () => { + const value = this.socket!.read(length) + if (value) { + cleanup() + resolve(value) + } + }) + this.socket.on('close', () => { + cleanup() + reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`)) + }) + }) } } -const streamDelegate = new ExampleCamera(); +const streamDelegate = new ExampleCamera() const cameraController = new CameraController({ cameraStreamCount: 2, // HomeKit requires at least 2 streams, but 1 is also just fine @@ -698,15 +721,15 @@ const cameraController = new CameraController({ motion: true, occupancy: true, }, -}); -streamDelegate.controller = cameraController; +}) +streamDelegate.controller = cameraController -camera.configureController(cameraController); +camera.configureController(cameraController) // a service to trigger the motion sensor! -camera.addService(Service.Switch, "MOTION TRIGGER") +camera.addService(Service.Switch, 'MOTION TRIGGER') .getCharacteristic(Characteristic.On) - .onSet(value => { + .onSet((value) => { camera.getService(Service.MotionSensor) - ?.updateCharacteristic(Characteristic.MotionDetected, value); - }); + ?.updateCharacteristic(Characteristic.MotionDetected, value) + }) diff --git a/src/accessories/Fan_accessory.ts b/src/accessories/Fan_accessory.ts index 101b46368..3c8074497 100644 --- a/src/accessories/Fan_accessory.ts +++ b/src/accessories/Fan_accessory.ts @@ -1,67 +1,67 @@ +/* eslint-disable no-console */ // here's a fake hardware device that we'll expose to HomeKit +import type { CharacteristicValue, VoidCallback } from '../index.js' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, - CharacteristicValue, Service, uuid, - VoidCallback, -} from ".."; +} from '../index.js' -// eslint-disable-next-line @typescript-eslint/no-explicit-any const FAKE_FAN: Record = { powerOn: false, rSpeed: 100, setPowerOn: (on: CharacteristicValue) => { if (on) { - //put your code here to turn on the fan - FAKE_FAN.powerOn = on; + // put your code here to turn on the fan + FAKE_FAN.powerOn = on } else { - //put your code here to turn off the fan - FAKE_FAN.powerOn = on; + // put your code here to turn off the fan + FAKE_FAN.powerOn = on } }, setSpeed: (value: CharacteristicValue) => { - console.log("Setting fan rSpeed to %s", value); - FAKE_FAN.rSpeed = value; - //put your code here to set the fan to a specific value + console.log('Setting fan rSpeed to %s', value) + FAKE_FAN.rSpeed = value + // put your code here to set the fan to a specific value }, identify: () => { - //put your code here to identify the fan - console.log("Fan Identified!"); + // put your code here to identify the fan + console.log('Fan Identified!') }, -}; +} // This is the Accessory that we'll return to HAP-NodeJS that represents our fake fan. -const fan = exports.accessory = new Accessory("Fan", uuid.generate("hap-nodejs:accessories:Fan")); +const fan = exports.accessory = new Accessory('Fan', uuid.generate('hap-nodejs:accessories:Fan')) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -fan.username = "1A:2B:3C:4D:5E:FF"; +fan.username = '1A:2B:3C:4D:5E:FF' // @ts-expect-error: Core/BridgeCore API -fan.pincode = "031-45-154"; -fan.category = Categories.FAN; +fan.pincode = '031-45-154' +fan.category = Categories.FAN // set some basic properties (these values are arbitrary and setting them is optional) fan .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Sample Company"); + .setCharacteristic(Characteristic.Manufacturer, 'Sample Company') // listen for the "identify" event for this Accessory fan.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - FAKE_FAN.identify(); - callback(); // success -}); + FAKE_FAN.identify() + callback() // success +}) // Add the actual Fan Service and listen for change events from iOS. fan - .addService(Service.Fan, "Fan") // services exposed to the user should have "names" like "Fake Light" for us + .addService(Service.Fan, 'Fan') // services exposed to the user should have "names" like "Fake Light" for us .getCharacteristic(Characteristic.On)! .onSet((value) => { - FAKE_FAN.setPowerOn(value); - }); + FAKE_FAN.setPowerOn(value) + }) // We want to intercept requests for our current power state so we can query the hardware itself instead of // allowing HAP-NodeJS to return the cached Characteristic.value. @@ -69,21 +69,20 @@ fan .getService(Service.Fan)! .getCharacteristic(Characteristic.On)! .onGet(() => { - // this event is emitted when you ask Siri directly whether your fan is on or not. you might query // the fan hardware itself to find this out, then call the callback. But if you take longer than a // few seconds to respond, Siri will give up. - return !!FAKE_FAN.powerOn; - }); + return !!FAKE_FAN.powerOn + }) // also add an "optional" Characteristic for speed fan .getService(Service.Fan)! .addCharacteristic(Characteristic.RotationSpeed) .onGet(async () => { - return FAKE_FAN.rSpeed; + return FAKE_FAN.rSpeed }) .onSet(async (value) => { - FAKE_FAN.setSpeed(value); - }); + FAKE_FAN.setSpeed(value) + }) diff --git a/src/accessories/GarageDoorOpener_accessory.ts b/src/accessories/GarageDoorOpener_accessory.ts index 26dd8d3e7..174c0604a 100644 --- a/src/accessories/GarageDoorOpener_accessory.ts +++ b/src/accessories/GarageDoorOpener_accessory.ts @@ -1,94 +1,92 @@ +/* eslint-disable no-console */ +import type { CharacteristicSetCallback, CharacteristicValue, NodeCallback, VoidCallback } from '../index.js' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, - CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, - NodeCallback, + CharacteristicEventTypes, Service, uuid, - VoidCallback, -} from ".."; +} from '../index.js' const FAKE_GARAGE = { opened: false, open: () => { - console.log("Opening the Garage!"); - //add your code here which allows the garage to open - FAKE_GARAGE.opened = true; + console.log('Opening the Garage!') + // add your code here which allows the garage to open + FAKE_GARAGE.opened = true }, close: () => { - console.log("Closing the Garage!"); - //add your code here which allows the garage to close - FAKE_GARAGE.opened = false; + console.log('Closing the Garage!') + // add your code here which allows the garage to close + FAKE_GARAGE.opened = false }, identify: () => { - //add your code here which allows the garage to be identified - console.log("Identify the Garage"); + // add your code here which allows the garage to be identified + console.log('Identify the Garage') }, status: () => { - //use this section to get sensor values. set the boolean FAKE_GARAGE.opened with a sensor value. - console.log("Sensor queried!"); - //FAKE_GARAGE.opened = true/false; + // use this section to get sensor values. set the boolean FAKE_GARAGE.opened with a sensor value. + console.log('Sensor queried!') + // FAKE_GARAGE.opened = true/false; }, -}; +} -const garageUUID = uuid.generate("hap-nodejs:accessories:" + "GarageDoor"); -const garage = exports.accessory = new Accessory("Garage Door", garageUUID); +const garageUUID = uuid.generate('hap-nodejs:accessories:' + 'GarageDoor') +const garage = exports.accessory = new Accessory('Garage Door', garageUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -garage.username = "C1:5D:3F:EE:5E:FA"; //edit this if you use Core.js +garage.username = 'C1:5D:3F:EE:5E:FA' // edit this if you use Core.js // @ts-expect-error: Core/BridgeCore API -garage.pincode = "031-45-154"; -garage.category = Categories.GARAGE_DOOR_OPENER; +garage.pincode = '031-45-154' +garage.category = Categories.GARAGE_DOOR_OPENER garage .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Liftmaster") - .setCharacteristic(Characteristic.Model, "Rev-1") - .setCharacteristic(Characteristic.SerialNumber, "TW000165"); + .setCharacteristic(Characteristic.Manufacturer, 'Liftmaster') + .setCharacteristic(Characteristic.Model, 'Rev-1') + .setCharacteristic(Characteristic.SerialNumber, 'TW000165') garage.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - FAKE_GARAGE.identify(); - callback(); -}); + FAKE_GARAGE.identify() + callback() +}) garage - .addService(Service.GarageDoorOpener, "Garage Door") - .setCharacteristic(Characteristic.TargetDoorState, Characteristic.TargetDoorState.CLOSED) // force initial state to CLOSED + .addService(Service.GarageDoorOpener, 'Garage Door') + .setCharacteristic(Characteristic.TargetDoorState, Characteristic.TargetDoorState.CLOSED) // force initial state to 'CLOSED' .getCharacteristic(Characteristic.TargetDoorState)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - if (value === Characteristic.TargetDoorState.CLOSED) { - FAKE_GARAGE.close(); - callback(); + FAKE_GARAGE.close() + callback() garage .getService(Service.GarageDoorOpener)! - .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.CLOSED); + .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.CLOSED) } else if (value === Characteristic.TargetDoorState.OPEN) { - FAKE_GARAGE.open(); - callback(); + FAKE_GARAGE.open() + callback() garage .getService(Service.GarageDoorOpener)! - .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.OPEN); + .setCharacteristic(Characteristic.CurrentDoorState, Characteristic.CurrentDoorState.OPEN) } - }); - + }) garage .getService(Service.GarageDoorOpener)! .getCharacteristic(Characteristic.CurrentDoorState)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - - const err = null; - FAKE_GARAGE.status(); + const err = null + FAKE_GARAGE.status() if (FAKE_GARAGE.opened) { - console.log("Query: Is Garage Open? Yes."); - callback(err, Characteristic.CurrentDoorState.OPEN); + console.log('Query: Is Garage Open? Yes.') + callback(err, Characteristic.CurrentDoorState.OPEN) } else { - console.log("Query: Is Garage Open? No."); - callback(err, Characteristic.CurrentDoorState.CLOSED); + console.log('Query: Is Garage Open? No.') + callback(err, Characteristic.CurrentDoorState.CLOSED) } - }); + }) diff --git a/src/accessories/Light-AdaptiveLighting_accessory.ts b/src/accessories/Light-AdaptiveLighting_accessory.ts index 6888c970b..fe4fd3d6c 100644 --- a/src/accessories/Light-AdaptiveLighting_accessory.ts +++ b/src/accessories/Light-AdaptiveLighting_accessory.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-console */ +import util from 'node:util' + import { Accessory, AdaptiveLightingController, @@ -7,8 +10,7 @@ import { ColorUtils, Service, uuid, -} from ".."; -import util from "util"; +} from '../index.js' /** * This example light gives an example how a light with AdaptiveLighting (in AUTOMATIC mode) support @@ -22,106 +24,106 @@ import util from "util"; * AdaptiveLighting setup is pretty much at the end of the file, don't miss it. */ -const lightUUID = uuid.generate("hap-nodejs:accessories:light-adaptive-lighting"); -const accessory = exports.accessory = new Accessory("Light Example", lightUUID); +const lightUUID = uuid.generate('hap-nodejs:accessories:light-adaptive-lighting') +const accessory = exports.accessory = new Accessory('Light Example', lightUUID) // this section stores the basic state of the lightbulb -let on = false; -let brightness = 100; -let colorTemperature = 140; // 140 is the lowest color temperature in mired as by the HAP spec (you can lower the minimum though) -let hue = 0; // we start with white color -let saturation = 0; +let on = false +let brightness = 100 +let colorTemperature = 140 // 140 is the lowest color temperature in mired as by the HAP spec (you can lower the minimum though) +let hue = 0 // we start with white color +let saturation = 0 // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -accessory.username = "AA:BB:CC:DD:EE:FF"; +accessory.username = 'AA:BB:CC:DD:EE:FF' // @ts-expect-error: Core/BridgeCore API -accessory.pincode = "031-45-154"; -accessory.category = Categories.LIGHTBULB; +accessory.pincode = '031-45-154' +accessory.category = Categories.LIGHTBULB accessory.getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "HAP-NodeJS") - .setCharacteristic(Characteristic.Model, "Light with AdaptiveLighting") - .setCharacteristic(Characteristic.FirmwareRevision, "1.0.0"); + .setCharacteristic(Characteristic.Manufacturer, 'HAP-NodeJS') + .setCharacteristic(Characteristic.Model, 'Light with AdaptiveLighting') + .setCharacteristic(Characteristic.FirmwareRevision, '1.0.0') -const lightbulbService = accessory.addService(Service.Lightbulb, "Light Example"); +const lightbulbService = accessory.addService(Service.Lightbulb, 'Light Example') lightbulbService.getCharacteristic(Characteristic.On) .onGet(() => { - console.log("Light power is currently " + on); - return on; + console.log(`Light power is currently ${on}`) + return on + }) + .onSet((value) => { + console.log(`Light power was turn to ${on}`) + on = value as boolean }) - .onSet(value => { - console.log("Light power was turn to " + on); - on = value as boolean; - }); lightbulbService.getCharacteristic(Characteristic.Brightness) // Brightness characteristic is required for adaptive lighting .updateValue(brightness) // ensure default value is set .onGet(() => { - console.log("Light brightness is currently " + brightness); - return brightness; + console.log(`Light brightness is currently ${brightness}`) + return brightness + }) + .onSet((value) => { + console.log(`Light brightness was set to ${value}%`) + brightness = value as number }) - .onSet(value => { - console.log("Light brightness was set to " + value + "%"); - brightness = value as number; - }); lightbulbService.getCharacteristic(Characteristic.ColorTemperature) // ColorTemperature characteristic is required for adaptive lighting .onGet(() => { - console.log("Light color temperature is currently " + colorTemperature); - return colorTemperature; + console.log(`Light color temperature is currently ${colorTemperature}`) + return colorTemperature }) - .onSet(value => { - console.log("Light color temperature was set to " + value); - colorTemperature = value as number; + .onSet((value) => { + console.log(`Light color temperature was set to ${value}`) + colorTemperature = value as number // following statements are only needed when using ColorTemperature characteristic in combination with Hue/Saturation - const color = ColorUtils.colorTemperatureToHueAndSaturation(colorTemperature); + const color = ColorUtils.colorTemperatureToHueAndSaturation(colorTemperature) // save internal values for read handlers - hue = color.hue; - saturation = color.saturation; + hue = color.hue + saturation = color.saturation // and notify HomeKit devices about changed values - lightbulbService.getCharacteristic(Characteristic.Hue).updateValue(hue); - lightbulbService.getCharacteristic(Characteristic.Saturation).updateValue(saturation); - }); + lightbulbService.getCharacteristic(Characteristic.Hue).updateValue(hue) + lightbulbService.getCharacteristic(Characteristic.Saturation).updateValue(saturation) + }) lightbulbService.getCharacteristic(Characteristic.Hue) .onGet(() => { - console.log("Light hue is currently " + hue); - return hue; + console.log(`Light hue is currently ${hue}`) + return hue + }) + .onSet((value) => { + console.log(`Light hue was set to ${value}`) + hue = value as number + colorTemperature = 140 // setting color temperature to lowest possible value }) - .onSet(value => { - console.log("Light hue was set to " + value); - hue = value as number; - colorTemperature = 140; // setting color temperature to lowest possible value - }); lightbulbService.getCharacteristic(Characteristic.Saturation) .onGet(() => { - console.log("Light saturation is currently " + saturation); - return saturation; + console.log(`Light saturation is currently ${saturation}`) + return saturation + }) + .onSet((value) => { + console.log(`Light saturation was set to ${value}`) + saturation = value as number + colorTemperature = 140 // setting color temperature to lowest possible value }) - .onSet(value => { - console.log("Light saturation was set to " + value); - saturation = value as number; - colorTemperature = 140; // setting color temperature to lowest possible value - }); const adaptiveLightingController = new AdaptiveLightingController(lightbulbService, { // options object is optional, default mode is AUTOMATIC, can be set to MANUAL to do transitions yourself // look into the docs for more information controllerMode: AdaptiveLightingControllerMode.AUTOMATIC, -}); +}) // Requires AdaptiveLightingControllerMode.MANUAL to be set as a controllerMode -adaptiveLightingController.on("update", () => { - console.log("Adaptive Lighting updated"); -}).on("update", (update) => { - console.log("Adaptive Lighting schedule updated to " + util.inspect(update)); -}).on("disable", () => { - console.log("Adaptive Lighting disabled"); -}); - -accessory.configureController(adaptiveLightingController); +adaptiveLightingController.on('update', () => { + console.log('Adaptive Lighting updated') +}).on('update', (update) => { + console.log(`Adaptive Lighting schedule updated to ${util.inspect(update)}`) +}).on('disable', () => { + console.log('Adaptive Lighting disabled') +}) + +accessory.configureController(adaptiveLightingController) diff --git a/src/accessories/Light_accessory.ts b/src/accessories/Light_accessory.ts index 9062f1487..2fc38f8c7 100644 --- a/src/accessories/Light_accessory.ts +++ b/src/accessories/Light_accessory.ts @@ -1,145 +1,148 @@ +/* eslint-disable no-console */ +import type { + CharacteristicSetCallback, + CharacteristicValue, + NodeCallback, + VoidCallback, +} from '../index.js' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicSetCallback, - CharacteristicValue, - NodeCallback, Service, uuid, - VoidCallback, -} from ".."; +} from '../index.js' class LightControllerClass { - - name = "Simple Light"; //name of accessory - pincode: CharacteristicValue = "031-45-154"; - username: CharacteristicValue = "FA:3C:ED:5A:1A:1A"; // MAC like address used by HomeKit to differentiate accessories. - manufacturer: CharacteristicValue = "HAP-NodeJS"; //manufacturer (optional) - model: CharacteristicValue = "v1.0"; //model (optional) - serialNumber: CharacteristicValue = "A12S345KGB"; //serial number (optional) - - power: CharacteristicValue = false; //current power status - brightness: CharacteristicValue = 100; //current brightness - hue: CharacteristicValue = 0; //current hue - saturation: CharacteristicValue = 0; //current saturation - - outputLogs = true; //output logs - - setPower(status: CharacteristicValue) { //set power of accessory - if(this.outputLogs) { - console.log("Turning the '%s' %s", this.name, status ? "on" : "off"); + name = 'Simple Light' // name of accessory + pincode: CharacteristicValue = '031-45-154' + username: CharacteristicValue = 'FA:3C:ED:5A:1A:1A' // MAC like address used by HomeKit to differentiate accessories. + manufacturer: CharacteristicValue = 'HAP-NodeJS' // manufacturer (optional) + model: CharacteristicValue = 'v1.0' // model (optional) + serialNumber: CharacteristicValue = 'A12S345KGB' // serial number (optional) + + power: CharacteristicValue = false // current power status + brightness: CharacteristicValue = 100 // current brightness + hue: CharacteristicValue = 0 // current hue + saturation: CharacteristicValue = 0 // current saturation + + outputLogs = true // output logs + + setPower(status: CharacteristicValue) { // set power of accessory + if (this.outputLogs) { + console.log('Turning the \'%s\' %s', this.name, status ? 'on' : 'off') } - this.power = status; + this.power = status } - getPower() { //get power of accessory - if(this.outputLogs) { - console.log("'%s' is %s.", this.name, this.power ? "on" : "off"); + getPower() { // get power of accessory + if (this.outputLogs) { + console.log('\'%s\' is %s.', this.name, this.power ? 'on' : 'off') } - return this.power; + return this.power } - setBrightness(brightness: CharacteristicValue) { //set brightness - if(this.outputLogs) { - console.log("Setting '%s' brightness to %s", this.name, brightness); + setBrightness(brightness: CharacteristicValue) { // set brightness + if (this.outputLogs) { + console.log('Setting \'%s\' brightness to %s', this.name, brightness) } - this.brightness = brightness; + this.brightness = brightness } - getBrightness() { //get brightness - if(this.outputLogs) { - console.log("'%s' brightness is %s", this.name, this.brightness); + getBrightness() { // get brightness + if (this.outputLogs) { + console.log('\'%s\' brightness is %s', this.name, this.brightness) } - return this.brightness; + return this.brightness } - setSaturation(saturation: CharacteristicValue) { //set brightness - if(this.outputLogs) { - console.log("Setting '%s' saturation to %s", this.name, saturation); + setSaturation(saturation: CharacteristicValue) { // set brightness + if (this.outputLogs) { + console.log('Setting \'%s\' saturation to %s', this.name, saturation) } - this.saturation = saturation; + this.saturation = saturation } - getSaturation() { //get brightness - if(this.outputLogs) { - console.log("'%s' saturation is %s", this.name, this.saturation); + getSaturation() { // get brightness + if (this.outputLogs) { + console.log('\'%s\' saturation is %s', this.name, this.saturation) } - return this.saturation; + return this.saturation } - setHue(hue: CharacteristicValue) { //set brightness - if(this.outputLogs) { - console.log("Setting '%s' hue to %s", this.name, hue); + setHue(hue: CharacteristicValue) { // set brightness + if (this.outputLogs) { + console.log('Setting \'%s\' hue to %s', this.name, hue) } - this.hue = hue; + this.hue = hue } - getHue() { //get hue - if(this.outputLogs) { - console.log("'%s' hue is %s", this.name, this.hue); + getHue() { // get hue + if (this.outputLogs) { + console.log('\'%s\' hue is %s', this.name, this.hue) } - return this.hue; + return this.hue } - identify() { //identify the accessory - if(this.outputLogs) { - console.log("Identify the '%s'", this.name); + identify() { // identify the accessory + if (this.outputLogs) { + console.log('Identify the \'%s\'', this.name) } } } -const LightController = new LightControllerClass(); +const LightController = new LightControllerClass() // Generate a consistent UUID for our light Accessory that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the word "light". -const lightUUID = uuid.generate("hap-nodejs:accessories:light" + LightController.name); +const lightUUID = uuid.generate(`hap-nodejs:accessories:light${LightController.name}`) // This is the Accessory that we'll return to HAP-NodeJS that represents our light. -const lightAccessory = exports.accessory = new Accessory(LightController.name as string, lightUUID); +const lightAccessory = exports.accessory = new Accessory(LightController.name as string, lightUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -lightAccessory.username = LightController.username; +lightAccessory.username = LightController.username // @ts-expect-error: Core/BridgeCore API -lightAccessory.pincode = LightController.pincode; -lightAccessory.category = Categories.LIGHTBULB; +lightAccessory.pincode = LightController.pincode +lightAccessory.category = Categories.LIGHTBULB // set some basic properties (these values are arbitrary and setting them is optional) lightAccessory .getService(Service.AccessoryInformation)! .setCharacteristic(Characteristic.Manufacturer, LightController.manufacturer) .setCharacteristic(Characteristic.Model, LightController.model) - .setCharacteristic(Characteristic.SerialNumber, LightController.serialNumber); + .setCharacteristic(Characteristic.SerialNumber, LightController.serialNumber) // listen for the "identify" event for this Accessory lightAccessory.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - LightController.identify(); - callback(); -}); + LightController.identify() + callback() +}) // services exposed to the user should have "names" like "Light" for this case -const lightbulb = lightAccessory.addService(Service.Lightbulb, LightController.name); +const lightbulb = lightAccessory.addService(Service.Lightbulb, LightController.name) lightbulb.getCharacteristic(Characteristic.On) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - LightController.setPower(value); + LightController.setPower(value) // Our light is synchronous - this value has been successfully set // Invoke the callback when you finished processing the request // If it's going to take more than 1s to finish the request, try to invoke the callback // after getting the request instead of after finishing it. This avoids blocking other // requests from HomeKit. - callback(); + callback() }) // We want to intercept requests for our current power state, so we can query the hardware itself instead of // allowing HAP-NodeJS to return the cached Characteristic.value. .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - callback(null, LightController.getPower()); - }); + callback(null, LightController.getPower()) + }) // To inform HomeKit about changes occurred outside of HomeKit (like user physically turn on the light) // Please use Characteristic.updateValue @@ -152,30 +155,29 @@ lightbulb.getCharacteristic(Characteristic.On) // also add an "optional" Characteristic for Brightness lightbulb.addCharacteristic(Characteristic.Brightness) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - LightController.setBrightness(value); - callback(); + LightController.setBrightness(value) + callback() }) .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - callback(null, LightController.getBrightness()); - }); - + callback(null, LightController.getBrightness()) + }) // also add an "optional" Characteristic for Saturation lightbulb.addCharacteristic(Characteristic.Saturation) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - LightController.setSaturation(value); - callback(); + LightController.setSaturation(value) + callback() }) .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - callback(null, LightController.getSaturation()); - }); + callback(null, LightController.getSaturation()) + }) // also add an "optional" Characteristic for Hue lightbulb.addCharacteristic(Characteristic.Hue) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - LightController.setHue(value); - callback(); + LightController.setHue(value) + callback() }) .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - callback(null, LightController.getHue()); - }); + callback(null, LightController.getHue()) + }) diff --git a/src/accessories/Lock_accessory.ts b/src/accessories/Lock_accessory.ts index 1764e3f85..3a44e5f72 100644 --- a/src/accessories/Lock_accessory.ts +++ b/src/accessories/Lock_accessory.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { Accessory, AccessoryEventTypes, @@ -6,89 +7,87 @@ import { CharacteristicEventTypes, Service, uuid, -} from "../"; +} from '../index.js' // here's a fake hardware device that we'll expose to HomeKit const FAKE_LOCK = { locked: false, lock: () => { - console.log("Locking the lock!"); - FAKE_LOCK.locked = true; + console.log('Locking the lock!') + FAKE_LOCK.locked = true }, unlock: () => { - console.log("Unlocking the lock!"); - FAKE_LOCK.locked = false; + console.log('Unlocking the lock!') + FAKE_LOCK.locked = false }, identify: () => { - console.log("Identify the lock!"); + console.log('Identify the lock!') }, -}; +} // Generate a consistent UUID for our Lock Accessory that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the word "lock". -const lockUUID = uuid.generate("hap-nodejs:accessories:lock"); +const lockUUID = uuid.generate('hap-nodejs:accessories:lock') // This is the Accessory that we'll return to HAP-NodeJS that represents our fake lock. -const lock = exports.accessory = new Accessory("Lock", lockUUID); +const lock = exports.accessory = new Accessory('Lock', lockUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -lock.username = "C1:5D:3A:EE:5E:FA"; +lock.username = 'C1:5D:3A:EE:5E:FA' // @ts-expect-error: Core/BridgeCore API -lock.pincode = "031-45-154"; -lock.category = Categories.DOOR_LOCK; +lock.pincode = '031-45-154' +lock.category = Categories.DOOR_LOCK // set some basic properties (these values are arbitrary and setting them is optional) lock .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Lock Manufacturer") - .setCharacteristic(Characteristic.Model, "Rev-2") - .setCharacteristic(Characteristic.SerialNumber, "MY-Serial-Number"); + .setCharacteristic(Characteristic.Manufacturer, 'Lock Manufacturer') + .setCharacteristic(Characteristic.Model, 'Rev-2') + .setCharacteristic(Characteristic.SerialNumber, 'MY-Serial-Number') // listen for the "identify" event for this Accessory lock.on(AccessoryEventTypes.IDENTIFY, (paired, callback) => { - FAKE_LOCK.identify(); - callback(); // success -}); + FAKE_LOCK.identify() + callback() // success +}) -const service = new Service.LockMechanism("Fake Lock"); +const service = new Service.LockMechanism('Fake Lock') // Add the actual Door Lock Service and listen for change events from iOS. service.getCharacteristic(Characteristic.LockTargetState) .on(CharacteristicEventTypes.SET, (value, callback) => { - if (value === Characteristic.LockTargetState.UNSECURED) { - FAKE_LOCK.unlock(); - callback(); // Our fake Lock is synchronous - this value has been successfully set + FAKE_LOCK.unlock() + callback() // Our fake Lock is synchronous - this value has been successfully set // now we want to set our lock's "actual state" to be unsecured so it shows as unlocked in iOS apps - service.updateCharacteristic(Characteristic.LockCurrentState, Characteristic.LockCurrentState.UNSECURED); + service.updateCharacteristic(Characteristic.LockCurrentState, Characteristic.LockCurrentState.UNSECURED) } else if (value === Characteristic.LockTargetState.SECURED) { - FAKE_LOCK.lock(); - callback(); // Our fake Lock is synchronous - this value has been successfully set + FAKE_LOCK.lock() + callback() // Our fake Lock is synchronous - this value has been successfully set // now we want to set our lock's "actual state" to be locked so it shows as open in iOS apps - service.updateCharacteristic(Characteristic.LockCurrentState, Characteristic.LockCurrentState.SECURED); + service.updateCharacteristic(Characteristic.LockCurrentState, Characteristic.LockCurrentState.SECURED) } - }); + }) // We want to intercept requests for our current state so we can query the hardware itself instead of // allowing HAP-NodeJS to return the cached Characteristic.value. service.getCharacteristic(Characteristic.LockCurrentState) - .on(CharacteristicEventTypes.GET, callback => { - + .on(CharacteristicEventTypes.GET, (callback) => { // this event is emitted when you ask Siri directly whether your lock is locked or not. you might query // the lock hardware itself to find this out, then call the callback. But if you take longer than a // few seconds to respond, Siri will give up. if (FAKE_LOCK.locked) { - console.log("Are we locked? Yes."); - callback(undefined, Characteristic.LockCurrentState.SECURED); + console.log('Are we locked? Yes.') + callback(undefined, Characteristic.LockCurrentState.SECURED) } else { - console.log("Are we locked? No."); - callback(undefined, Characteristic.LockCurrentState.UNSECURED); + console.log('Are we locked? No.') + callback(undefined, Characteristic.LockCurrentState.UNSECURED) } - }); + }) -lock.addService(service); +lock.addService(service) diff --git a/src/accessories/MotionSensor_accessory.ts b/src/accessories/MotionSensor_accessory.ts index 1dd9f2796..d4a4c4d92 100644 --- a/src/accessories/MotionSensor_accessory.ts +++ b/src/accessories/MotionSensor_accessory.ts @@ -1,60 +1,65 @@ +/* eslint-disable no-console */ // here's a fake hardware device that we'll expose to HomeKit +import type { + CharacteristicValue, + NodeCallback, + VoidCallback, +} from '../index.js' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicValue, - NodeCallback, Service, - uuid, VoidCallback, -} from ".."; + uuid, +} from '../index.js' const MOTION_SENSOR = { motionDetected: false, getStatus: () => { - //set the boolean here, this will be returned to the device - MOTION_SENSOR.motionDetected = false; + // set the boolean here, this will be returned to the device + MOTION_SENSOR.motionDetected = false }, identify: () => { - console.log("Identify the motion sensor!"); + console.log('Identify the motion sensor!') }, -}; +} // Generate a consistent UUID for our Motion Sensor Accessory that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the word "motionsensor". -const motionSensorUUID = uuid.generate("hap-nodejs:accessories:motionsensor"); +const motionSensorUUID = uuid.generate('hap-nodejs:accessories:motionsensor') // This is the Accessory that we'll return to HAP-NodeJS that represents our fake motionSensor. -const motionSensor = exports.accessory = new Accessory("Motion Sensor", motionSensorUUID); +const motionSensor = exports.accessory = new Accessory('Motion Sensor', motionSensorUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -motionSensor.username = "1A:2B:3D:4D:2E:AF"; +motionSensor.username = '1A:2B:3D:4D:2E:AF' // @ts-expect-error: Core/BridgeCore API -motionSensor.pincode = "031-45-154"; -motionSensor.category = Categories.SENSOR; +motionSensor.pincode = '031-45-154' +motionSensor.category = Categories.SENSOR // set some basic properties (these values are arbitrary and setting them is optional) motionSensor .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Oltica") - .setCharacteristic(Characteristic.Model, "Rev-1") - .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); + .setCharacteristic(Characteristic.Manufacturer, 'Oltica') + .setCharacteristic(Characteristic.Model, 'Rev-1') + .setCharacteristic(Characteristic.SerialNumber, 'A1S2NASF88EW') // listen for the "identify" event for this Accessory motionSensor.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - MOTION_SENSOR.identify(); - callback(); // success -}); + MOTION_SENSOR.identify() + callback() // success +}) motionSensor - .addService(Service.MotionSensor, "Fake Motion Sensor") // services exposed to the user should have "names" like "Fake Motion Sensor" for us + .addService(Service.MotionSensor, 'Fake Motion Sensor') // services exposed to the user should have "names" like "Fake Motion Sensor" for us .getCharacteristic(Characteristic.MotionDetected)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - MOTION_SENSOR.getStatus(); - callback(null, Boolean(MOTION_SENSOR.motionDetected)); - }); + MOTION_SENSOR.getStatus() + callback(null, Boolean(MOTION_SENSOR.motionDetected)) + }) diff --git a/src/accessories/Outlet_accessory.ts b/src/accessories/Outlet_accessory.ts index e9a371f02..f9763eb9e 100644 --- a/src/accessories/Outlet_accessory.ts +++ b/src/accessories/Outlet_accessory.ts @@ -1,78 +1,79 @@ +/* eslint-disable no-console */ +import type { CharacteristicSetCallback, CharacteristicValue, NodeCallback, VoidCallback } from '../index.js' +import type { Nullable } from '../types' + import { Accessory, AccessoryEventTypes, Categories, Characteristic, - CharacteristicEventTypes, CharacteristicSetCallback, - CharacteristicValue, NodeCallback, + CharacteristicEventTypes, Service, uuid, - VoidCallback, -} from ".."; -import { Nullable } from "../types"; +} from '../index.js' -const err: Nullable = null; // in case there were any problems +const err: Nullable = null // in case there were any problems // here's a fake hardware device that we'll expose to HomeKit const FAKE_OUTLET = { powerOn: false, setPowerOn: (on: CharacteristicValue) => { - console.log("Turning the outlet %s!...", on ? "on" : "off"); + console.log('Turning the outlet %s!...', on ? 'on' : 'off') if (on) { - FAKE_OUTLET.powerOn = true; + FAKE_OUTLET.powerOn = true if (err) { - return console.log(err); + return console.log(err) } - console.log("...outlet is now on."); + console.log('...outlet is now on.') } else { - FAKE_OUTLET.powerOn = false; + FAKE_OUTLET.powerOn = false if (err) { - return console.log(err); + return console.log(err) } - console.log("...outlet is now off."); + console.log('...outlet is now off.') } }, - identify: function () { - console.log("Identify the outlet."); + identify() { + console.log('Identify the outlet.') }, -}; +} // Generate a consistent UUID for our outlet Accessory that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the accessory name. -const outletUUID = uuid.generate("hap-nodejs:accessories:Outlet"); +const outletUUID = uuid.generate('hap-nodejs:accessories:Outlet') // This is the Accessory that we'll return to HAP-NodeJS that represents our fake light. -const outlet = exports.accessory = new Accessory("Outlet", outletUUID); +const outlet = exports.accessory = new Accessory('Outlet', outletUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -outlet.username = "1A:2B:3C:4D:5D:FF"; +outlet.username = '1A:2B:3C:4D:5D:FF' // @ts-expect-error: Core/BridgeCore API -outlet.pincode = "031-45-154"; -outlet.category = Categories.OUTLET; +outlet.pincode = '031-45-154' +outlet.category = Categories.OUTLET // set some basic properties (these values are arbitrary and setting them is optional) outlet .getService(Service.AccessoryInformation)! - .setCharacteristic(Characteristic.Manufacturer, "Oltica") - .setCharacteristic(Characteristic.Model, "Rev-1") - .setCharacteristic(Characteristic.SerialNumber, "A1S2NASF88EW"); + .setCharacteristic(Characteristic.Manufacturer, 'Oltica') + .setCharacteristic(Characteristic.Model, 'Rev-1') + .setCharacteristic(Characteristic.SerialNumber, 'A1S2NASF88EW') // listen for the "identify" event for this Accessory outlet.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - FAKE_OUTLET.identify(); - callback(); // success -}); + FAKE_OUTLET.identify() + callback() // success +}) // Add the actual outlet Service and listen for change events from iOS. outlet - .addService(Service.Outlet, "Fake Outlet") // services exposed to the user should have "names" like "Fake Light" for us + .addService(Service.Outlet, 'Fake Outlet') // services exposed to the user should have "names" like "Fake Light" for us .getCharacteristic(Characteristic.On)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - FAKE_OUTLET.setPowerOn(value); - callback(); // Our fake Outlet is synchronous - this value has been successfully set - }); + FAKE_OUTLET.setPowerOn(value) + callback() // Our fake Outlet is synchronous - this value has been successfully set + }) // We want to intercept requests for our current power state so we can query the hardware itself instead of // allowing HAP-NodeJS to return the cached Characteristic.value. @@ -80,18 +81,17 @@ outlet .getService(Service.Outlet)! .getCharacteristic(Characteristic.On)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - // this event is emitted when you ask Siri directly whether your light is on or not. you might query // the light hardware itself to find this out, then call the callback. But if you take longer than a // few seconds to respond, Siri will give up. - const err = null; // in case there were any problems + const err = null // in case there were any problems if (FAKE_OUTLET.powerOn) { - console.log("Are we on? Yes."); - callback(err, true); + console.log('Are we on? Yes.') + callback(err, true) } else { - console.log("Are we on? No."); - callback(err, false); + console.log('Are we on? No.') + callback(err, false) } - }); + }) diff --git a/src/accessories/SmartSpeaker_accessory.ts b/src/accessories/SmartSpeaker_accessory.ts index ea49d602b..8bdf8405f 100644 --- a/src/accessories/SmartSpeaker_accessory.ts +++ b/src/accessories/SmartSpeaker_accessory.ts @@ -1,64 +1,62 @@ +/* eslint-disable no-console */ +import type { CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue } from '../index.js' + import { Accessory, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicGetCallback, - CharacteristicSetCallback, - CharacteristicValue, Service, uuid, -} from ".."; +} from '../index.js' -const speakerUUID = uuid.generate("hap-nodejs:accessories:smart-speaker"); -const speaker = exports.accessory = new Accessory("SmartSpeaker", speakerUUID); +const speakerUUID = uuid.generate('hap-nodejs:accessories:smart-speaker') +const speaker = exports.accessory = new Accessory('SmartSpeaker', speakerUUID) // @ts-expect-error: Core/BridgeCore API -speaker.username = "89:A8:E4:1E:95:EE"; +speaker.username = '89:A8:E4:1E:95:EE' // @ts-expect-error: Core/BridgeCore API -speaker.pincode = "676-54-344"; -speaker.category = Categories.SPEAKER; +speaker.pincode = '676-54-344' +speaker.category = Categories.SPEAKER -const service = new Service.SmartSpeaker("Smart Speaker", ""); +const service = new Service.SmartSpeaker('Smart Speaker', '') -let currentMediaState: number = Characteristic.CurrentMediaState.PAUSE; -let targetMediaState: number = Characteristic.TargetMediaState.PAUSE; +let currentMediaState: number = Characteristic.CurrentMediaState.PAUSE +let targetMediaState: number = Characteristic.TargetMediaState.PAUSE // ConfigureName is used to listen for Name changes inside the Home App. // A device manufacturer would probably need to adjust the name of the device in the AirPlay 2 protocol (or something) -service.setCharacteristic(Characteristic.ConfiguredName, "Smart Speaker"); -service.setCharacteristic(Characteristic.Mute, false); -service.setCharacteristic(Characteristic.Volume, 100); +service.setCharacteristic(Characteristic.ConfiguredName, 'Smart Speaker') +service.setCharacteristic(Characteristic.Mute, false) +service.setCharacteristic(Characteristic.Volume, 100) service.getCharacteristic(Characteristic.CurrentMediaState)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - console.log("Reading CurrentMediaState: " + currentMediaState); - callback(undefined, currentMediaState); + console.log(`Reading CurrentMediaState: ${currentMediaState}`) + callback(undefined, currentMediaState) }) - .updateValue(currentMediaState); // init value + .updateValue(currentMediaState) // init value service.getCharacteristic(Characteristic.TargetMediaState)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("Setting TargetMediaState to: " + value); - targetMediaState = value as number; - currentMediaState = targetMediaState; + console.log(`Setting TargetMediaState to: ${value}`) + targetMediaState = value as number + currentMediaState = targetMediaState - callback(); + callback() - service.setCharacteristic(Characteristic.CurrentMediaState, targetMediaState); + service.setCharacteristic(Characteristic.CurrentMediaState, targetMediaState) }) .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - console.log("Reading TargetMediaState: " + targetMediaState); - callback(undefined, targetMediaState); + console.log(`Reading TargetMediaState: ${targetMediaState}`) + callback(undefined, targetMediaState) }) - .updateValue(targetMediaState); + .updateValue(targetMediaState) service.getCharacteristic(Characteristic.ConfiguredName)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log(`Name was changed to: '${value}'`); - callback(); - }); - -speaker.addService(service); - + console.log(`Name was changed to: '${value}'`) + callback() + }) +speaker.addService(service) diff --git a/src/accessories/Sprinkler_accessory.ts b/src/accessories/Sprinkler_accessory.ts index 69139e435..52913a7fa 100644 --- a/src/accessories/Sprinkler_accessory.ts +++ b/src/accessories/Sprinkler_accessory.ts @@ -1,52 +1,51 @@ +/* eslint-disable no-console */ // here's a fake hardware device that we'll expose to HomeKit +import type { CharacteristicSetCallback, CharacteristicValue, NodeCallback } from '../index.js' + import { Accessory, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicSetCallback, - CharacteristicValue, - NodeCallback, Service, uuid, -} from ".."; +} from '../index.js' const SPRINKLER = { active: false, - name: "Garten Hinten", + name: 'Garten Hinten', timerEnd: 0, defaultDuration: 3600, motionDetected: false, getStatus: () => { - //set the boolean here, this will be returned to the device - SPRINKLER.motionDetected = false; + // set the boolean here, this will be returned to the device + SPRINKLER.motionDetected = false }, identify: () => { - console.log("Identify the sprinkler!"); + console.log('Identify the sprinkler!') }, -}; - +} // Generate a consistent UUID for our Motion Sensor Accessory that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the word "motionsensor". -const sprinklerUUID = uuid.generate("hap-nodejs:accessories:sprinkler"); +const sprinklerUUID = uuid.generate('hap-nodejs:accessories:sprinkler') // This is the Accessory that we'll return to HAP-NodeJS that represents our fake motionSensor. -const sprinkler = exports.accessory = new Accessory("Sprinkler", sprinklerUUID); +const sprinkler = exports.accessory = new Accessory('Sprinkler', sprinklerUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -sprinkler.username = "A3:AB:3D:4D:2E:A3"; +sprinkler.username = 'A3:AB:3D:4D:2E:A3' // @ts-expect-error: Core/BridgeCore API -sprinkler.pincode = "123-44-567"; -sprinkler.category = Categories.SPRINKLER; +sprinkler.pincode = '123-44-567' +sprinkler.category = Categories.SPRINKLER // Add the actual Valve Service and listen for change events from iOS. -const sprinklerService = sprinkler.addService(Service.Valve, "Sprinkler"); +const sprinklerService = sprinkler.addService(Service.Valve, 'Sprinkler') -// Sprinkler Controll +// Sprinkler Control function openVentile() { // Add your code here } @@ -57,106 +56,98 @@ function closeVentile() { // set some basic properties (these values are arbitrary and setting them is optional) sprinklerService - .setCharacteristic(Characteristic.ValveType, "1") // IRRIGATION/SPRINKLER = 1; SHOWER_HEAD = 2; WATER_FAUCET = 3; - .setCharacteristic(Characteristic.Name, SPRINKLER.name); + .setCharacteristic(Characteristic.ValveType, '1') // IRRIGATION/SPRINKLER = 1; SHOWER_HEAD = 2; WATER_FAUCET = 3; + .setCharacteristic(Characteristic.Name, SPRINKLER.name) sprinklerService .getCharacteristic(Characteristic.Active)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - - console.log("get Active"); - const err = null; // in case there were any problems + console.log('get Active') + const err = null // in case there were any problems if (SPRINKLER.active) { - callback(err, true); + callback(err, true) } else { - callback(err, false); + callback(err, false) } }) .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - - console.log("set Active => setNewValue: " + newValue); + console.log(`set Active => setNewValue: ${newValue}`) if (SPRINKLER.active) { - SPRINKLER.active = false; - closeVentile(); + SPRINKLER.active = false + closeVentile() setTimeout(() => { - console.log("Ausgeschaltet"); - SPRINKLER.timerEnd = SPRINKLER.defaultDuration + Math.floor(new Date().getTime() / 1000); - callback(null); + console.log('Ausgeschaltet') + SPRINKLER.timerEnd = SPRINKLER.defaultDuration + Math.floor(new Date().getTime() / 1000) + callback(null) sprinkler .getService(Service.Valve)! - .setCharacteristic(Characteristic.SetDuration, 0); + .setCharacteristic(Characteristic.SetDuration, 0) sprinkler .getService(Service.Valve)! - .setCharacteristic(Characteristic.InUse, 0); - - }, 1000); + .setCharacteristic(Characteristic.InUse, 0) + }, 1000) } else { - SPRINKLER.active = true; - openVentile(); + SPRINKLER.active = true + openVentile() setTimeout(() => { - console.log("Eingeschaltet"); - SPRINKLER.timerEnd = SPRINKLER.defaultDuration + Math.floor(new Date().getTime() / 1000); - callback(null, SPRINKLER.defaultDuration); + console.log('Eingeschaltet') + SPRINKLER.timerEnd = SPRINKLER.defaultDuration + Math.floor(new Date().getTime() / 1000) + callback(null, SPRINKLER.defaultDuration) sprinkler .getService(Service.Valve)! - .setCharacteristic(Characteristic.InUse, 1); + .setCharacteristic(Characteristic.InUse, 1) sprinkler .getService(Service.Valve)! - .setCharacteristic(Characteristic.RemainingDuration, SPRINKLER.defaultDuration); + .setCharacteristic(Characteristic.RemainingDuration, SPRINKLER.defaultDuration) sprinkler .getService(Service.Valve)! - .setCharacteristic(Characteristic.SetDuration, SPRINKLER.defaultDuration); - - }, 1000); + .setCharacteristic(Characteristic.SetDuration, SPRINKLER.defaultDuration) + }, 1000) } - }); - + }) sprinklerService .getCharacteristic(Characteristic.InUse)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - console.log("get In_Use"); - const err = null; // in case there were any problems + console.log('get In_Use') + const err = null // in case there were any problems if (SPRINKLER.active) { - callback(err, true); + callback(err, true) } else { - callback(err, false); + callback(err, false) } }) .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set In_Use => NewValue: " + newValue); - callback(); - }); - + console.log(`set In_Use => NewValue: ${newValue}`) + callback() + }) sprinklerService .getCharacteristic(Characteristic.RemainingDuration)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - - const err = null; // in case there were any problems + const err = null // in case there were any problems if (SPRINKLER.active) { - - const duration = SPRINKLER.timerEnd - Math.floor(new Date().getTime() / 1000); - console.log("RemainingDuration: " + duration); - callback(err, duration); + const duration = SPRINKLER.timerEnd - Math.floor(new Date().getTime() / 1000) + console.log(`RemainingDuration: ${duration}`) + callback(err, duration) } else { - callback(err, 0); + callback(err, 0) } - }); + }) sprinklerService .getCharacteristic(Characteristic.SetDuration)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("SetDuration => NewValue: " + newValue); - SPRINKLER.defaultDuration = newValue as number; - callback(); - }); + console.log(`SetDuration => NewValue: ${newValue}`) + SPRINKLER.defaultDuration = newValue as number + callback() + }) diff --git a/src/accessories/TV_accessory.ts b/src/accessories/TV_accessory.ts index 7fdf2ccc6..af9ebd6a3 100644 --- a/src/accessories/TV_accessory.ts +++ b/src/accessories/TV_accessory.ts @@ -1,140 +1,140 @@ +/* eslint-disable no-console */ +import type { AccessLevel, CharacteristicSetCallback, CharacteristicValue } from '../index.js' + import { AccessControlEvent, AccessControlManagement, - AccessLevel, Accessory, Categories, Characteristic, CharacteristicEventTypes, - CharacteristicSetCallback, - CharacteristicValue, Service, uuid, -} from ".."; +} from '../index.js' // Generate a consistent UUID for TV that will remain the same even when // restarting our server. We use the `uuid.generate` helper function to create a deterministic // UUID based on an arbitrary "namespace" and the word "tv". -const tvUUID = uuid.generate("hap-nodejs:accessories:tv"); +const tvUUID = uuid.generate('hap-nodejs:accessories:tv') // This is the Accessory that we'll return to HAP-NodeJS. -const tv = exports.accessory = new Accessory("TV", tvUUID); +const tv = exports.accessory = new Accessory('TV', tvUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -tv.username = "A3:FB:3D:4D:2E:AC"; +tv.username = 'A3:FB:3D:4D:2E:AC' // @ts-expect-error: Core/BridgeCore API -tv.pincode = "031-45-154"; -tv.category = Categories.TELEVISION; +tv.pincode = '031-45-154' +tv.category = Categories.TELEVISION // Add the actual TV Service and listen for change events from iOS. -const televisionService = tv.addService(Service.Television, "Television", "Television"); +const televisionService = tv.addService(Service.Television, 'Television', 'Television') televisionService - .setCharacteristic(Characteristic.ConfiguredName, "Television"); + .setCharacteristic(Characteristic.ConfiguredName, 'Television') televisionService .setCharacteristic( Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE, - ); + ) televisionService .getCharacteristic(Characteristic.Active)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set Active => setNewValue: " + newValue); - callback(null); - }); + console.log(`set Active => setNewValue: ${newValue}`) + callback(null) + }) televisionService - .setCharacteristic(Characteristic.ActiveIdentifier, 1); + .setCharacteristic(Characteristic.ActiveIdentifier, 1) televisionService .getCharacteristic(Characteristic.ActiveIdentifier)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set Active Identifier => setNewValue: " + newValue); - callback(null); - }); + console.log(`set Active Identifier => setNewValue: ${newValue}`) + callback(null) + }) televisionService .getCharacteristic(Characteristic.RemoteKey)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set Remote Key => setNewValue: " + newValue); - callback(null); - }); + console.log(`set Remote Key => setNewValue: ${newValue}`) + callback(null) + }) televisionService .getCharacteristic(Characteristic.PictureMode)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set PictureMode => setNewValue: " + newValue); - callback(null); - }); + console.log(`set PictureMode => setNewValue: ${newValue}`) + callback(null) + }) televisionService .getCharacteristic(Characteristic.PowerModeSelection)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set PowerModeSelection => setNewValue: " + newValue); - callback(null); - }); + console.log(`set PowerModeSelection => setNewValue: ${newValue}`) + callback(null) + }) // Speaker -const speakerService = tv.addService(Service.TelevisionSpeaker); +const speakerService = tv.addService(Service.TelevisionSpeaker) speakerService .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) - .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); + .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE) speakerService.getCharacteristic(Characteristic.VolumeSelector)! .on(CharacteristicEventTypes.SET, (newValue: CharacteristicValue, callback: CharacteristicSetCallback) => { - console.log("set VolumeSelector => setNewValue: " + newValue); - callback(null); - }); + console.log(`set VolumeSelector => setNewValue: ${newValue}`) + callback(null) + }) // HDMI 1 -const inputHDMI1 = tv.addService(Service.InputSource, "hdmi1", "HDMI 1"); +const inputHDMI1 = tv.addService(Service.InputSource, 'hdmi1', 'HDMI 1') inputHDMI1 .setCharacteristic(Characteristic.Identifier, 1) - .setCharacteristic(Characteristic.ConfiguredName, "HDMI 1") + .setCharacteristic(Characteristic.ConfiguredName, 'HDMI 1') .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) - .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI); + .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI) // HDMI 2 -const inputHDMI2 = tv.addService(Service.InputSource, "hdmi2", "HDMI 2"); +const inputHDMI2 = tv.addService(Service.InputSource, 'hdmi2', 'HDMI 2') inputHDMI2 .setCharacteristic(Characteristic.Identifier, 2) - .setCharacteristic(Characteristic.ConfiguredName, "HDMI 2") + .setCharacteristic(Characteristic.ConfiguredName, 'HDMI 2') .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) - .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI); + .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI) // Netflix -const inputNetflix = tv.addService(Service.InputSource, "netflix", "Netflix"); +const inputNetflix = tv.addService(Service.InputSource, 'netflix', 'Netflix') inputNetflix .setCharacteristic(Characteristic.Identifier, 3) - .setCharacteristic(Characteristic.ConfiguredName, "Netflix") + .setCharacteristic(Characteristic.ConfiguredName, 'Netflix') .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) - .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION); + .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION) -televisionService.addLinkedService(inputHDMI1); -televisionService.addLinkedService(inputHDMI2); -televisionService.addLinkedService(inputNetflix); +televisionService.addLinkedService(inputHDMI1) +televisionService.addLinkedService(inputHDMI2) +televisionService.addLinkedService(inputNetflix) -const accessControl = new AccessControlManagement(true); +const accessControl = new AccessControlManagement(true) accessControl.on(AccessControlEvent.ACCESS_LEVEL_UPDATED, (level: AccessLevel) => { - console.log("New access control level of " + level); -}); + console.log(`New access control level of ${level}`) +}) accessControl.on(AccessControlEvent.PASSWORD_SETTING_UPDATED, (password: string | undefined, passwordRequired: boolean) => { if (passwordRequired) { - console.log("A required password was specified"); + console.log('A required password was specified') } else { - console.log("No password set!"); + console.log('No password set!') } -}); +}) -tv.addService(accessControl.getService()); +tv.addService(accessControl.getService()) diff --git a/src/accessories/TemperatureSensor_accessory.ts b/src/accessories/TemperatureSensor_accessory.ts index 6c4cdca61..6f0876ff5 100644 --- a/src/accessories/TemperatureSensor_accessory.ts +++ b/src/accessories/TemperatureSensor_accessory.ts @@ -1,52 +1,51 @@ +/* eslint-disable no-console */ // here's a fake temperature sensor device that we'll expose to HomeKit -import { Accessory, Categories, Characteristic, CharacteristicEventTypes, CharacteristicValue, NodeCallback, Service, uuid } from ".."; +import type { CharacteristicValue, NodeCallback } from '../index.js' + +import { Accessory, Categories, Characteristic, CharacteristicEventTypes, Service, uuid } from '../index.js' const FAKE_SENSOR = { currentTemperature: 50, - getTemperature: function () { - console.log("Getting the current temperature!"); - return FAKE_SENSOR.currentTemperature; + getTemperature() { + console.log('Getting the current temperature!') + return FAKE_SENSOR.currentTemperature }, - randomizeTemperature: function () { + randomizeTemperature() { // randomize temperature to a value between 0 and 100 - FAKE_SENSOR.currentTemperature = Math.round(Math.random() * 100); + FAKE_SENSOR.currentTemperature = Math.round(Math.random() * 100) }, -}; - +} // Generate a consistent UUID for our Temperature Sensor Accessory that will remain the same // even when restarting our server. We use the `uuid.generate` helper function to create // a deterministic UUID based on an arbitrary "namespace" and the string "temperature-sensor". -const sensorUUID = uuid.generate("hap-nodejs:accessories:temperature-sensor"); +const sensorUUID = uuid.generate('hap-nodejs:accessories:temperature-sensor') // This is the Accessory that we'll return to HAP-NodeJS that represents our fake lock. -const sensor = exports.accessory = new Accessory("Temperature Sensor", sensorUUID); +const sensor = exports.accessory = new Accessory('Temperature Sensor', sensorUUID) // Add properties for publishing (in case we're using Core.js and not BridgedCore.js) // @ts-expect-error: Core/BridgeCore API -sensor.username = "C1:5D:3A:AE:5E:FA"; +sensor.username = 'C1:5D:3A:AE:5E:FA' // @ts-expect-error: Core/BridgeCore API -sensor.pincode = "031-45-154"; -sensor.category = Categories.SENSOR; +sensor.pincode = '031-45-154' +sensor.category = Categories.SENSOR // Add the actual TemperatureSensor Service. sensor .addService(Service.TemperatureSensor)! .getCharacteristic(Characteristic.CurrentTemperature)! .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - // return our current value - callback(null, FAKE_SENSOR.getTemperature()); - }); + callback(null, FAKE_SENSOR.getTemperature()) + }) // randomize our temperature reading every 3 seconds setInterval(() => { - - FAKE_SENSOR.randomizeTemperature(); + FAKE_SENSOR.randomizeTemperature() // update the characteristic value so interested iOS devices can get notified sensor .getService(Service.TemperatureSensor)! - .setCharacteristic(Characteristic.CurrentTemperature, FAKE_SENSOR.currentTemperature); - -}, 3000); + .setCharacteristic(Characteristic.CurrentTemperature, FAKE_SENSOR.currentTemperature) +}, 3000) diff --git a/src/accessories/Wi-FiRouter_accessory.ts b/src/accessories/Wi-FiRouter_accessory.ts index cd8290336..cb252a963 100644 --- a/src/accessories/Wi-FiRouter_accessory.ts +++ b/src/accessories/Wi-FiRouter_accessory.ts @@ -1,17 +1,20 @@ -import { Accessory, AccessoryEventTypes, Categories, Service, uuid, VoidCallback } from ".."; +/* eslint-disable no-console */ +import type { VoidCallback } from '../index.js' -const UUID = uuid.generate("hap-nodejs:accessories:wifi-router"); -export const accessory = new Accessory("Wi-Fi Router", UUID); +import { Accessory, AccessoryEventTypes, Categories, Service, uuid } from '../index.js' + +const UUID = uuid.generate('hap-nodejs:accessories:wifi-router') +export const accessory = new Accessory('Wi-Fi Router', UUID) // @ts-expect-error: Core/BridgeCore API -accessory.username = "FA:3C:ED:D2:1A:A2"; +accessory.username = 'FA:3C:ED:D2:1A:A2' // @ts-expect-error: Core/BridgeCore API -accessory.pincode = "031-45-154"; -accessory.category = Categories.ROUTER; +accessory.pincode = '031-45-154' +accessory.category = Categories.ROUTER accessory.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - console.log("Identify the '%s'", accessory.displayName); - callback(); -}); + console.log('Identify the \'%s\'', accessory.displayName) + callback() +}) -accessory.addService(Service.WiFiRouter); +accessory.addService(Service.WiFiRouter) diff --git a/src/accessories/Wi-FiSatellite_accessory.ts b/src/accessories/Wi-FiSatellite_accessory.ts index 7d91783ca..6a5efab16 100644 --- a/src/accessories/Wi-FiSatellite_accessory.ts +++ b/src/accessories/Wi-FiSatellite_accessory.ts @@ -1,20 +1,23 @@ -import { Accessory, AccessoryEventTypes, Categories, Characteristic, Service, uuid, VoidCallback } from ".."; +/* eslint-disable no-console */ +import type { VoidCallback } from '../index.js' -const UUID = uuid.generate("hap-nodejs:accessories:wifi-satellite"); -export const accessory = new Accessory("Wi-Fi Satellite", UUID); +import { Accessory, AccessoryEventTypes, Categories, Characteristic, Service, uuid } from '../index.js' + +const UUID = uuid.generate('hap-nodejs:accessories:wifi-satellite') +export const accessory = new Accessory('Wi-Fi Satellite', UUID) // @ts-expect-error: Core/BridgeCore API -accessory.username = "FA:3C:ED:5A:1A:A2"; +accessory.username = 'FA:3C:ED:5A:1A:A2' // @ts-expect-error: Core/BridgeCore API -accessory.pincode = "031-45-154"; -accessory.category = Categories.ROUTER; +accessory.pincode = '031-45-154' +accessory.category = Categories.ROUTER accessory.on(AccessoryEventTypes.IDENTIFY, (paired: boolean, callback: VoidCallback) => { - console.log("Identify the '%s'", accessory.displayName); - callback(); -}); + console.log('Identify the \'%s\'', accessory.displayName) + callback() +}) -const satellite = accessory.addService(Service.WiFiSatellite); +const satellite = accessory.addService(Service.WiFiSatellite) satellite.getCharacteristic(Characteristic.WiFiSatelliteStatus)! - .updateValue(Characteristic.WiFiSatelliteStatus.CONNECTED); + .updateValue(Characteristic.WiFiSatelliteStatus.CONNECTED) diff --git a/src/accessories/gstreamer-audioProducer.ts b/src/accessories/gstreamer-audioProducer.ts index 2c5e46952..91fa1d66e 100644 --- a/src/accessories/gstreamer-audioProducer.ts +++ b/src/accessories/gstreamer-audioProducer.ts @@ -1,40 +1,52 @@ -import assert from "assert"; -import { ChildProcess, spawn } from "child_process"; -import createDebug from "debug"; +import type { Buffer } from 'node:buffer' +import type { ChildProcess } from 'node:child_process' + +import type { + AudioCodecConfiguration, + ErrorHandler, + FrameHandler, + SiriAudioStreamProducer, +} from '../index.js' + +import assert from 'node:assert' +import { spawn } from 'node:child_process' +import process from 'node:process' + +import createDebug from 'debug' + import { AudioBitrate, - AudioCodecConfiguration, AudioCodecTypes, AudioSamplerate, - ErrorHandler, - FrameHandler, HDSProtocolSpecificErrorReason, - SiriAudioStreamProducer, -} from ".."; +} from '../index.js' -const debug = createDebug("HAP-NodeJS:Remote:GStreamer"); +const debug = createDebug('HAP-NodeJS:Remote:GStreamer') +// eslint-disable-next-line no-restricted-syntax const enum AudioType { - GENERIC = 2049, - VOICE = 2048 + GENERIC = 2049, + VOICE = 2048, } +// eslint-disable-next-line no-restricted-syntax const enum Bandwidth { - NARROW_BAND = 1101, - MEDIUM_BAND = 1102, - WIDE_BAND = 1103, - SUPER_WIDE_BAND = 1104, - FULL_BAND = 1105, - AUTO = -1000 + NARROW_BAND = 1101, + MEDIUM_BAND = 1102, + WIDE_BAND = 1103, + SUPER_WIDE_BAND = 1104, + FULL_BAND = 1105, + AUTO = -1000, } +// eslint-disable-next-line no-restricted-syntax const enum BitrateType { - CONSTANT = 0, - VARIABLE = 1, + CONSTANT = 0, + VARIABLE = 1, } -export type GStreamerOptions = { - alsaSrc: string, +export interface GStreamerOptions { + alsaSrc: string } /** @@ -48,89 +60,88 @@ export type GStreamerOptions = { * */ export class GStreamerAudioProducer implements SiriAudioStreamProducer { - private readonly options: GStreamerOptions = { - alsaSrc: "plughw:1", - }; + alsaSrc: 'plughw:1', + } - private readonly frameHandler: FrameHandler; - private readonly errorHandler: ErrorHandler; + private readonly frameHandler: FrameHandler + private readonly errorHandler: ErrorHandler - private process?: ChildProcess; - private running = false; + private process?: ChildProcess + private running = false constructor(frameHandler: FrameHandler, errorHandler: ErrorHandler, options?: Partial) { - this.frameHandler = frameHandler; - this.errorHandler = errorHandler; + this.frameHandler = frameHandler + this.errorHandler = errorHandler if (options) { - for (const [ key, value ] of Object.entries(options)) { + for (const [key, value] of Object.entries(options)) { // @ts-expect-error: type mismatch - GStreamerAudioProducer.options[key] = value; + GStreamerAudioProducer.options[key] = value } } } startAudioProduction(selectedAudioConfiguration: AudioCodecConfiguration): void { if (this.running) { - throw new Error("Gstreamer already running"); + throw new Error('Gstreamer already running') } - const codecParameters = selectedAudioConfiguration.parameters; - assert(selectedAudioConfiguration.codecType === AudioCodecTypes.OPUS); + const codecParameters = selectedAudioConfiguration.parameters + assert(selectedAudioConfiguration.codecType === AudioCodecTypes.OPUS) - let bitrateType = BitrateType.VARIABLE; + let bitrateType = BitrateType.VARIABLE switch (codecParameters.bitrate) { - case AudioBitrate.CONSTANT: - bitrateType = BitrateType.CONSTANT; - break; - case AudioBitrate.VARIABLE: - bitrateType = BitrateType.VARIABLE; - break; + case AudioBitrate.CONSTANT: + bitrateType = BitrateType.CONSTANT + break + case AudioBitrate.VARIABLE: + bitrateType = BitrateType.VARIABLE + break } - let bandwidth = Bandwidth.SUPER_WIDE_BAND; + let bandwidth = Bandwidth.SUPER_WIDE_BAND switch (codecParameters.samplerate) { - case AudioSamplerate.KHZ_8: - bandwidth = Bandwidth.NARROW_BAND; - break; - case AudioSamplerate.KHZ_16: - bandwidth = Bandwidth.WIDE_BAND; - break; - case AudioSamplerate.KHZ_24: - bandwidth = Bandwidth.SUPER_WIDE_BAND; - break; + case AudioSamplerate.KHZ_8: + bandwidth = Bandwidth.NARROW_BAND + break + case AudioSamplerate.KHZ_16: + bandwidth = Bandwidth.WIDE_BAND + break + case AudioSamplerate.KHZ_24: + bandwidth = Bandwidth.SUPER_WIDE_BAND + break } - const packetTime = codecParameters.rtpTime; - - debug("Launching gstreamer..."); - this.running = true; - - const args = "-q " + - "alsasrc device=" + this.options.alsaSrc + " ! " + - "capsfilter caps=audio/x-raw,format=S16LE,rate=24000 ! " + - // "level post-messages=true interval=" + packetTime + "000000 ! " + // used to capture rms - "opusenc " + - "bitrate-type=" + bitrateType + " " + - "bitrate=24000 " + - "audio-type=" + AudioType.VOICE + " " + - "bandwidth=" + bandwidth + " " + - "frame-size=" + packetTime + " ! " + - "fdsink fd=1"; - - this.process = spawn("gst-launch-1.0", args.split(" "), { env: process.env }); - this.process.on("error", error => { + const packetTime = codecParameters.rtpTime + + debug('Launching gstreamer...') + this.running = true + + const args = `-q ` + + `alsasrc device=${this.options.alsaSrc} ! ` + + `capsfilter caps=audio/x-raw,format=S16LE,rate=24000 ! ` + // "level post-messages=true interval=" + packetTime + "000000 ! " + // used to capture rms + + `opusenc ` + + `bitrate-type=${bitrateType} ` + + `bitrate=24000 ` + + `audio-type=${AudioType.VOICE} ` + + `bandwidth=${bandwidth} ` + + `frame-size=${packetTime} ! ` + + `fdsink fd=1` + + this.process = spawn('gst-launch-1.0', args.split(' '), { env: process.env }) + this.process.on('error', (error) => { if (this.running) { - debug("Failed to spawn gstreamer process: " + error.message); - this.errorHandler(HDSProtocolSpecificErrorReason.CANCELLED); + debug(`Failed to spawn gstreamer process: ${error.message}`) + this.errorHandler(HDSProtocolSpecificErrorReason.CANCELLED) } else { - debug("Failed to kill gstreamer process: " + error.message); + debug(`Failed to kill gstreamer process: ${error.message}`) } - }); - this.process.stdout?.on("data", (data: Buffer) => { + }) + this.process.stdout?.on('data', (data: Buffer) => { if (!this.running) { // received data after it was closed - return; + return } /* @@ -140,33 +151,32 @@ export class GStreamerAudioProducer implements SiriAudioStreamProducer { Opus relies on the container format to specify the length of the frame. Although sometimes multiple opus frames are squashed together the decoder seems to be able to handle that as it just creates a not very noticeable distortion. - If we wanted to make this perfect we would need to write a nodejs c++ submodule or something + If we wanted to make this perfect we would need to write a Node.js c++ submodule or something to interface directly with gstreamer api. */ this.frameHandler({ - data: data, - rms: 0.25, // only way currently to extract rms from gstreamer is by interfacing with the api directly (nodejs c++ submodule could be a solution) - }); - }); - this.process.stderr?.on("data", data => { - debug("GStreamer process reports the following error: " + String(data)); - }); - this.process.on("exit", (code, signal) => { - if (signal !== "SIGTERM") { // if we receive SIGTERM, process exited gracefully (we stopped it) - debug("GStreamer process unexpectedly exited with code %d (signal: %s)", code, signal); - this.errorHandler(HDSProtocolSpecificErrorReason.UNEXPECTED_FAILURE); + data, + rms: 0.25, // only way currently to extract rms from gstreamer is by interfacing with the api directly (Node.js c++ submodule could be a solution) + }) + }) + this.process.stderr?.on('data', (data) => { + debug(`GStreamer process reports the following error: ${String(data)}`) + }) + this.process.on('exit', (code, signal) => { + if (signal !== 'SIGTERM') { // if we receive SIGTERM, process exited gracefully (we stopped it) + debug('GStreamer process unexpectedly exited with code %d (signal: %s)', code, signal) + this.errorHandler(HDSProtocolSpecificErrorReason.UNEXPECTED_FAILURE) } - }); + }) } stopAudioProduction(): void { if (this.running) { - this.process!.kill("SIGTERM"); - this.running = false; + this.process!.kill('SIGTERM') + this.running = false } - this.process = undefined; + this.process = undefined } - } diff --git a/src/accessories/types.ts b/src/accessories/types.ts index 91f5f7efb..71083db67 100644 --- a/src/accessories/types.ts +++ b/src/accessories/types.ts @@ -1,91 +1,89 @@ -//HomeKit Types UUID's +// HomeKit Types UUID's -const stPre = "000000"; -const stPost = "-0000-1000-8000-0026BB765291"; +const stPre = '000000' +const stPost = '-0000-1000-8000-0026BB765291' +// HomeKitTransportCategoryTypes +export const OTHER_TCTYPE = 1 +export const FAN_TCTYPE = 3 +export const GARAGE_DOOR_OPENER_TCTYPE = 4 +export const LIGHTBULB_TCTYPE = 5 +export const DOOR_LOCK_TCTYPE = 6 +export const OUTLET_TCTYPE = 7 +export const SWITCH_TCTYPE = 8 +export const THERMOSTAT_TCTYPE = 9 +export const SENSOR_TCTYPE = 10 +export const ALARM_SYSTEM_TCTYPE = 11 +export const DOOR_TCTYPE = 12 +export const WINDOW_TCTYPE = 13 +export const WINDOW_COVERING_TCTYPE = 14 +export const PROGRAMMABLE_SWITCH_TCTYPE = 15 -//HomeKitTransportCategoryTypes -export const OTHER_TCTYPE = 1; -export const FAN_TCTYPE = 3; -export const GARAGE_DOOR_OPENER_TCTYPE = 4; -export const LIGHTBULB_TCTYPE = 5; -export const DOOR_LOCK_TCTYPE = 6; -export const OUTLET_TCTYPE = 7; -export const SWITCH_TCTYPE = 8; -export const THERMOSTAT_TCTYPE = 9; -export const SENSOR_TCTYPE = 10; -export const ALARM_SYSTEM_TCTYPE = 11; -export const DOOR_TCTYPE = 12; -export const WINDOW_TCTYPE = 13; -export const WINDOW_COVERING_TCTYPE = 14; -export const PROGRAMMABLE_SWITCH_TCTYPE = 15; +// HomeKitServiceTypes -//HomeKitServiceTypes +export const LIGHTBULB_STYPE = `${stPre}43${stPost}` +export const SWITCH_STYPE = `${stPre}49${stPost}` +export const THERMOSTAT_STYPE = `${stPre}4A${stPost}` +export const GARAGE_DOOR_OPENER_STYPE = `${stPre}41${stPost}` +export const ACCESSORY_INFORMATION_STYPE = `${stPre}3E${stPost}` +export const FAN_STYPE = `${stPre}40${stPost}` +export const OUTLET_STYPE = `${stPre}47${stPost}` +export const LOCK_MECHANISM_STYPE = `${stPre}45${stPost}` +export const LOCK_MANAGEMENT_STYPE = `${stPre}44${stPost}` +export const ALARM_STYPE = `${stPre}7E${stPost}` +export const WINDOW_COVERING_STYPE = `${stPre}8C${stPost}` +export const OCCUPANCY_SENSOR_STYPE = `${stPre}86${stPost}` +export const CONTACT_SENSOR_STYPE = `${stPre}80${stPost}` +export const MOTION_SENSOR_STYPE = `${stPre}85${stPost}` +export const HUMIDITY_SENSOR_STYPE = `${stPre}82${stPost}` +export const TEMPERATURE_SENSOR_STYPE = `${stPre}8A${stPost}` -export const LIGHTBULB_STYPE = stPre + "43" + stPost; -export const SWITCH_STYPE = stPre + "49" + stPost; -export const THERMOSTAT_STYPE = stPre + "4A" + stPost; -export const GARAGE_DOOR_OPENER_STYPE = stPre + "41" + stPost; -export const ACCESSORY_INFORMATION_STYPE = stPre + "3E" + stPost; -export const FAN_STYPE = stPre + "40" + stPost; -export const OUTLET_STYPE = stPre + "47" + stPost; -export const LOCK_MECHANISM_STYPE = stPre + "45" + stPost; -export const LOCK_MANAGEMENT_STYPE = stPre + "44" + stPost; -export const ALARM_STYPE = stPre + "7E" + stPost; -export const WINDOW_COVERING_STYPE = stPre + "8C" + stPost; -export const OCCUPANCY_SENSOR_STYPE = stPre + "86" + stPost; -export const CONTACT_SENSOR_STYPE = stPre + "80" + stPost; -export const MOTION_SENSOR_STYPE = stPre + "85" + stPost; -export const HUMIDITY_SENSOR_STYPE = stPre + "82" + stPost; -export const TEMPERATURE_SENSOR_STYPE = stPre + "8A" + stPost; +// HomeKitCharacteristicsTypes -//HomeKitCharacteristicsTypes - - -export const ALARM_CURRENT_STATE_CTYPE = stPre + "66" + stPost; -export const ALARM_TARGET_STATE_CTYPE = stPre + "67" + stPost; -export const ADMIN_ONLY_ACCESS_CTYPE = stPre + "01" + stPost; -export const AUDIO_FEEDBACK_CTYPE = stPre + "05" + stPost; -export const BRIGHTNESS_CTYPE = stPre + "08" + stPost; -export const BATTERY_LEVEL_CTYPE = stPre + "68" + stPost; -export const COOLING_THRESHOLD_CTYPE = stPre + "0D" + stPost; -export const CONTACT_SENSOR_STATE_CTYPE = stPre + "6A" + stPost; -export const CURRENT_DOOR_STATE_CTYPE = stPre + "0E" + stPost; -export const CURRENT_LOCK_MECHANISM_STATE_CTYPE = stPre + "1D" + stPost; -export const CURRENT_RELATIVE_HUMIDITY_CTYPE = stPre + "10" + stPost; -export const CURRENT_TEMPERATURE_CTYPE = stPre + "11" + stPost; -export const HEATING_THRESHOLD_CTYPE = stPre + "12" + stPost; -export const HUE_CTYPE = stPre + "13" + stPost; -export const IDENTIFY_CTYPE = stPre + "14" + stPost; -export const LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE = stPre + "1A" + stPost; -export const LOCK_MANAGEMENT_CONTROL_POINT_CTYPE = stPre + "19" + stPost; -export const LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE = stPre + "1C" + stPost; -export const LOGS_CTYPE = stPre + "1F" + stPost; -export const MANUFACTURER_CTYPE = stPre + "20" + stPost; -export const MODEL_CTYPE = stPre + "21" + stPost; -export const MOTION_DETECTED_CTYPE = stPre + "22" + stPost; -export const NAME_CTYPE = stPre + "23" + stPost; -export const OBSTRUCTION_DETECTED_CTYPE = stPre + "24" + stPost; -export const OUTLET_IN_USE_CTYPE = stPre + "26" + stPost; -export const OCCUPANCY_DETECTED_CTYPE = stPre + "71" + stPost; -export const POWER_STATE_CTYPE = stPre + "25" + stPost; -export const PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE = stPre + "73" + stPost; -export const PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE = stPre + "74" + stPost; -export const ROTATION_DIRECTION_CTYPE = stPre + "28" + stPost; -export const ROTATION_SPEED_CTYPE = stPre + "29" + stPost; -export const SATURATION_CTYPE = stPre + "2F" + stPost; -export const SERIAL_NUMBER_CTYPE = stPre + "30" + stPost; -export const FIRMWARE_REVISION_CTYPE = stPre + "52" + stPost; -export const STATUS_LOW_BATTERY_CTYPE = stPre + "79" + stPost; -export const STATUS_FAULT_CTYPE = stPre + "77" + stPost; -export const TARGET_DOORSTATE_CTYPE = stPre + "32" + stPost; -export const TARGET_LOCK_MECHANISM_STATE_CTYPE = stPre + "1E" + stPost; -export const TARGET_RELATIVE_HUMIDITY_CTYPE = stPre + "34" + stPost; -export const TARGET_TEMPERATURE_CTYPE = stPre + "35" + stPost; -export const TEMPERATURE_UNITS_CTYPE = stPre + "36" + stPost; -export const VERSION_CTYPE = stPre + "37" + stPost; -export const WINDOW_COVERING_TARGET_POSITION_CTYPE = stPre + "7C" + stPost; -export const WINDOW_COVERING_CURRENT_POSITION_CTYPE = stPre + "6D" + stPost; -export const WINDOW_COVERING_OPERATION_STATE_CTYPE = stPre + "72" + stPost; -export const CURRENTHEATINGCOOLING_CTYPE = stPre + "0F" + stPost; -export const TARGETHEATINGCOOLING_CTYPE = stPre + "33" + stPost; +export const ALARM_CURRENT_STATE_CTYPE = `${stPre}66${stPost}` +export const ALARM_TARGET_STATE_CTYPE = `${stPre}67${stPost}` +export const ADMIN_ONLY_ACCESS_CTYPE = `${stPre}01${stPost}` +export const AUDIO_FEEDBACK_CTYPE = `${stPre}05${stPost}` +export const BRIGHTNESS_CTYPE = `${stPre}08${stPost}` +export const BATTERY_LEVEL_CTYPE = `${stPre}68${stPost}` +export const COOLING_THRESHOLD_CTYPE = `${stPre}0D${stPost}` +export const CONTACT_SENSOR_STATE_CTYPE = `${stPre}6A${stPost}` +export const CURRENT_DOOR_STATE_CTYPE = `${stPre}0E${stPost}` +export const CURRENT_LOCK_MECHANISM_STATE_CTYPE = `${stPre}1D${stPost}` +export const CURRENT_RELATIVE_HUMIDITY_CTYPE = `${stPre}10${stPost}` +export const CURRENT_TEMPERATURE_CTYPE = `${stPre}11${stPost}` +export const HEATING_THRESHOLD_CTYPE = `${stPre}12${stPost}` +export const HUE_CTYPE = `${stPre}13${stPost}` +export const IDENTIFY_CTYPE = `${stPre}14${stPost}` +export const LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE = `${stPre}1A${stPost}` +export const LOCK_MANAGEMENT_CONTROL_POINT_CTYPE = `${stPre}19${stPost}` +export const LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE = `${stPre}1C${stPost}` +export const LOGS_CTYPE = `${stPre}1F${stPost}` +export const MANUFACTURER_CTYPE = `${stPre}20${stPost}` +export const MODEL_CTYPE = `${stPre}21${stPost}` +export const MOTION_DETECTED_CTYPE = `${stPre}22${stPost}` +export const NAME_CTYPE = `${stPre}23${stPost}` +export const OBSTRUCTION_DETECTED_CTYPE = `${stPre}24${stPost}` +export const OUTLET_IN_USE_CTYPE = `${stPre}26${stPost}` +export const OCCUPANCY_DETECTED_CTYPE = `${stPre}71${stPost}` +export const POWER_STATE_CTYPE = `${stPre}25${stPost}` +export const PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE = `${stPre}73${stPost}` +export const PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE = `${stPre}74${stPost}` +export const ROTATION_DIRECTION_CTYPE = `${stPre}28${stPost}` +export const ROTATION_SPEED_CTYPE = `${stPre}29${stPost}` +export const SATURATION_CTYPE = `${stPre}2F${stPost}` +export const SERIAL_NUMBER_CTYPE = `${stPre}30${stPost}` +export const FIRMWARE_REVISION_CTYPE = `${stPre}52${stPost}` +export const STATUS_LOW_BATTERY_CTYPE = `${stPre}79${stPost}` +export const STATUS_FAULT_CTYPE = `${stPre}77${stPost}` +export const TARGET_DOORSTATE_CTYPE = `${stPre}32${stPost}` +export const TARGET_LOCK_MECHANISM_STATE_CTYPE = `${stPre}1E${stPost}` +export const TARGET_RELATIVE_HUMIDITY_CTYPE = `${stPre}34${stPost}` +export const TARGET_TEMPERATURE_CTYPE = `${stPre}35${stPost}` +export const TEMPERATURE_UNITS_CTYPE = `${stPre}36${stPost}` +export const VERSION_CTYPE = `${stPre}37${stPost}` +export const WINDOW_COVERING_TARGET_POSITION_CTYPE = `${stPre}7C${stPost}` +export const WINDOW_COVERING_CURRENT_POSITION_CTYPE = `${stPre}6D${stPost}` +export const WINDOW_COVERING_OPERATION_STATE_CTYPE = `${stPre}72${stPost}` +export const CURRENTHEATINGCOOLING_CTYPE = `${stPre}0F${stPost}` +export const TARGETHEATINGCOOLING_CTYPE = `${stPre}33${stPost}` diff --git a/src/index.spec.ts b/src/index.spec.ts index 8ac789ff5..2b1b0cd86 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,8 +1,10 @@ // we just test that we can import index e.g. without any cyclic imports -import "./index"; +import { describe, expect, it } from 'vitest' -describe("index", () => { - test("test index import", () => { - expect(true).toBeTruthy(); - }); -}); +import './index.js' + +describe('index', () => { + it('test index import', () => { + expect(true).toBeTruthy() + }) +}) diff --git a/src/index.ts b/src/index.ts index d4c453e69..43d8c04f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,38 +1,44 @@ -import "source-map-support/register"; // registering node-source-map-support for typescript stack traces -import "./lib/definitions"; // must be loaded before Characteristic and Service class -import createDebug from "debug"; +import 'source-map-support/register.js' // registering node-source-map-support for typescript stack traces +import './lib/definitions/index.js' // must be loaded before Characteristic and Service class +import { createRequire } from 'node:module' + +import createDebug from 'debug' + +import * as Characteristics from './lib/definitions/CharacteristicDefinitions.js' +import * as Services from './lib/definitions/ServiceDefinitions.js' /** * @group Utils */ -export * as uuid from "./lib/util/uuid"; -export * from "./lib/model/HAPStorage"; -export * from "./lib/Accessory"; -export * from "./lib/Bridge"; -export * from "./lib/Service"; -export * from "./lib/Characteristic"; -export * from "./lib/camera"; -export * from "./lib/tv/AccessControlManagement"; -export * from "./lib/HAPServer"; -export * from "./lib/datastream"; -export * from "./lib/controller"; -export * from "./lib/model/AccessoryInfo"; +export * as LegacyTypes from './accessories/types.js' +export * from './lib/Accessory.js' +export * from './lib/Bridge.js' +export * from './lib/camera/index.js' +export * from './lib/Characteristic.js' +export * from './lib/controller/index.js' +export * from './lib/datastream/index.js' +export * from './lib/HAPServer.js' +export * from './lib/model/AccessoryInfo.js' +export * from './lib/model/HAPStorage.js' +export * from './lib/Service.js' +export * from './lib/tv/AccessControlManagement.js' -export * from "./lib/util/clone"; -export * from "./lib/util/once"; -export * from "./lib/util/tlv"; -export * from "./lib/util/hapStatusError"; -export * from "./lib/util/color-utils"; -export * from "./lib/util/time"; -export * from "./lib/util/eventedhttp"; +export * from './lib/util/clone.js' +export * from './lib/util/color-utils.js' +export * from './lib/util/eventedhttp.js' +export * from './lib/util/hapStatusError.js' +export * from './lib/util/once.js' +export * from './lib/util/time.js' +export * from './lib/util/tlv.js' -export * from "./types"; /** * @group Utils */ -export * as LegacyTypes from "./accessories/types"; +export * as uuid from './lib/util/uuid.js' +export * from './types.js' -const debug = createDebug("HAP-NodeJS:Advertiser"); +const require = createRequire(import.meta.url) +const debug = createDebug('HAP-NodeJS:Advertiser') /** * This method can be used to retrieve the current running library version of the HAP-NodeJS framework. @@ -41,18 +47,14 @@ const debug = createDebug("HAP-NodeJS:Advertiser"); * @group Utils */ export function HAPLibraryVersion(): string { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const packageJson = require("../package.json"); - return packageJson.version; + const packageJson = require('../package.json') + return packageJson.version } function printInit() { - debug("Initializing HAP-NodeJS v%s ...", HAPLibraryVersion()); + debug('Initializing HAP-NodeJS v%s ...', HAPLibraryVersion()) } -printInit(); - -import * as Services from "./lib/definitions/ServiceDefinitions"; -import * as Characteristics from "./lib/definitions/CharacteristicDefinitions"; +printInit() /** * This namespace doesn't actually exist and is only used to generate documentation for all Service and Characteristic Definitions. @@ -61,9 +63,4 @@ import * as Characteristics from "./lib/definitions/CharacteristicDefinitions"; * * @group Utils */ -export declare namespace _definitions { // eslint-disable-line @typescript-eslint/no-namespace - export { - Services, - Characteristics, - }; -} +export { Characteristics, Services } diff --git a/src/internal-types.ts b/src/internal-types.ts index 9670f0dc7..10bae4767 100644 --- a/src/internal-types.ts +++ b/src/internal-types.ts @@ -1,19 +1,19 @@ -import { CharacteristicValue, Nullable } from "./types"; +import type { CharacteristicValue, Nullable } from './types' /** * @group HAP Accessory Server */ export interface EventNotification { - characteristics: CharacteristicEventNotification[], + characteristics: CharacteristicEventNotification[] } /** * @group HAP Accessory Server */ export interface CharacteristicEventNotification { - aid: number, - iid: number, - value: Nullable, + aid: number + iid: number + value: Nullable } /** @@ -21,53 +21,57 @@ export interface CharacteristicEventNotification { */ export function consideredTrue(input: string | null): boolean { if (!input) { - return false; + return false } - return input === "true" || input === "1"; + return input === 'true' || input === '1' } /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum TLVValues { - // noinspection JSUnusedGlobalSymbols REQUEST_TYPE = 0x00, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + + // eslint-disable-next-line ts/no-duplicate-enum-values METHOD = 0x00, // (match the terminology of the spec sheet but keep backwards compatibility with entry above) USERNAME = 0x01, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + + // eslint-disable-next-line ts/no-duplicate-enum-values IDENTIFIER = 0x01, SALT = 0x02, PUBLIC_KEY = 0x03, PASSWORD_PROOF = 0x04, ENCRYPTED_DATA = 0x05, SEQUENCE_NUM = 0x06, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + + // eslint-disable-next-line ts/no-duplicate-enum-values STATE = 0x06, ERROR_CODE = 0x07, RETRY_DELAY = 0x08, CERTIFICATE = 0x09, // x.509 certificate PROOF = 0x0A, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - SIGNATURE = 0x0A, // apple authentication coprocessor + + // eslint-disable-next-line ts/no-duplicate-enum-values + SIGNATURE = 0x0A, // apple authentication coprocessor PERMISSIONS = 0x0B, // None (0x00): regular user, 0x01: Admin (able to add/remove/list pairings) FRAGMENT_DATA = 0x0C, FRAGMENT_LAST = 0x0D, - SEPARATOR = 0x0FF // Zero-length TLV that separates different TLVs in a list. + SEPARATOR = 0x0FF, // Zero-length TLV that separates different TLVs in a list. } /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum PairMethods { - // noinspection JSUnusedGlobalSymbols PAIR_SETUP = 0x00, PAIR_SETUP_WITH_AUTH = 0x01, PAIR_VERIFY = 0x02, ADD_PAIRING = 0x03, REMOVE_PAIRING = 0x04, - LIST_PAIRINGS = 0x05 + LIST_PAIRINGS = 0x05, } /** @@ -75,20 +79,22 @@ export const enum PairMethods { * * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum PairingStates { M1 = 0x01, M2 = 0x02, M3 = 0x03, M4 = 0x04, M5 = 0x05, - M6 = 0x06 + M6 = 0x06, } /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPMimeTypes { - PAIRING_TLV8 = "application/pairing+tlv8", - HAP_JSON = "application/hap+json", - IMAGE_JPEG = "image/jpeg", + PAIRING_TLV8 = 'application/pairing+tlv8', + HAP_JSON = 'application/hap+json', + IMAGE_JPEG = 'image/jpeg', } diff --git a/src/lib/Accessory.spec.ts b/src/lib/Accessory.spec.ts index 814c85afb..10481363b 100644 --- a/src/lib/Accessory.spec.ts +++ b/src/lib/Accessory.spec.ts @@ -1,5 +1,8 @@ -import crypto from "crypto"; -import { +import type { Buffer } from 'node:buffer' + +import type { Mock, MockInstance } from 'vitest' + +import type { AccessoriesResponse, CharacteristicJsonObject, CharacteristicReadData, @@ -12,813 +15,829 @@ import { InterfaceName, IPAddress, ResourceRequest, - ResourceRequestType, -} from "../types"; -import { Accessory, AccessoryEventTypes, Categories, CharacteristicWarningType, MDNSAdvertiser, PublishInfo } from "./Accessory"; -import { BonjourHAPAdvertiser } from "./Advertiser"; -import { Bridge } from "./Bridge"; +} from '../types' +import type { PublishInfo } from './Accessory' +import type { CharacteristicGetCallback, CharacteristicSetCallback } from './Characteristic' +import type { Controller, ControllerIdentifier, ControllerServiceMap } from './controller' +import type { IdentifyCallback } from './HAPServer' +import type { PairingInformation } from './model/AccessoryInfo' +import type { HAPConnection } from './util/eventedhttp' + +import { randomBytes } from 'node:crypto' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { ResourceRequestType } from '../types.js' +import { + Accessory, + AccessoryEventTypes, + Categories, + CharacteristicWarningType, + MDNSAdvertiser, +} from './Accessory.js' +import { BonjourHAPAdvertiser } from './Advertiser.js' +import { Bridge } from './Bridge.js' import { Access, Characteristic, CharacteristicEventTypes, - CharacteristicGetCallback, - CharacteristicSetCallback, Formats, Perms, Units, -} from "./Characteristic"; -import { CameraController, Controller, ControllerIdentifier, ControllerServiceMap } from "./controller"; -import { createCameraControllerOptions, MOCK_IMAGE } from "./controller/CameraController.spec"; -import { HAPHTTPCode, HAPStatus, IdentifyCallback, TLVErrorCode } from "./HAPServer"; -import { AccessoryInfo, PairingInformation, PermissionTypes } from "./model/AccessoryInfo"; -import { IdentifierCache } from "./model/IdentifierCache"; -import { Service } from "./Service"; -import { EventedHTTPServer, HAPConnection } from "./util/eventedhttp"; -import { HapStatusError } from "./util/hapStatusError"; -import { awaitEventOnce, PromiseTimeout } from "./util/promise-utils"; -import * as uuid from "./util/uuid"; -import { toShortForm } from "./util/uuid"; -import Mock = jest.Mock; - -describe("Accessory", () => { - const TEST_DISPLAY_NAME = "Test Accessory"; - const TEST_UUID = uuid.generate("HAP-NODEJS-TEST-ACCESSORY!"); - - let accessory: Accessory; - let connection: HAPConnection; - - const serverUsername = "AB:CD:EF:00:11:22"; - const clientUsername0 = "AB:CD:EF:00:11:23"; - let clientPublicKey0: Buffer; - const clientUsername1 = "AB:CD:EF:00:11:24"; - let clientPublicKey1: Buffer; - - let accessoryInfoUnpaired: AccessoryInfo; - let accessoryInfoPaired: AccessoryInfo; - const saveMock: Mock = jest.fn(); - - let callback: Mock; +} from './Characteristic.js' +import { createCameraControllerOptions, MOCK_IMAGE } from './controller/CameraController.spec.js' +import { CameraController } from './controller/index.js' +import { HAPHTTPCode, HAPStatus, TLVErrorCode } from './HAPServer.js' +import { AccessoryInfo, PermissionTypes } from './model/AccessoryInfo.js' +import { IdentifierCache } from './model/IdentifierCache.js' +import { Service } from './Service.js' +import { EventedHTTPServer } from './util/eventedhttp.js' +import { HapStatusError } from './util/hapStatusError.js' +import { awaitEventOnce, PromiseTimeout } from './util/promise-utils.js' +import { generate, toShortForm } from './util/uuid.js' + +describe('accessory', () => { + const TEST_DISPLAY_NAME = 'Test Accessory' + const TEST_UUID = generate('HAP-NODEJS-TEST-ACCESSORY!') + + let accessory: Accessory + let connection: HAPConnection + + const serverUsername = 'AB:CD:EF:00:11:22' + const clientUsername0 = 'AB:CD:EF:00:11:23' + let clientPublicKey0: Buffer + const clientUsername1 = 'AB:CD:EF:00:11:24' + let clientPublicKey1: Buffer + + let accessoryInfoUnpaired: AccessoryInfo + let accessoryInfoPaired: AccessoryInfo + const saveMock: Mock = vi.fn() + + let callback: Mock // a void promise to wait for the above callback to be called! - let callbackPromise: Promise; + let callbackPromise: Promise beforeEach(() => { - clientPublicKey0 = crypto.randomBytes(32); - clientPublicKey1 = crypto.randomBytes(32); + clientPublicKey0 = randomBytes(32) + clientPublicKey1 = randomBytes(32) - accessoryInfoUnpaired = AccessoryInfo.create(serverUsername); + accessoryInfoUnpaired = AccessoryInfo.create(serverUsername) // @ts-expect-error: private access - accessoryInfoUnpaired.setupID = Accessory._generateSetupID(); - accessoryInfoUnpaired.displayName = "Outlet"; - accessoryInfoUnpaired.category = 7; - accessoryInfoUnpaired.pincode = " 031-45-154"; - accessoryInfoUnpaired.save = saveMock; + accessoryInfoUnpaired.setupID = Accessory._generateSetupID() + accessoryInfoUnpaired.displayName = 'Outlet' + accessoryInfoUnpaired.category = 7 + accessoryInfoUnpaired.pincode = ' 031-45-154' + accessoryInfoUnpaired.save = saveMock - accessoryInfoPaired = AccessoryInfo.create(serverUsername); + accessoryInfoPaired = AccessoryInfo.create(serverUsername) // @ts-expect-error: private access - accessoryInfoPaired.setupID = Accessory._generateSetupID(); - accessoryInfoPaired.displayName = "Outlet"; - accessoryInfoPaired.category = 7; - accessoryInfoPaired.pincode = " 031-45-154"; - accessoryInfoPaired.addPairedClient(clientUsername0, clientPublicKey0, PermissionTypes.ADMIN); - accessoryInfoPaired.save = saveMock; + accessoryInfoPaired.setupID = Accessory._generateSetupID() + accessoryInfoPaired.displayName = 'Outlet' + accessoryInfoPaired.category = 7 + accessoryInfoPaired.pincode = ' 031-45-154' + accessoryInfoPaired.addPairedClient(clientUsername0, clientPublicKey0, PermissionTypes.ADMIN) + accessoryInfoPaired.save = saveMock // ensure we start with a clean Accessory for every test - Accessory.cleanupAccessoryData(serverUsername); - accessory = new Accessory(TEST_DISPLAY_NAME, TEST_UUID); + Accessory.cleanupAccessoryData(serverUsername) + accessory = new Accessory(TEST_DISPLAY_NAME, TEST_UUID) connection = { username: clientUsername0, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; + } as any - callback = jest.fn(); - const originalReset = callback.mockReset; + callback = vi.fn() + const originalReset = callback.mockReset callback.mockReset = () => { - const resetResult = originalReset(); - callbackPromise = new Promise(resolve => { - callback.mockImplementationOnce(() => resolve()); - }); - return resetResult; - }; + const resetResult = originalReset() + callbackPromise = new Promise((resolve) => { + callback.mockImplementationOnce(() => resolve()) + }) + return resetResult + } - callback.mockReset(); + callback.mockReset() - saveMock.mockReset(); - }); + saveMock.mockReset() + }) afterEach(async () => { - await accessory?.unpublish(); - await accessory?.destroy(); - }); - - describe("constructor", () => { - test("fail to load with no display name", () => { - expect(() => new Accessory("", "")) - .toThrow("non-empty displayName"); - }); - - test("fail to load with no UUID", () => { - expect(() => new Accessory("Test", "")) - .toThrow("valid UUID"); - }); - - test("fail to load with an invalid UUID", () => { - expect(() => new Accessory("Test", "test")) - .toThrow("not a valid UUID"); - }); - }); - - describe("handling services", () => { - test("addService", () => { - const existingCount = 1; // accessoryInformation service; protocolInformation is only added on publish - - const instance = new Service.Switch("Switch"); - expect(accessory.services.length).toEqual(existingCount); - - const switchService = accessory.addService(instance); - expect(accessory.services.length).toEqual(existingCount + 1); - expect(accessory.services.includes(instance)).toBeTruthy(); - expect(switchService).toBe(instance); - - const outletService = accessory.addService(Service.Outlet, "Outlet"); - expect(outletService.displayName).toEqual("Outlet"); - expect(accessory.services.length).toEqual(existingCount + 2); - expect(accessory.services.includes(outletService)).toBeTruthy(); + await accessory?.unpublish() + await accessory?.destroy() + }) + + describe('constructor', () => { + it('fail to load with no display name', () => { + expect(() => new Accessory('', '')) + .toThrow('non-empty displayName') + }) + + it('fail to load with no UUID', () => { + expect(() => new Accessory('Test', '')) + .toThrow('valid UUID') + }) + + it('fail to load with an invalid UUID', () => { + expect(() => new Accessory('Test', 'test')) + .toThrow('not a valid UUID') + }) + }) + + describe('handling services', () => { + it('addService', () => { + const existingCount = 1 // accessoryInformation service; protocolInformation is only added on publish + + const instance = new Service.Switch('Switch') + expect(accessory.services.length).toEqual(existingCount) + + const switchService = accessory.addService(instance) + expect(accessory.services.length).toEqual(existingCount + 1) + expect(accessory.services.includes(instance)).toBeTruthy() + expect(switchService).toBe(instance) + + const outletService = accessory.addService(Service.Outlet, 'Outlet') + expect(outletService.displayName).toEqual('Outlet') + expect(accessory.services.length).toEqual(existingCount + 2) + expect(accessory.services.includes(outletService)).toBeTruthy() // CHECKING DUPLICATES - const instance0 = new Service.Switch("Switch1"); - expect(() => accessory.addService(instance0)).toThrow(); + const instance0 = new Service.Switch('Switch1') + expect(() => accessory.addService(instance0)).toThrow() - const instance1 = accessory.addService(Service.Switch, "Switch2", "subtype"); - expect(instance1.displayName).toEqual("Switch2"); - expect(() => accessory.addService(Service.Switch, "Switch3", "subtype")).toThrow(); + const instance1 = accessory.addService(Service.Switch, 'Switch2', 'subtype') + expect(instance1.displayName).toEqual('Switch2') + expect(() => accessory.addService(Service.Switch, 'Switch3', 'subtype')).toThrow() expect(accessory.getService(Service.Switch)) - .toBe(instance); - expect(accessory.getService("Switch")) - .toBe(instance); - - expect(accessory.getServiceById(Service.Switch, "subtype")) - .toBe(instance1); - expect(accessory.getServiceById("Switch2", "subtype")) - .toBe(instance1); - expect(accessory.getServiceById("Switch3", "subtype")) - .toBeUndefined(); - }); + .toBe(instance) + expect(accessory.getService('Switch')) + .toBe(instance) - test("removeService", () => { - const existingCount = 1; // accessoryInformation service; protocolInformation is only added on publish + expect(accessory.getServiceById(Service.Switch, 'subtype')) + .toBe(instance1) + expect(accessory.getServiceById('Switch2', 'subtype')) + .toBe(instance1) + expect(accessory.getServiceById('Switch3', 'subtype')) + .toBeUndefined() + }) - const instance = new Service.Switch("Switch"); - instance.setPrimaryService(); - expect(accessory.services.length).toEqual(existingCount); + it('removeService', () => { + const existingCount = 1 // accessoryInformation service; protocolInformation is only added on publish + const instance = new Service.Switch('Switch') + instance.setPrimaryService() + expect(accessory.services.length).toEqual(existingCount) - accessory.addService(instance); - expect(accessory.services.length).toEqual(existingCount + 1); + accessory.addService(instance) + expect(accessory.services.length).toEqual(existingCount + 1) - const outlet = accessory.addService(Service.Outlet); - outlet.addLinkedService(instance); - expect(accessory.services.length).toEqual(existingCount + 2); + const outlet = accessory.addService(Service.Outlet) + outlet.addLinkedService(instance) + expect(accessory.services.length).toEqual(existingCount + 2) - accessory.removeService(instance); - expect(accessory.services.length).toEqual(existingCount + 1); - expect(accessory.services.includes(instance)).toBeFalsy(); + accessory.removeService(instance) + expect(accessory.services.length).toEqual(existingCount + 1) + expect(accessory.services.includes(instance)).toBeFalsy() // HANDLING PRIMARY AND LINKED SERVICES // @ts-expect-error: private access - expect(accessory.primaryService).toBe(undefined); - expect(instance.isPrimaryService).toBeTruthy(); - expect(outlet.linkedServices.length).toEqual(0); - }); + expect(accessory.primaryService).toBe(undefined) + expect(instance.isPrimaryService).toBeTruthy() + expect(outlet.linkedServices.length).toEqual(0) + }) - test("primary service handling", () => { - const instance = new Service.Switch("Switch"); - instance.setPrimaryService(); - accessory.addService(instance); + it('primary service handling', () => { + const instance = new Service.Switch('Switch') + instance.setPrimaryService() + accessory.addService(instance) // @ts-expect-error: private access - expect(accessory.primaryService).toBe(instance); + expect(accessory.primaryService).toBe(instance) - const outlet = new Service.Outlet("Outlet"); - outlet.setPrimaryService(); - accessory.addService(outlet); + const outlet = new Service.Outlet('Outlet') + outlet.setPrimaryService() + accessory.addService(outlet) // @ts-expect-error: private access - expect(accessory.primaryService).toBe(outlet); - expect(instance.isPrimaryService).toBeFalsy(); - expect(outlet.isPrimaryService).toBeTruthy(); + expect(accessory.primaryService).toBe(outlet) + expect(instance.isPrimaryService).toBeFalsy() + expect(outlet.isPrimaryService).toBeTruthy() - instance.setPrimaryService(); + instance.setPrimaryService() // @ts-expect-error: private access - expect(accessory.primaryService).toBe(instance); - expect(instance.isPrimaryService).toBeTruthy(); - expect(outlet.isPrimaryService).toBeFalsy(); + expect(accessory.primaryService).toBe(instance) + expect(instance.isPrimaryService).toBeTruthy() + expect(outlet.isPrimaryService).toBeFalsy() - instance.setPrimaryService(false); + instance.setPrimaryService(false) // @ts-expect-error: private access - expect(accessory.primaryService).toBe(undefined); - expect(instance.isPrimaryService).toBeFalsy(); - expect(outlet.isPrimaryService).toBeFalsy(); - }); - }); + expect(accessory.primaryService).toBe(undefined) + expect(instance.isPrimaryService).toBeFalsy() + expect(outlet.isPrimaryService).toBeFalsy() + }) + }) - describe("bridged accessories", () => { - test("addBridgedAccessory", () => { - const bridge = new Bridge("TestBridge", uuid.generate("bridge test")); + describe('bridged accessories', () => { + it('addBridgedAccessory', () => { + const bridge = new Bridge('TestBridge', generate('bridge test')) - bridge.addBridgedAccessories([ accessory ]); - expect(bridge.bridged).toBeFalsy(); - expect(bridge.bridge).toBeUndefined(); - expect(accessory.bridged).toBeTruthy(); - expect(accessory.bridge).toBe(bridge); + bridge.addBridgedAccessories([accessory]) + expect(bridge.bridged).toBeFalsy() + expect(bridge.bridge).toBeUndefined() + expect(accessory.bridged).toBeTruthy() + expect(accessory.bridge).toBe(bridge) - expect(bridge.getPrimaryAccessory()).toBe(bridge); - expect(accessory.getPrimaryAccessory()).toBe(bridge); + expect(bridge.getPrimaryAccessory()).toBe(bridge) + expect(accessory.getPrimaryAccessory()).toBe(bridge) - expect(bridge.bridgedAccessories.includes(accessory)).toBeTruthy(); + expect(bridge.bridgedAccessories.includes(accessory)).toBeTruthy() - expect(() => bridge.addBridgedAccessory(accessory)).toThrow(); - expect(() => bridge.addBridgedAccessory(new Bridge("asdf", uuid.generate("asdf")))) - .toThrow(); - }); + expect(() => bridge.addBridgedAccessory(accessory)).toThrow() + expect(() => bridge.addBridgedAccessory(new Bridge('asdf', generate('asdf')))) + .toThrow() + }) - test("removeBridgedAccessory", () => { - const bridge = new Bridge("TestBridge", uuid.generate("bridge test")); + it('removeBridgedAccessory', () => { + const bridge = new Bridge('TestBridge', generate('bridge test')) const validate = () => { - expect(bridge.bridged).toBeFalsy(); - expect(bridge.bridge).toBeUndefined(); - expect(accessory.bridged).toBeFalsy(); - expect(accessory.bridge).toBeUndefined(); + expect(bridge.bridged).toBeFalsy() + expect(bridge.bridge).toBeUndefined() + expect(accessory.bridged).toBeFalsy() + expect(accessory.bridge).toBeUndefined() - expect(bridge.getPrimaryAccessory()).toBe(bridge); - expect(accessory.getPrimaryAccessory()).toBe(accessory); + expect(bridge.getPrimaryAccessory()).toBe(bridge) + expect(accessory.getPrimaryAccessory()).toBe(accessory) - expect(bridge.bridgedAccessories.includes(accessory)).toBeFalsy(); - }; + expect(bridge.bridgedAccessories.includes(accessory)).toBeFalsy() + } - bridge.addBridgedAccessories([ accessory ]); - bridge.removeBridgedAccessory(accessory); - validate(); + bridge.addBridgedAccessories([accessory]) + bridge.removeBridgedAccessory(accessory) + validate() - bridge.addBridgedAccessories([ accessory ]); - bridge.removeBridgedAccessories([ accessory ]); - validate(); + bridge.addBridgedAccessories([accessory]) + bridge.removeBridgedAccessories([accessory]) + validate() - bridge.addBridgedAccessories([ accessory ]); - bridge.removeAllBridgedAccessories(); - validate(); + bridge.addBridgedAccessories([accessory]) + bridge.removeAllBridgedAccessories() + validate() - expect(() => bridge.removeBridgedAccessory(accessory)).toThrow(); - }); + expect(() => bridge.removeBridgedAccessory(accessory)).toThrow() + }) - test("getAccessoryByAID", () => { - const bridge = new Bridge("TestBridge", uuid.generate("bridge test")); - bridge.addBridgedAccessory(accessory); + it('getAccessoryByAID', () => { + const bridge = new Bridge('TestBridge', generate('bridge test')) + bridge.addBridgedAccessory(accessory) - bridge._identifierCache = new IdentifierCache(serverUsername); - bridge._assignIDs(bridge._identifierCache); + bridge._identifierCache = new IdentifierCache(serverUsername) + bridge._assignIDs(bridge._identifierCache) // @ts-expect-error: private access - expect(bridge.getAccessoryByAID(1)).toBe(bridge); + expect(bridge.getAccessoryByAID(1)).toBe(bridge) // @ts-expect-error: private access - expect(bridge.getAccessoryByAID(2)).toBe(accessory); + expect(bridge.getAccessoryByAID(2)).toBe(accessory) // @ts-expect-error: private access - expect(accessory.getAccessoryByAID(2)).toBe(accessory); - }); - }); + expect(accessory.getAccessoryByAID(2)).toBe(accessory) + }) + }) - describe("accessory controllers", () => { - test("configureController deserialize controllers and remove/add/replace services correctly", () => { - accessory.configureController(new TestController()); + describe('accessory controllers', () => { + it('configureController deserialize controllers and remove/add/replace services correctly', () => { + accessory.configureController(new TestController()) - const serialized = Accessory.serialize(accessory); + const serialized = Accessory.serialize(accessory) - const restoredAccessory = Accessory.deserialize(serialized); - restoredAccessory.configureController(new TestController()); // restore Controller; + const restoredAccessory = Accessory.deserialize(serialized) + restoredAccessory.configureController(new TestController()) // restore Controller; - expect(restoredAccessory.services).toBeDefined(); - expect(restoredAccessory.services.length).toEqual(3); // accessory information, light sensor, outlet + expect(restoredAccessory.services).toBeDefined() + expect(restoredAccessory.services.length).toEqual(3) // accessory information, light sensor, outlet - expect(restoredAccessory.getService(Service.Lightbulb)).toBeUndefined(); - expect(restoredAccessory.getService(Service.LightSensor)).toBeDefined(); - expect(restoredAccessory.getService(Service.Outlet)).toBeDefined(); - expect(restoredAccessory.getService(Service.Switch)).toBeUndefined(); - }); - }); + expect(restoredAccessory.getService(Service.Lightbulb)).toBeUndefined() + expect(restoredAccessory.getService(Service.LightSensor)).toBeDefined() + expect(restoredAccessory.getService(Service.Outlet)).toBeDefined() + expect(restoredAccessory.getService(Service.Switch)).toBeUndefined() + }) + }) - describe("publish", () => { - test.each` + describe('publish', () => { + it.each` advertiser | republish ${MDNSAdvertiser.BONJOUR} | ${false} ${MDNSAdvertiser.CIAO} | ${false} ${MDNSAdvertiser.BONJOUR} | ${true} ${MDNSAdvertiser.CIAO} | ${true} - `("Clean Accessory publish and unpublish (advertiser: $advertiser; republish: $republish)", async ({ advertiser, republish }) => { - const switchService = new Service.Switch("My Example Switch"); - accessory.addService(switchService); + `('clean Accessory publish and unpublish (advertiser: $advertiser; republish: $republish)', async ({ advertiser, republish }) => { + const switchService = new Service.Switch('My Example Switch') + accessory.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: advertiser, - }; + advertiser, + } - await accessory.publish(publishInfo); + await accessory.publish(publishInfo) - expect(accessory.displayName.startsWith(TEST_DISPLAY_NAME)); - expect(accessory.displayName.length).toEqual(TEST_DISPLAY_NAME.length + 1 + 4); // added hash! + expect(accessory.displayName.startsWith(TEST_DISPLAY_NAME)) + expect(accessory.displayName.length).toEqual(TEST_DISPLAY_NAME.length + 1 + 4) // added hash! - await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED); + await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED) - const displayNameWithIdentifyingMaterial = accessory.displayName; + const displayNameWithIdentifyingMaterial = accessory.displayName - await accessory.unpublish(); + await accessory.unpublish() if (!republish) { - return; + return } // This second round tests, that the Accessory is reusable after unpublished was called - await PromiseTimeout(200); + await PromiseTimeout(200) - await accessory.publish(publishInfo); + await accessory.publish(publishInfo) // ensure unification isn't done twice! - expect(accessory.displayName).toEqual(displayNameWithIdentifyingMaterial); + expect(accessory.displayName).toEqual(displayNameWithIdentifyingMaterial) - await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED); - }); + await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED) + }) - test("Clean Accessory publish and unpublish with default advertiser selection", async () => { - const switchService = new Service.Switch("My Example Switch"); - accessory.addService(switchService); + it('clean Accessory publish and unpublish with default advertiser (ciao) selection', async () => { + const switchService = new Service.Switch('My Example Switch') + accessory.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser: MDNSAdvertiser.CIAO, + } - await accessory.publish(publishInfo); + await accessory.publish(publishInfo) - expect(accessory.displayName.startsWith(TEST_DISPLAY_NAME)); - expect(accessory.displayName.length).toEqual(TEST_DISPLAY_NAME.length + 1 + 4); // added hash! + expect(accessory.displayName.startsWith(TEST_DISPLAY_NAME)) + expect(accessory.displayName.length).toEqual(TEST_DISPLAY_NAME.length + 1 + 4) // added hash! - await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED); - }); - }); + await awaitEventOnce(accessory, AccessoryEventTypes.ADVERTISED) + }) + }) - describe("Accessory and Service naming checks", () => { - let consoleWarnSpy: jest.SpyInstance; + describe('accessory and Service naming checks', () => { + const advertiser = MDNSAdvertiser.CIAO + let consoleWarnSpy: MockInstance beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) afterEach(() => { - consoleWarnSpy.mockRestore(); - }); + consoleWarnSpy.mockRestore() + }) - test("Accessory Name ending with !", async () => { - const accessoryBadName = new Accessory("Bad Name!",uuid.generate("Bad Name")); + it('accessory Name ending with !', async () => { + const accessoryBadName = new Accessory('Bad Name!', generate('Bad Name')) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } + + await accessoryBadName.publish(publishInfo) - await accessoryBadName.publish(publishInfo); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'Bad Name!' has an invalid 'Name' characteristic ('Bad Name!'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'Bad Name!\' has an invalid \'Name\' characteristic (\'Bad Name!\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Accessory Name containing !", async () => { - const accessoryBadName = new Accessory("Bad ! Name",uuid.generate("Bad Name")); + it('accessory Name containing !', async () => { + const accessoryBadName = new Accessory('Bad ! Name', generate('Bad Name')) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } - await accessoryBadName.publish(publishInfo); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'Bad ! Name' has an invalid 'Name' characteristic ('Bad ! Name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + await accessoryBadName.publish(publishInfo) - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'Bad ! Name\' has an invalid \'Name\' characteristic (\'Bad ! Name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - test("Accessory Name containing '", async () => { - const accessoryBadName = new Accessory("Bad ' Name",uuid.generate("Bad ' Name")); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) + + it('accessory Name containing apostrophe', async () => { + const accessoryBadName = new Accessory('Bad \' Name', generate('Bad \' Name')) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } - await accessoryBadName.publish(publishInfo); - expect(consoleWarnSpy).toBeCalledTimes(0); + await accessoryBadName.publish(publishInfo) + expect(consoleWarnSpy).toBeCalledTimes(0) - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Accessory Name starting with '", async () => { - const accessoryBadName = new Accessory("'Bad Name",uuid.generate("Bad Name'")); + it('accessory Name starting with apostrophe', async () => { + const accessoryBadName = new Accessory('\'Bad Name', generate('Bad Name\'')) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } - await accessoryBadName.publish(publishInfo); - expect(accessoryBadName.displayName.startsWith(TEST_DISPLAY_NAME)); - expect(consoleWarnSpy).toBeCalledTimes(2); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory ''Bad Name' has an invalid 'Name' characteristic (''Bad Name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + await accessoryBadName.publish(publishInfo) + expect(accessoryBadName.displayName.startsWith(TEST_DISPLAY_NAME)) + expect(consoleWarnSpy).toBeCalledTimes(2) - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'\'Bad Name\' has an invalid \'Name\' characteristic (\'\'Bad Name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - test("Service Name containing !", async () => { - const switchService = new Service.Switch("My Bad ! Switch"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - accessoryBadName.addService(switchService); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) + + it('service Name containing !', async () => { + const switchService = new Service.Switch('My Bad ! Switch') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } + + await accessoryBadName.publish(publishInfo) - await accessoryBadName.publish(publishInfo); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'My Bad ! Switch' has an invalid 'Name' characteristic ('My Bad ! Switch'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'My Bad ! Switch\' has an invalid \'Name\' characteristic (\'My Bad ! Switch\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Service Name ending with !", async () => { - const switchService = new Service.Switch("My Bad Switch!"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - accessoryBadName.addService(switchService); + it('service Name ending with !', async () => { + const switchService = new Service.Switch('My Bad Switch!') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } + + await accessoryBadName.publish(publishInfo) + expect(consoleWarnSpy).toBeCalledTimes(1) - await accessoryBadName.publish(publishInfo); - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'My Bad Switch!' has an invalid 'Name' characteristic ('My Bad Switch!'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'My Bad Switch!\' has an invalid \'Name\' characteristic (\'My Bad Switch!\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Service Name containing '", async () => { - const switchService = new Service.Switch("My Bad ' Switch"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - accessoryBadName.addService(switchService); + it('service Name containing apostrophe', async () => { + const switchService = new Service.Switch('My Bad \' Switch') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } - await accessoryBadName.publish(publishInfo); - expect(consoleWarnSpy).toBeCalledTimes(0); + await accessoryBadName.publish(publishInfo) + expect(consoleWarnSpy).toBeCalledTimes(0) - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Service Name ending with '", async () => { - const switchService = new Service.Switch("My Bad Switch'"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - accessoryBadName.addService(switchService); + it('service Name ending with apostrophe', async () => { + const switchService = new Service.Switch('My Bad Switch\'') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } + + await accessoryBadName.publish(publishInfo) + expect(consoleWarnSpy).toBeCalledTimes(1) - await accessoryBadName.publish(publishInfo); - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'My Bad Switch'' has an invalid 'Name' characteristic ('My Bad Switch''). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'My Bad Switch\'\' has an invalid \'Name\' characteristic (\'My Bad Switch\'\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Service Name beginning with '", async () => { - const switchService = new Service.Switch("'My Bad Switch"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - accessoryBadName.addService(switchService); + it('service Name beginning with apostrophe', async () => { + const switchService = new Service.Switch('\'My Bad Switch') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } + + await accessoryBadName.publish(publishInfo) + expect(consoleWarnSpy).toBeCalledTimes(1) - await accessoryBadName.publish(publishInfo); - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory ''My Bad Switch' has an invalid 'Name' characteristic (''My Bad Switch'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'\'My Bad Switch\' has an invalid \'Name\' characteristic (\'\'My Bad Switch\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) - test("Service ConfiguredName beginning with '", async () => { - const switchService = new Service.Switch("My Bad Switch"); - const accessoryBadName = new Accessory("Bad Name",uuid.generate("Bad Name")); - switchService.addCharacteristic(Characteristic.ConfiguredName); - accessoryBadName.addService(switchService); + it('service ConfiguredName beginning with apostrophe', async () => { + const switchService = new Service.Switch('My Bad Switch') + const accessoryBadName = new Accessory('Bad Name', generate('Bad Name')) + switchService.addCharacteristic(Characteristic.ConfiguredName) + accessoryBadName.addService(switchService) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - advertiser: undefined, - }; + advertiser, + } - await accessoryBadName.publish(publishInfo); + await accessoryBadName.publish(publishInfo) - switchService.getCharacteristic(Characteristic.ConfiguredName).updateValue("'Bad Name"); + switchService.getCharacteristic(Characteristic.ConfiguredName).updateValue('\'Bad Name') - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'Configured Name' has an invalid 'ConfiguredName' characteristic (''Bad Name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); + expect(consoleWarnSpy).toBeCalledTimes(1) - await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED); - await accessoryBadName?.unpublish(); - await accessoryBadName?.destroy(); - }); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'Configured Name\' has an invalid \'ConfiguredName\' characteristic (\'\'Bad Name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') - }); + await awaitEventOnce(accessoryBadName, AccessoryEventTypes.ADVERTISED) + await accessoryBadName?.unpublish() + await accessoryBadName?.destroy() + }) + }) - describe("pairing", () => { - let defaultPairingInfo: PairingInformation; + describe('pairing', () => { + let defaultPairingInfo: PairingInformation beforeEach(() => { - defaultPairingInfo = { username: clientUsername0, publicKey: clientPublicKey0, permission: PermissionTypes.ADMIN }; - }); + defaultPairingInfo = { username: clientUsername0, publicKey: clientPublicKey0, permission: PermissionTypes.ADMIN } + }) - test("handleInitialPairSetupFinished", async () => { - const advertiser = new BonjourHAPAdvertiser(accessoryInfoUnpaired); - advertiser.updateAdvertisement = jest.fn(); - accessory._advertiser = advertiser; + it('handleInitialPairSetupFinished', async () => { + const advertiser = new BonjourHAPAdvertiser(accessoryInfoUnpaired) + advertiser.updateAdvertisement = vi.fn() + accessory._advertiser = advertiser - accessoryInfoUnpaired.addPairedClient = jest.fn(); - accessory._accessoryInfo = accessoryInfoUnpaired; + accessoryInfoUnpaired.addPairedClient = vi.fn() + accessory._accessoryInfo = accessoryInfoUnpaired - const publicKey = crypto.randomBytes(32); - // eslint-disable-next-line @typescript-eslint/no-empty-function - const callback = jest.fn(); + const publicKey = randomBytes(32) + const callback = vi.fn() // @ts-expect-error: private access - accessory.handleInitialPairSetupFinished(clientUsername0, publicKey, callback); + accessory.handleInitialPairSetupFinished(clientUsername0, publicKey, callback) - expect(accessoryInfoUnpaired.addPairedClient).toBeCalledTimes(1); - expect(accessoryInfoUnpaired.addPairedClient).toBeCalledWith(clientUsername0, publicKey, PermissionTypes.ADMIN); + expect(accessoryInfoUnpaired.addPairedClient).toBeCalledTimes(1) + expect(accessoryInfoUnpaired.addPairedClient).toBeCalledWith(clientUsername0, publicKey, PermissionTypes.ADMIN) - expect(saveMock).toBeCalledTimes(1); + expect(saveMock).toBeCalledTimes(1) - expect(advertiser.updateAdvertisement).toBeCalledTimes(1); + expect(advertiser.updateAdvertisement).toBeCalledTimes(1) - await advertiser.destroy(); - }); + advertiser.destroy() + }) - describe("handleAddPairing", () => { - test("unavailable", () => { + describe('handleAddPairing', () => { + it('unavailable', () => { // @ts-expect-error: private access - accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, PermissionTypes.USER, callback); - expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE); - }); + accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, PermissionTypes.USER, callback) + expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE) + }) - test("missing admin permissions", () => { - accessory._accessoryInfo = accessoryInfoUnpaired; + it('missing admin permissions', () => { + accessory._accessoryInfo = accessoryInfoUnpaired // @ts-expect-error: private access - accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, PermissionTypes.USER, callback); - expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION); - }); + accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, PermissionTypes.USER, callback) + expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION) + }) - test.each([PermissionTypes.USER, PermissionTypes.ADMIN])( - "adding pairing: %p", type => { - accessory._accessoryInfo = accessoryInfoPaired; + it.each([PermissionTypes.USER, PermissionTypes.ADMIN])( + 'adding pairing: %p', + (type) => { + accessory._accessoryInfo = accessoryInfoPaired // @ts-expect-error: private access - accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, type, callback); - expect(callback).toBeCalledWith(0); + accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, type, callback) + expect(callback).toBeCalledWith(0) const expectedPairings: PairingInformation[] = [ defaultPairingInfo, { username: clientUsername1, publicKey: clientPublicKey1, permission: type }, - ]; - expect(accessoryInfoPaired.listPairings()).toEqual(expectedPairings); + ] + expect(accessoryInfoPaired.listPairings()).toEqual(expectedPairings) expect(accessoryInfoPaired.pairedAdminClients) - .toEqual(type === PermissionTypes.ADMIN ? 2 : 1); + .toEqual(type === PermissionTypes.ADMIN ? 2 : 1) - expect(saveMock).toBeCalledTimes(1); - }); + expect(saveMock).toBeCalledTimes(1) + }, + ) - test.each([ + it.each([ { previous: PermissionTypes.USER, update: PermissionTypes.ADMIN }, { previous: PermissionTypes.ADMIN, update: PermissionTypes.USER }, { previous: PermissionTypes.USER, update: PermissionTypes.USER }, { previous: PermissionTypes.ADMIN, update: PermissionTypes.ADMIN }, ])( - "update pairing: %p", type => { - accessory._accessoryInfo = accessoryInfoPaired; - accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, type.previous); + 'update pairing: %p', + (type) => { + accessory._accessoryInfo = accessoryInfoPaired + accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, type.previous) expect(accessoryInfoPaired.pairedAdminClients) - .toEqual(type.previous === PermissionTypes.ADMIN ? 2 : 1); + .toEqual(type.previous === PermissionTypes.ADMIN ? 2 : 1) // @ts-expect-error: private access - accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, type.update, callback); - expect(callback).toBeCalledWith(0); + accessory.handleAddPairing(connection, clientUsername1, clientPublicKey1, type.update, callback) + expect(callback).toBeCalledWith(0) const expectedPairings: PairingInformation[] = [ defaultPairingInfo, { username: clientUsername1, publicKey: clientPublicKey1, permission: type.update }, - ]; - expect(accessoryInfoPaired.listPairings()).toEqual(expectedPairings); + ] + expect(accessoryInfoPaired.listPairings()).toEqual(expectedPairings) expect(accessoryInfoPaired.pairedAdminClients) - .toEqual(type.update === PermissionTypes.ADMIN ? 2 : 1); + .toEqual(type.update === PermissionTypes.ADMIN ? 2 : 1) - expect(saveMock).toBeCalledTimes(1); - }); + expect(saveMock).toBeCalledTimes(1) + }, + ) - test("update permission with non-matching public key", () => { - accessory._accessoryInfo = accessoryInfoPaired; - accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER); + it('update permission with non-matching public key', () => { + accessory._accessoryInfo = accessoryInfoPaired + accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER) // @ts-expect-error: private access - accessory.handleAddPairing(connection, clientUsername1, crypto.randomBytes(32), PermissionTypes.ADMIN, callback); - expect(callback).toBeCalledWith(TLVErrorCode.UNKNOWN); - }); - }); + accessory.handleAddPairing(connection, clientUsername1, randomBytes(32), PermissionTypes.ADMIN, callback) + expect(callback).toBeCalledWith(TLVErrorCode.UNKNOWN) + }) + }) - describe("handleRemovePairing", () => { - let storage: typeof EventedHTTPServer.destroyExistingConnectionsAfterUnpair; + describe('handleRemovePairing', () => { + let storage: typeof EventedHTTPServer.destroyExistingConnectionsAfterUnpair beforeEach(() => { - storage = EventedHTTPServer.destroyExistingConnectionsAfterUnpair; - EventedHTTPServer.destroyExistingConnectionsAfterUnpair = jest.fn(); - }); + storage = EventedHTTPServer.destroyExistingConnectionsAfterUnpair + EventedHTTPServer.destroyExistingConnectionsAfterUnpair = vi.fn() + }) afterEach(() => { - EventedHTTPServer.destroyExistingConnectionsAfterUnpair = storage; - }); + EventedHTTPServer.destroyExistingConnectionsAfterUnpair = storage + }) - test("unavailable", () => { + it('unavailable', () => { // @ts-expect-error: private access - accessory.handleRemovePairing(connection, clientUsername1, callback); - expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE); - }); + accessory.handleRemovePairing(connection, clientUsername1, callback) + expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE) + }) - test("missing admin permissions", () => { - accessory._accessoryInfo = accessoryInfoUnpaired; + it('missing admin permissions', () => { + accessory._accessoryInfo = accessoryInfoUnpaired // @ts-expect-error: private access - accessory.handleRemovePairing(connection, clientUsername1, callback); - expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION); - }); - - test.each([PermissionTypes.ADMIN, PermissionTypes.USER])( - "remove pairing: %s", type => { - const count = type === PermissionTypes.ADMIN ? 2 : 1; - accessory._accessoryInfo = accessoryInfoPaired; - accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, type); - expect(accessoryInfoPaired.pairedAdminClients).toEqual(count); + accessory.handleRemovePairing(connection, clientUsername1, callback) + expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION) + }) + + it.each([PermissionTypes.ADMIN, PermissionTypes.USER])( + 'remove pairing: %s', + (type) => { + const count = type === PermissionTypes.ADMIN ? 2 : 1 + accessory._accessoryInfo = accessoryInfoPaired + accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, type) + expect(accessoryInfoPaired.pairedAdminClients).toEqual(count) // @ts-expect-error: private access - accessory.handleRemovePairing(connection, clientUsername1, callback); - expect(callback).toBeCalledWith(0); + accessory.handleRemovePairing(connection, clientUsername1, callback) + expect(callback).toBeCalledWith(0) expect(accessoryInfoPaired.listPairings()) - .toEqual([ defaultPairingInfo ]); - expect(accessoryInfoPaired.pairedAdminClients).toEqual(1); + .toEqual([defaultPairingInfo]) + expect(accessoryInfoPaired.pairedAdminClients).toEqual(1) - expect(EventedHTTPServer.destroyExistingConnectionsAfterUnpair).toBeCalledTimes(1); + expect(EventedHTTPServer.destroyExistingConnectionsAfterUnpair).toBeCalledTimes(1) expect(EventedHTTPServer.destroyExistingConnectionsAfterUnpair) - .toBeCalledWith(connection, clientUsername1); - }); + .toBeCalledWith(connection, clientUsername1) + }, + ) - test("remove last ADMIN pairing", () => { - accessory._accessoryInfo = accessoryInfoPaired; - accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER); + it('remove last ADMIN pairing', () => { + accessory._accessoryInfo = accessoryInfoPaired + accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER) // a mock which just forwards to the normal function call, so that data integrity is ensured - const _removePairedClient0Mock = jest.fn(); + const _removePairedClient0Mock = vi.fn() // @ts-expect-error: private access - _removePairedClient0Mock.mockImplementation(accessoryInfoPaired._removePairedClient0); + _removePairedClient0Mock.mockImplementation(accessoryInfoPaired._removePairedClient0) // @ts-expect-error: private access - accessoryInfoPaired._removePairedClient0 = _removePairedClient0Mock; + accessoryInfoPaired._removePairedClient0 = _removePairedClient0Mock - expect(accessoryInfoPaired.pairedAdminClients).toEqual(1); + expect(accessoryInfoPaired.pairedAdminClients).toEqual(1) // after we removed pairing we also expect that the accessory is advertised as unpaired - const advertiser = new BonjourHAPAdvertiser(accessoryInfoUnpaired); - advertiser.updateAdvertisement = jest.fn(); - accessory._advertiser = advertiser; - const eventMock = jest.fn(); - accessory.on(AccessoryEventTypes.UNPAIRED, eventMock); + const advertiser = new BonjourHAPAdvertiser(accessoryInfoUnpaired) + advertiser.updateAdvertisement = vi.fn() + accessory._advertiser = advertiser + const eventMock = vi.fn() + accessory.on(AccessoryEventTypes.UNPAIRED, eventMock) // @ts-expect-error: private access - accessory.handleRemovePairing(connection, clientUsername0, callback); - expect(callback).toBeCalledWith(0); + accessory.handleRemovePairing(connection, clientUsername0, callback) + expect(callback).toBeCalledWith(0) - expect(accessoryInfoPaired.listPairings()).toEqual([]); - expect(accessoryInfoPaired.pairedAdminClients).toEqual(0); + expect(accessoryInfoPaired.listPairings()).toEqual([]) + expect(accessoryInfoPaired.pairedAdminClients).toEqual(0) // verify calls to _removePairedClient0 - expect(_removePairedClient0Mock).toBeCalledTimes(2); + expect(_removePairedClient0Mock).toBeCalledTimes(2) expect(_removePairedClient0Mock) - .toHaveBeenNthCalledWith(1, connection, clientUsername0); + .toHaveBeenNthCalledWith(1, connection, clientUsername0) expect(_removePairedClient0Mock) - .toHaveBeenNthCalledWith(2, connection, clientUsername1); // it shall also remove the user pairing + .toHaveBeenNthCalledWith(2, connection, clientUsername1) // it shall also remove the user pairing - expect(EventedHTTPServer.destroyExistingConnectionsAfterUnpair).toBeCalledTimes(2); + expect(EventedHTTPServer.destroyExistingConnectionsAfterUnpair).toBeCalledTimes(2) // verify that accessory is marked as unpaired again - expect(advertiser.updateAdvertisement).toBeCalledTimes(1); - expect(eventMock).toBeCalledTimes(1); - }); - }); + expect(advertiser.updateAdvertisement).toBeCalledTimes(1) + expect(eventMock).toBeCalledTimes(1) + }) + }) - describe("handleListPairings", () => { - test("unavailable", () => { + describe('handleListPairings', () => { + it('unavailable', () => { // @ts-expect-error: private access - accessory.handleListPairings(connection, callback); - expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE); - }); + accessory.handleListPairings(connection, callback) + expect(callback).toBeCalledWith(TLVErrorCode.UNAVAILABLE) + }) - test("missing admin permissions", () => { - accessory._accessoryInfo = accessoryInfoUnpaired; + it('missing admin permissions', () => { + accessory._accessoryInfo = accessoryInfoUnpaired // @ts-expect-error: private access - accessory.handleListPairings(connection, callback); - expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION); + accessory.handleListPairings(connection, callback) + expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION) - accessory._accessoryInfo = accessoryInfoPaired; - accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER); - connection.username = clientUsername1; + accessory._accessoryInfo = accessoryInfoPaired + accessoryInfoPaired.addPairedClient(clientUsername1, clientPublicKey1, PermissionTypes.USER) + connection.username = clientUsername1 // @ts-expect-error: private access - accessory.handleListPairings(connection, callback); - expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION); - }); + accessory.handleListPairings(connection, callback) + expect(callback).toBeCalledWith(TLVErrorCode.AUTHENTICATION) + }) - test("list pairings", () => { - accessory._accessoryInfo = accessoryInfoPaired; + it('list pairings', () => { + accessory._accessoryInfo = accessoryInfoPaired // @ts-expect-error: private access - accessory.handleListPairings(connection, callback); - expect(callback).toBeCalledWith(0, [ defaultPairingInfo ]); - }); - }); - }); + accessory.handleListPairings(connection, callback) + expect(callback).toBeCalledWith(0, [defaultPairingInfo]) + }) + }) + }) - describe("published switch service", () => { - let switchService: Service; - let onCharacteristic: Characteristic; + describe('published switch service', () => { + let switchService: Service + let onCharacteristic: Characteristic - const aid = 1; + const aid = 1 const iids = { accessoryInformation: 1, identify: 2, @@ -834,114 +853,115 @@ describe("Accessory", () => { protocolInformation: 11, version: 12, - }; + } beforeEach(() => { - const loadBackup = AccessoryInfo.load; - AccessoryInfo.load = jest.fn(() => { + const loadBackup = AccessoryInfo.load + AccessoryInfo.load = vi.fn(() => { // inject our mocked accessoryInfo object - return accessoryInfoPaired; - }); + return accessoryInfoPaired + }) const publishInfo: PublishInfo = { username: serverUsername, - pincode: "123-45-678", + pincode: '123-45-678', category: Categories.SWITCH, advertiser: MDNSAdvertiser.BONJOUR, - }; + } - switchService = new Service.Switch("Switch"); - accessory.addService(switchService); + switchService = new Service.Switch('Switch') + accessory.addService(switchService) - onCharacteristic = switchService.getCharacteristic(Characteristic.On); + onCharacteristic = switchService.getCharacteristic(Characteristic.On) - accessory.publish(publishInfo); + accessory.publish(publishInfo) - AccessoryInfo.load = loadBackup; + AccessoryInfo.load = loadBackup // saveMock may be called in `publish` - saveMock.mockReset(); + saveMock.mockReset() - expect(aid).toEqual(accessory.aid); - }); + expect(aid).toEqual(accessory.aid) + }) - test("purgeUnusedIDs", () => { - expect(accessory.shouldPurgeUnusedIDs).toBeTruthy(); - accessory.purgeUnusedIDs(); - expect(accessory.shouldPurgeUnusedIDs).toBeTruthy(); + it('purgeUnusedIDs', () => { + expect(accessory.shouldPurgeUnusedIDs).toBeTruthy() + accessory.purgeUnusedIDs() + expect(accessory.shouldPurgeUnusedIDs).toBeTruthy() - accessory.disableUnusedIDPurge(); + accessory.disableUnusedIDPurge() - expect(accessory.shouldPurgeUnusedIDs).toBeFalsy(); - accessory.purgeUnusedIDs(); - expect(accessory.shouldPurgeUnusedIDs).toBeFalsy(); + expect(accessory.shouldPurgeUnusedIDs).toBeFalsy() + accessory.purgeUnusedIDs() + expect(accessory.shouldPurgeUnusedIDs).toBeFalsy() - accessory.enableUnusedIDPurge(); - expect(accessory.shouldPurgeUnusedIDs).toBeTruthy(); - }); + accessory.enableUnusedIDPurge() + expect(accessory.shouldPurgeUnusedIDs).toBeTruthy() + }) - test("setupURI", () => { - let setupURI = accessory.setupURI(); + it('setupURI', () => { + let setupURI = accessory.setupURI() - const originalSetupURI = setupURI; + const originalSetupURI = setupURI - expect(setupURI.startsWith("X-HM://")).toBeTruthy(); - setupURI = setupURI.substring(7); + expect(setupURI.startsWith('X-HM://')).toBeTruthy() + setupURI = setupURI.substring(7) - const encodedPayload = setupURI.substring(0, 9); - const setupId = setupURI.substring(9); + const encodedPayload = setupURI.substring(0, 9) + const setupId = setupURI.substring(9) - expect(setupId).toEqual(accessory._setupID); + expect(setupId).toEqual(accessory._setupID) - const payload = parseInt(encodedPayload, 36); - const low = payload & 0xFFFFFFFF; - const high = (payload - low) / 0x100000000; + const payload = Number.parseInt(encodedPayload, 36) + const low = payload & 0xFFFFFFFF + const high = (payload - low) / 0x100000000 - const setupCode = low & 0x7FFFFFF; - const pairedWithController = (low >> 27) & 0x01; - const supportsIP = (low >> 28) & 0x01; - const supportsBLE = (low >> 29) & 0x01; - const supportsWAC = (low >> 30) & 0x01; - const category = ((low >> 31) & 0x01) + ((high & 0x7F) << 1); - const reserved = (high >> 8) & 0xF; - const version = (high >> 12) & 0x7; + const setupCode = low & 0x7FFFFFF + const pairedWithController = (low >> 27) & 0x01 + const supportsIP = (low >> 28) & 0x01 + const supportsBLE = (low >> 29) & 0x01 + const supportsWAC = (low >> 30) & 0x01 + const category = ((low >> 31) & 0x01) + ((high & 0x7F) << 1) + const reserved = (high >> 8) & 0xF + const version = (high >> 12) & 0x7 - expect(setupCode).toEqual(parseInt(accessory._accessoryInfo!.pincode.replace(/-/g, ""), 10)); - expect(pairedWithController).toEqual(0); - expect(supportsIP).toEqual(1); - expect(supportsBLE).toEqual(0); - expect(supportsWAC).toEqual(0); - expect(category).toEqual(Categories.SWITCH); - expect(reserved).toEqual(0); - expect(version).toEqual(0); + expect(setupCode).toEqual(Number.parseInt(accessory._accessoryInfo!.pincode.replace(/-/g, ''), 10)) + expect(pairedWithController).toEqual(0) + expect(supportsIP).toEqual(1) + expect(supportsBLE).toEqual(0) + expect(supportsWAC).toEqual(0) + expect(category).toEqual(Categories.SWITCH) + expect(reserved).toEqual(0) + expect(version).toEqual(0) - expect(accessory.setupURI()).toEqual(originalSetupURI); - }); + expect(accessory.setupURI()).toEqual(originalSetupURI) + }) - describe("handleAccessories", () => { + describe('handleAccessories', () => { const characteristicHAPInfo = async ( characteristicConstructor: new () => Characteristic, iid: number, value?: CharacteristicValue, - ): Promise => { - const characteristic = new characteristicConstructor(); + ): Promise => { // eslint-disable-line unicorn/consistent-function-scoping + // eslint-disable-next-line new-cap + const characteristic = new characteristicConstructor() if (value !== undefined) { - characteristic.value = value; + characteristic.value = value } - characteristic.iid = iid; - return characteristic.toHAP(connection); - }; + characteristic.iid = iid + return characteristic.toHAP(connection) + } - test("test accessories database retrieval", async () => { + it('test accessories database retrieval', async () => { // @ts-expect-error: private access - accessory.handleAccessories(connection, callback); + accessory.handleAccessories(connection, callback) - await callbackPromise; + await callbackPromise // TODO test Service.toHAP and Characteristic.toHAP separately! const expected: AccessoriesResponse = { accessories: [{ - aid: aid, + aid, services: [ { iid: iids.accessoryInformation, @@ -963,7 +983,7 @@ describe("Accessory", () => { hidden: undefined, primary: undefined, characteristics: [ - await characteristicHAPInfo(Characteristic.Name, iids.switchName, "Switch"), + await characteristicHAPInfo(Characteristic.Name, iids.switchName, 'Switch'), await characteristicHAPInfo(Characteristic.On, iids.on, false), ], }, @@ -973,22 +993,22 @@ describe("Accessory", () => { hidden: undefined, primary: undefined, characteristics: [ - await characteristicHAPInfo(Characteristic.Version, iids.version, "1.1.0"), + await characteristicHAPInfo(Characteristic.Version, iids.version, '1.1.0'), ], }, ], }], - }; - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith(undefined, expected); - }); - }); + } + expect(callback).toBeCalledTimes(1) + expect(callback).toBeCalledWith(undefined, expected) + }) + }) - describe("handleGetCharacteristic", () => { + describe('handleGetCharacteristic', () => { const testRequestResponse = async ( request: Partial, ...expectedReadData: CharacteristicReadData[] - ): Promise => { + ): Promise => { // eslint-disable-line unicorn/consistent-function-scoping // @ts-expect-error: private access accessory.handleGetCharacteristics(connection, { ids: [], @@ -997,104 +1017,104 @@ describe("Accessory", () => { includeType: false, includePerms: false, ...request, - }, callback); + }, callback) - await callbackPromise; + await callbackPromise const expectedResponse: CharacteristicsReadResponse = { characteristics: expectedReadData, - }; + } - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith(undefined, expectedResponse); + expect(callback).toBeCalledTimes(1) + expect(callback).toBeCalledWith(undefined, expectedResponse) - callback.mockReset(); - }; + callback.mockReset() + } - test("read Switch.On characteristic", async () => { + it('read Switch.On characteristic', async () => { await testRequestResponse({ - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, value: 0, - }); + }) // testing that errors are forwarded properly! onCharacteristic.onGet(() => { - throw new HapStatusError(HAPStatus.OUT_OF_RESOURCE); - }); + throw new HapStatusError(HAPStatus.OUT_OF_RESOURCE) + }) await testRequestResponse({ - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.OUT_OF_RESOURCE, - }); - }); + }) + }) - test("read non-existent characteristic", async () => { + it('read non-existent characteristic', async () => { await testRequestResponse({ ids: [{ aid: 2, iid: iids.on }], }, { aid: 2, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); + }) await testRequestResponse({ - ids: [{ aid: aid, iid: 15 }], + ids: [{ aid, iid: 15 }], }, { - aid: aid, + aid, iid: 15, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); - }); + }) + }) - test("reading write-only characteristic", async () => { + it('reading write-only characteristic', async () => { await testRequestResponse({ - ids: [{ aid: aid, iid: iids.identify }], + ids: [{ aid, iid: iids.identify }], }, { - aid: aid, + aid, iid: iids.identify, status: HAPStatus.WRITE_ONLY_CHARACTERISTIC, - }); - }); + }) + }) - test("read includeMeta, includePerms, includeType, includeEvent", async () => { - const ids = [{ aid: aid, iid: iids.on }]; + it('read includeMeta, includePerms, includeType, includeEvent', async () => { + const ids = [{ aid, iid: iids.on }] const partialExpectedResponse = { - aid: aid, + aid, iid: iids.on, value: 0, - }; + } - const customCharacteristic = new Characteristic("Custom", uuid.generate("custom"), { + const customCharacteristic = new Characteristic('Custom', generate('custom'), { format: Formats.UINT64, perms: [Perms.PAIRED_READ], unit: Units.SECONDS, minValue: 10, maxValue: 100, minStep: 2, - }); - customCharacteristic.value = 20; - switchService.addCharacteristic(customCharacteristic); - accessory._assignIDs(accessory._identifierCache!); + }) + customCharacteristic.value = 20 + switchService.addCharacteristic(customCharacteristic) + accessory._assignIDs(accessory._identifierCache!) await testRequestResponse({ - ids: ids, + ids, includeMeta: true, }, { ...partialExpectedResponse, format: Formats.BOOL, - }); + }) await testRequestResponse({ - ids: [{ aid: aid, iid: customCharacteristic.iid! }], + ids: [{ aid, iid: customCharacteristic.iid! }], includeMeta: true, }, { - aid: aid, + aid, iid: customCharacteristic.iid!, value: 20, format: Formats.UINT64, @@ -1102,1140 +1122,1147 @@ describe("Accessory", () => { minValue: 10, maxValue: 100, minStep: 2, - }); + }) await testRequestResponse({ - ids: [{ aid: aid, iid: iids.serialNumber }], + ids: [{ aid, iid: iids.serialNumber }], includeMeta: true, }, { - aid: aid, + aid, iid: 6, - value: "Default-SerialNumber", + value: 'Default-SerialNumber', format: Formats.STRING, maxLen: 64, - }); + }) await testRequestResponse({ - ids: ids, + ids, includePerms: true, }, { ...partialExpectedResponse, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) await testRequestResponse({ - ids: ids, + ids, includeType: true, }, { ...partialExpectedResponse, type: toShortForm(Characteristic.On.UUID), - }); - + }) - const hasEventNotificationsMock: Mock = jest.fn(); - connection.hasEventNotifications = hasEventNotificationsMock; + // @ts-expect-error Generic type Mock requires between 0 and 1 type arguments + const hasEventNotificationsMock: Mock = vi.fn() + connection.hasEventNotifications = hasEventNotificationsMock - hasEventNotificationsMock.mockImplementationOnce(() => true); + hasEventNotificationsMock.mockImplementationOnce(() => true) await testRequestResponse({ - ids: ids, + ids, includeEvent: true, }, { ...partialExpectedResponse, ev: true, - }); - expect(hasEventNotificationsMock).toHaveBeenCalledWith(aid, iids.on); - hasEventNotificationsMock.mockReset(); + }) + expect(hasEventNotificationsMock).toHaveBeenCalledWith(aid, iids.on) + hasEventNotificationsMock.mockReset() - hasEventNotificationsMock.mockImplementationOnce(() => false); + hasEventNotificationsMock.mockImplementationOnce(() => false) await testRequestResponse({ - ids: ids, + ids, includeEvent: true, }, { ...partialExpectedResponse, ev: false, - }); - expect(hasEventNotificationsMock).toHaveBeenCalledWith(aid, iids.on); - }); + }) + expect(hasEventNotificationsMock).toHaveBeenCalledWith(aid, iids.on) + }) - test("reading adminOnly", async () => { + it('reading adminOnly', async () => { onCharacteristic.setProps({ adminOnlyAccess: [Access.READ], - }); + }) await testRequestResponse({ - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, value: 0, - }); + }) - connection.username = clientUsername1; + connection.username = clientUsername1 await testRequestResponse({ - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); + }) - connection.username = undefined; - accessory._accessoryInfo = undefined; + connection.username = undefined + accessory._accessoryInfo = undefined await testRequestResponse({ - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); - }); - }); + }) + }) + }) - describe("handleSetCharacteristic", () => { - let consoleWarnSpy: jest.SpyInstance; + describe('handleSetCharacteristic', () => { + let consoleWarnSpy: MockInstance beforeEach(() => { // Mock console.warn before each test that needs it - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) afterEach(() => { // Restore console.warn after each test - consoleWarnSpy.mockRestore(); - }); + consoleWarnSpy.mockRestore() + }) const testRequestResponse = async ( request: Partial, ...expectedReadData: CharacteristicWriteData[] - ): Promise => { + ): Promise => { // eslint-disable-line unicorn/consistent-function-scoping // @ts-expect-error: private access accessory.handleSetCharacteristics(connection, { characteristics: [], ...request, - }, callback); + }, callback) - await callbackPromise; + await callbackPromise const expectedResponse: CharacteristicsWriteResponse = { characteristics: expectedReadData, - }; + } - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith(undefined, expectedResponse); + expect(callback).toBeCalledTimes(1) + expect(callback).toBeCalledWith(undefined, expectedResponse) - callback.mockReset(); - }; + callback.mockReset() + } - test("write Switch.On characteristic", async () => { + it('write Switch.On characteristic', async () => { await testRequestResponse({ characteristics: [{ - aid: aid, + aid, iid: iids.on, value: true, }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); + }) - expect(onCharacteristic.value).toEqual(true); + expect(onCharacteristic.value).toEqual(true) // testing that errors are forwarder properly onCharacteristic.onSet(() => { - throw new HapStatusError(HAPStatus.RESOURCE_BUSY); - }); + throw new HapStatusError(HAPStatus.RESOURCE_BUSY) + }) await testRequestResponse({ characteristics: [{ - aid: aid, + aid, iid: iids.on, value: true, }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.RESOURCE_BUSY, - }); - }); + }) + }) - test.each([true, false])( - "write-response characteristic with requesting r: %s", async rValue => { - onCharacteristic.props.perms.push(Perms.WRITE_RESPONSE); + it.each([true, false])( + 'write-response characteristic with requesting r: %s', + async (rValue) => { + onCharacteristic.props.perms.push(Perms.WRITE_RESPONSE) onCharacteristic.on(CharacteristicEventTypes.SET, (value, callback) => { - expect(value).toEqual(false); - callback(undefined, true); - }); + expect(value).toEqual(false) + callback(undefined, true) + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: false, r: rValue }], + characteristics: [{ aid, iid: iids.on, value: false, r: rValue }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, value: rValue ? 1 : undefined, - }); - }); + }) + }, + ) - test("requesting write-response on non-write-response characteristic", async () => { + it('requesting write-response on non-write-response characteristic', async () => { onCharacteristic.onSet(() => { - return true; // write response - }); + return true // write response + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: false, r: true }], + characteristics: [{ aid, iid: iids.on, value: false, r: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - }); + }) + }) - test("write non-existent characteristic", async () => { + it('write non-existent characteristic', async () => { await testRequestResponse({ characteristics: [{ aid: 2, iid: iids.on, value: true }], }, { aid: 2, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: 15, value: true }], + characteristics: [{ aid, iid: 15, value: true }], }, { - aid: aid, + aid, iid: 15, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); - }); + }) + }) - test("writing read-only characteristic", async () => { + it('writing read-only characteristic', async () => { await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.model, value: "New Model" }], + characteristics: [{ aid, iid: iids.model, value: 'New Model' }], }, { - aid: aid, + aid, iid: iids.model, status: HAPStatus.READ_ONLY_CHARACTERISTIC, - }); - }); + }) + }) - test("writing adminOnly", async () => { + it('writing adminOnly', async () => { onCharacteristic.setProps({ adminOnlyAccess: [Access.WRITE], - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); + }) - connection.username = clientUsername1; // changing to non-adming username + connection.username = clientUsername1 // changing to non-adming username await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); + }) - connection.username = undefined; - accessory._accessoryInfo = undefined; + connection.username = undefined + accessory._accessoryInfo = undefined await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); - }); + }) + }) - test("enabling and disabling event notifications", async () => { - const hasEventNotificationsMock: Mock = jest.fn(); - connection.hasEventNotifications = hasEventNotificationsMock; - connection.enableEventNotifications = jest.fn(); - connection.disableEventNotifications = jest.fn(); + it('enabling and disabling event notifications', async () => { + // @ts-expect-error Generic type Mock requires between 0 and 1 type arguments + const hasEventNotificationsMock: Mock = vi.fn() + connection.hasEventNotifications = hasEventNotificationsMock + connection.enableEventNotifications = vi.fn() + connection.disableEventNotifications = vi.fn() - hasEventNotificationsMock.mockImplementationOnce(() => false); + hasEventNotificationsMock.mockImplementationOnce(() => false) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: false }], + characteristics: [{ aid, iid: iids.on, ev: false }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.enableEventNotifications).not.toBeCalled(); - expect(connection.disableEventNotifications).not.toBeCalled(); + }) + expect(connection.enableEventNotifications).not.toBeCalled() + expect(connection.disableEventNotifications).not.toBeCalled() // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(0); + expect(onCharacteristic.subscriptions).toEqual(0) - hasEventNotificationsMock.mockImplementationOnce(() => false); + hasEventNotificationsMock.mockImplementationOnce(() => false) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.enableEventNotifications).toHaveBeenCalledWith(aid, iids.on); + }) + expect(connection.enableEventNotifications).toHaveBeenCalledWith(aid, iids.on) // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(1); + expect(onCharacteristic.subscriptions).toEqual(1) - hasEventNotificationsMock.mockImplementationOnce(() => true); + hasEventNotificationsMock.mockImplementationOnce(() => true) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.enableEventNotifications).toBeCalledTimes(1); // >stays< at 1 invocation - expect(connection.disableEventNotifications).not.toBeCalled(); + }) + expect(connection.enableEventNotifications).toBeCalledTimes(1) // >stays< at 1 invocation + expect(connection.disableEventNotifications).not.toBeCalled() // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(1); + expect(onCharacteristic.subscriptions).toEqual(1) - hasEventNotificationsMock.mockImplementationOnce(() => true); + hasEventNotificationsMock.mockImplementationOnce(() => true) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: false }], + characteristics: [{ aid, iid: iids.on, ev: false }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.disableEventNotifications).toHaveBeenCalledWith(aid, iids.on); + }) + expect(connection.disableEventNotifications).toHaveBeenCalledWith(aid, iids.on) // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(0); - }); + expect(onCharacteristic.subscriptions).toEqual(0) + }) - test("unsupported event notifications", async () => { - connection.hasEventNotifications = jest.fn(() => false); + it('unsupported event notifications', async () => { + connection.hasEventNotifications = vi.fn(() => false) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.model, ev: true }], + characteristics: [{ aid, iid: iids.model, ev: true }], }, { - aid: aid, + aid, iid: iids.model, status: HAPStatus.NOTIFICATION_NOT_SUPPORTED, - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.model, ev: false }], + characteristics: [{ aid, iid: iids.model, ev: false }], }, { - aid: aid, + aid, iid: iids.model, status: HAPStatus.NOTIFICATION_NOT_SUPPORTED, - }); - }); + }) + }) - test("clearing event notifications on disconnect", async () => { - const hasEventNotificationsMock: Mock = jest.fn(); - connection.hasEventNotifications = hasEventNotificationsMock; - connection.enableEventNotifications = jest.fn(); + it('clearing event notifications on disconnect', async () => { + // @ts-expect-error Generic type Mock requires between 0 and 1 type arguments + const hasEventNotificationsMock: Mock = vi.fn() + connection.hasEventNotifications = hasEventNotificationsMock + connection.enableEventNotifications = vi.fn() - hasEventNotificationsMock.mockImplementationOnce(() => false); + hasEventNotificationsMock.mockImplementationOnce(() => false) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.enableEventNotifications).toBeCalled(); + }) + expect(connection.enableEventNotifications).toBeCalled() // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(1); + expect(onCharacteristic.subscriptions).toEqual(1) - connection.getRegisteredEvents = jest.fn(() => { - return new Set([aid + "." + iids.on]); - }); - connection.clearRegisteredEvents = jest.fn(); + connection.getRegisteredEvents = vi.fn(() => { + return new Set([`${aid}.${iids.on}`]) + }) + connection.clearRegisteredEvents = vi.fn() // @ts-expect-error: private access - const originalImplementation = accessory.findCharacteristic.bind(accessory); + const originalImplementation = accessory.findCharacteristic.bind(accessory) // @ts-expect-error: private access - accessory.findCharacteristic = jest.fn((aid, iid) => { - return originalImplementation(aid, iid); - }); + accessory.findCharacteristic = vi.fn((aid, iid) => { + return originalImplementation(aid as number, iid as number) + }) // @ts-expect-error: private access - accessory.handleHAPConnectionClosed(connection); + accessory.handleHAPConnectionClosed(connection) - expect(connection.clearRegisteredEvents).toBeCalled(); + expect(connection.clearRegisteredEvents).toBeCalled() // @ts-expect-error: private access - expect(accessory.findCharacteristic).toHaveBeenCalledWith(aid, iids.on); + expect(accessory.findCharacteristic).toHaveBeenCalledWith(aid, iids.on) // @ts-expect-error: private access - expect(onCharacteristic.subscriptions).toEqual(0); - }); + expect(onCharacteristic.subscriptions).toEqual(0) + }) - test("adminOnly notifications", async () => { - connection.hasEventNotifications = jest.fn(() => false); - connection.enableEventNotifications = jest.fn(); - connection.disableEventNotifications = jest.fn(); + it('adminOnly notifications', async () => { + connection.hasEventNotifications = vi.fn(() => false) + connection.enableEventNotifications = vi.fn() + connection.disableEventNotifications = vi.fn() onCharacteristic.setProps({ adminOnlyAccess: [Access.NOTIFY], - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(connection.enableEventNotifications).toHaveBeenCalledTimes(1); + }) + expect(connection.enableEventNotifications).toHaveBeenCalledTimes(1) - connection.username = clientUsername1; // changing to non-admin username + connection.username = clientUsername1 // changing to non-admin username await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); - + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: false }], + characteristics: [{ aid, iid: iids.on, ev: false }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); + }) - connection.username = undefined; - accessory._accessoryInfo = undefined; + connection.username = undefined + accessory._accessoryInfo = undefined await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, ev: true }], + characteristics: [{ aid, iid: iids.on, ev: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_PRIVILEGES, - }); - }); + }) + }) - test("write with additional authorization", async () => { - const handler: Mock = jest.fn(); + it('write with additional authorization', async () => { + // @ts-expect-error Generic type Mock requires between 0 and 1 type arguments. + const handler: Mock = vi.fn() // write with no handler await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true, authData: "xyz" }], + characteristics: [{ aid, iid: iids.on, value: true, authData: 'xyz' }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); + }) - onCharacteristic.setupAdditionalAuthorization(handler); + onCharacteristic.setupAdditionalAuthorization(handler) // allowed to write - handler.mockImplementationOnce(() => true); + handler.mockImplementationOnce(() => true) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true, authData: "xyz" }], + characteristics: [{ aid, iid: iids.on, value: true, authData: 'xyz' }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); - expect(handler).toHaveBeenCalledWith("xyz"); + }) + expect(handler).toHaveBeenCalledWith('xyz') // rejected write - handler.mockImplementationOnce(() => false); + handler.mockImplementationOnce(() => false) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true, authData: "xyz" }], + characteristics: [{ aid, iid: iids.on, value: true, authData: 'xyz' }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_AUTHORIZATION, - }); - expect(handler).toHaveBeenCalledWith("xyz"); + }) + expect(handler).toHaveBeenCalledWith('xyz') // exception handler.mockImplementationOnce(() => { - throw new Error("EXPECTED TEST ERROR"); - }); + throw new Error('EXPECTED TEST ERROR') + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true, authData: "xyz" }], + characteristics: [{ aid, iid: iids.on, value: true, authData: 'xyz' }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INSUFFICIENT_AUTHORIZATION, - }); - expect(handler).toHaveBeenCalledWith("xyz"); - }); + }) + expect(handler).toHaveBeenCalledWith('xyz') + }) - test.each([false, true])( - "timed write with timed write being required: %s", async timedWriteRequired => { + it.each([false, true])( + 'timed write with timed write being required: %s', + async (timedWriteRequired) => { if (timedWriteRequired) { - onCharacteristic.props.perms.push(Perms.TIMED_WRITE); + onCharacteristic.props.perms.push(Perms.TIMED_WRITE) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); + }) } await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], pid: 1337, // pid already expired }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); + }) - connection.timedWritePid = 1337; + connection.timedWritePid = 1337 connection.timedWriteTimeout = setTimeout(() => { - fail(new Error("timed-write timeout was never cleared")); - }, 1000); + throw new Error('timed-write timeout was never cleared') + }, 1000) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], pid: 1336, // invalid pid }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], pid: 1337, // correct pid }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, - }); + }) await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on, value: true }], + characteristics: [{ aid, iid: iids.on, value: true }], pid: 1337, // can't reuse pid }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); - }); + }) + }, + ) - test("empty write", async () => { + it('empty write', async () => { await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.on }], + characteristics: [{ aid, iid: iids.on }], }, { - aid: aid, + aid, iid: iids.on, status: HAPStatus.INVALID_VALUE_IN_REQUEST, - }); - }); + }) + }) - test("Identify Write", async () => { + it('identify Write', async () => { // PAIRED IDENTIFY await testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.identify, value: true }], + characteristics: [{ aid, iid: iids.identify, value: true }], }, { - aid: aid, + aid, iid: iids.identify, status: HAPStatus.SUCCESS, - }); + }) - const eventPromise: Promise<[boolean, IdentifyCallback]> = awaitEventOnce(accessory, AccessoryEventTypes.IDENTIFY); + const eventPromise: Promise<[boolean, IdentifyCallback]> = awaitEventOnce(accessory, AccessoryEventTypes.IDENTIFY) const response = testRequestResponse({ - characteristics: [{ aid: aid, iid: iids.identify, value: true }], + characteristics: [{ aid, iid: iids.identify, value: true }], }, { - aid: aid, + aid, iid: iids.identify, status: HAPStatus.SUCCESS, - }); + }) - const eventResult = await eventPromise; - expect(eventResult[0]).toEqual(true); - eventResult[1](); + const eventResult = await eventPromise + expect(eventResult[0]).toEqual(true) + eventResult[1]() - await response; - }); - }); + await response + }) + }) - describe("handleResource", () => { - let cameraController: CameraController; + describe('handleResource', () => { + let cameraController: CameraController beforeEach(() => { - cameraController = new CameraController(createCameraControllerOptions()); - accessory.configureController(cameraController); - }); + cameraController = new CameraController(createCameraControllerOptions()) + accessory.configureController(cameraController) + }) + // eslint-disable-next-line unicorn/consistent-function-scoping const testRequestResponse = async (partial: Partial = {}) => { // @ts-expect-error: private access accessory.handleResource({ - "resource-type": ResourceRequestType.IMAGE, - "image-width": 200, - "image-height": 200, + 'resource-type': ResourceRequestType.IMAGE, + 'image-width': 200, + 'image-height': 200, ...partial, - }, callback); - - await callbackPromise; + }, callback) - }; + await callbackPromise + } - test("unknown resource type", () => { + it('unknown resource type', () => { // @ts-expect-error: private access - accessory.handleResource({ "resource-type": "unknown" }, callback); + accessory.handleResource({ 'resource-type': 'unknown' }, callback) expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST, - }); - }); + }) + }) - test("missing camera controller", () => { - accessory.removeController(cameraController); + it('missing camera controller', () => { + accessory.removeController(cameraController) // @ts-expect-error: private access accessory.handleResource({ - "resource-type": ResourceRequestType.IMAGE, - "image-width": 200, - "image-height": 200, - }, callback); + 'resource-type': ResourceRequestType.IMAGE, + 'image-width': 200, + 'image-height': 200, + }, callback) expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST, - }); - }); + }) + }) - test("retrieve image resource", async () => { - await testRequestResponse(); - expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE); + it('retrieve image resource', async () => { + await testRequestResponse() + expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE) - await testRequestResponse({ aid: 1 }); - expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE); - }); + await testRequestResponse({ aid: 1 }) + expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE) + }) - test("retrieve image resource on bridge", async () => { - accessory.addBridgedAccessory(new Accessory("Bridged accessory", uuid.generate("bridged"))); - accessory._assignIDs(accessory._identifierCache!); + it('retrieve image resource on bridge', async () => { + accessory.addBridgedAccessory(new Accessory('Bridged accessory', generate('bridged'))) + accessory._assignIDs(accessory._identifierCache!) - await testRequestResponse(); - expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE); + await testRequestResponse() + expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE) - await testRequestResponse({ aid: 1 }); - expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE); - }); + await testRequestResponse({ aid: 1 }) + expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE) + }) - test("retrieve image resource on bridged accessory", async () => { - accessory.removeController(cameraController); + it('retrieve image resource on bridged accessory', async () => { + accessory.removeController(cameraController) - const bridged = new Accessory("Bridged accessory", uuid.generate("bridged")); - bridged.configureController(cameraController); + const bridged = new Accessory('Bridged accessory', generate('bridged')) + bridged.configureController(cameraController) - accessory.addBridgedAccessory(bridged); - accessory._assignIDs(accessory._identifierCache!); - expect(bridged.aid).toBeDefined(); + accessory.addBridgedAccessory(bridged) + accessory._assignIDs(accessory._identifierCache!) + expect(bridged.aid).toBeDefined() - await testRequestResponse(); + await testRequestResponse() expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST, - }); + }) - await testRequestResponse({ aid: 1 }); + await testRequestResponse({ aid: 1 }) expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST, - }); + }) - await testRequestResponse({ aid: bridged.aid! }); - expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE); - }); + await testRequestResponse({ aid: bridged.aid! }) + expect(callback).toHaveBeenCalledWith(undefined, MOCK_IMAGE) + }) - test("properly forward erroneous conditions", async () => { + it('properly forward erroneous conditions', async () => { cameraController.recordingManagement!.operatingModeService .getCharacteristic(Characteristic.HomeKitCameraActive) - .value = false; + .value = false - await testRequestResponse(); - expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.MULTI_STATUS, status: HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE }); - }); - }); + await testRequestResponse() + expect(callback).toHaveBeenCalledWith({ httpCode: HAPHTTPCode.MULTI_STATUS, status: HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE }) + }) + }) - describe("characteristic read/write characteristicWarning", () => { + describe('characteristic read/write characteristicWarning', () => { // @ts-expect-error: private access - const originalWarning = Accessory.TIMEOUT_WARNING; + const originalWarning = Accessory.TIMEOUT_WARNING // @ts-expect-error: private access - const originalTimeoutAfterWarning = Accessory.TIMEOUT_AFTER_WARNING; + const originalTimeoutAfterWarning = Accessory.TIMEOUT_AFTER_WARNING - let characteristicWarningHandler: Mock; + // @ts-expect-error Generic type Mock requires between 0 and 1 type arguments. + let characteristicWarningHandler: Mock const adjustTimeouts = (warning: number, timeout: number) => { // @ts-expect-error: private access - Accessory.TIMEOUT_WARNING = warning; + Accessory.TIMEOUT_WARNING = warning // @ts-expect-error: private access - Accessory.TIMEOUT_AFTER_WARNING = timeout; - }; + Accessory.TIMEOUT_AFTER_WARNING = timeout + } beforeEach(() => { - characteristicWarningHandler = jest.fn(); + characteristicWarningHandler = vi.fn() // @ts-expect-error: private access - accessory.sendCharacteristicWarning = characteristicWarningHandler; - }); + accessory.sendCharacteristicWarning = characteristicWarningHandler + }) afterEach(() => { - adjustTimeouts(originalWarning, originalTimeoutAfterWarning); - }); + adjustTimeouts(originalWarning, originalTimeoutAfterWarning) + }) - test("slow read notification", async () => { - adjustTimeouts(60, 10000); + it('slow read notification', async () => { + adjustTimeouts(60, 10000) - const getEvent: Promise<[CharacteristicGetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.GET); + const getEvent: Promise<[CharacteristicGetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.GET) // @ts-expect-error: private access accessory.handleGetCharacteristics(connection, { - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], includeMeta: false, includeEvent: false, includeType: false, includePerms: false, - }, callback); + }, callback) - await PromiseTimeout(2); - expect(callback).not.toHaveBeenCalled(); + await PromiseTimeout(2) + expect(callback).not.toHaveBeenCalled() - await PromiseTimeout(70); + await PromiseTimeout(70) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_READ, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_READ, expect.anything()) - const eventResult = await getEvent; - eventResult[0](undefined, true); + const eventResult = await getEvent + eventResult[0](undefined, true) - await callbackPromise; + await callbackPromise expect(callback).toHaveBeenCalledWith(undefined, { characteristics: [{ - aid: aid, + aid, iid: iids.on, value: 1, }], - }); - }); + }) + }) - test("timeout read notification", async () => { - adjustTimeouts(10, 50); + it('timeout read notification', async () => { + adjustTimeouts(10, 50) - const getEvent: Promise<[CharacteristicGetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.GET); + const getEvent: Promise<[CharacteristicGetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.GET) // @ts-expect-error: private access accessory.handleGetCharacteristics(connection, { - ids: [{ aid: aid, iid: iids.on }], + ids: [{ aid, iid: iids.on }], includeMeta: false, includeEvent: false, includeType: false, includePerms: false, - }, callback); + }, callback) - await PromiseTimeout(2); - expect(callback).not.toHaveBeenCalled(); + await PromiseTimeout(2) + expect(callback).not.toHaveBeenCalled() - await PromiseTimeout(70); + await PromiseTimeout(70) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_READ, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_READ, expect.anything()) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.TIMEOUT_READ, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.TIMEOUT_READ, expect.anything()) - const eventResult = await getEvent; - eventResult[0](undefined, true); + const eventResult = await getEvent + eventResult[0](undefined, true) - await callbackPromise; + await callbackPromise expect(callback).toHaveBeenCalledWith(undefined, { characteristics: [{ - aid: aid, + aid, iid: iids.on, status: HAPStatus.OPERATION_TIMED_OUT, }], - }); - }); + }) + }) - test("slow write notification", async () => { - adjustTimeouts(60, 10000); + it('slow write notification', async () => { + adjustTimeouts(60, 10000) - const setEvent: Promise<[CharacteristicValue, CharacteristicSetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.SET); + const setEvent: Promise<[CharacteristicValue, CharacteristicSetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.SET) // @ts-expect-error: private access accessory.handleSetCharacteristics(connection, { - characteristics: [{ aid: aid, iid: iids.on, value: true }], - }, callback); + characteristics: [{ aid, iid: iids.on, value: true }], + }, callback) - await PromiseTimeout(2); - expect(callback).not.toHaveBeenCalled(); + await PromiseTimeout(2) + expect(callback).not.toHaveBeenCalled() - await PromiseTimeout(70); + await PromiseTimeout(70) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_WRITE, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_WRITE, expect.anything()) - const eventResult = await setEvent; - expect(eventResult[0]).toBe(true); - eventResult[1](); + const eventResult = await setEvent + expect(eventResult[0]).toBe(true) + eventResult[1]() - await callbackPromise; + await callbackPromise expect(callback).toHaveBeenCalledWith(undefined, { characteristics: [{ - aid: aid, + aid, iid: iids.on, status: HAPStatus.SUCCESS, }], - }); - }); + }) + }) - test("timeout read notification", async () => { - adjustTimeouts(10, 50); + it('timeout write notification', async () => { + adjustTimeouts(10, 50) - const setEvent: Promise<[CharacteristicValue, CharacteristicSetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.SET); + const setEvent: Promise<[CharacteristicValue, CharacteristicSetCallback]> = awaitEventOnce(onCharacteristic, CharacteristicEventTypes.SET) // @ts-expect-error: private access accessory.handleSetCharacteristics(connection, { - characteristics: [{ aid: aid, iid: iids.on, value: true }], - }, callback); + characteristics: [{ aid, iid: iids.on, value: true }], + }, callback) - await PromiseTimeout(2); - expect(callback).not.toHaveBeenCalled(); + await PromiseTimeout(2) + expect(callback).not.toHaveBeenCalled() - await PromiseTimeout(70); + await PromiseTimeout(70) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_WRITE, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.SLOW_WRITE, expect.anything()) expect(characteristicWarningHandler) - .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.TIMEOUT_WRITE, expect.anything()); + .toHaveBeenCalledWith(onCharacteristic, CharacteristicWarningType.TIMEOUT_WRITE, expect.anything()) - const eventResult = await setEvent; - expect(eventResult[0]).toBe(true); - eventResult[1](); + const eventResult = await setEvent + expect(eventResult[0]).toBe(true) + eventResult[1]() - await callbackPromise; + await callbackPromise expect(callback).toHaveBeenCalledWith(undefined, { characteristics: [{ - aid: aid, + aid, iid: iids.on, status: HAPStatus.OPERATION_TIMED_OUT, }], - }); - }); - }); - }); + }) + }) + }) + }) - describe("characteristicWarning", () => { - let consoleWarnSpy: jest.SpyInstance; + describe('characteristicWarning', () => { + let consoleWarnSpy: MockInstance beforeEach(() => { // Mock console.warn before each test that needs it - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) afterEach(() => { // Restore console.warn after each test - consoleWarnSpy.mockRestore(); - }); + consoleWarnSpy.mockRestore() + }) - test("emit characteristic warning", () => { - accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback); + it('emit characteristic warning', () => { + accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback) - const service = accessory.addService(Service.Lightbulb, "Light"); - const on = service.getCharacteristic(Characteristic.On); + const service = accessory.addService(Service.Lightbulb, 'Light') + const on = service.getCharacteristic(Characteristic.On) - on.updateValue({}); - expect(callback).toHaveBeenCalledTimes(1); - }); + on.updateValue({}) + expect(callback).toHaveBeenCalledTimes(1) + }) - test("forward characteristic on bridged accessory", () => { - const bridge = new Bridge("Test bridge", uuid.generate("bridge test")); + it('forward characteristic on bridged accessory', () => { + const bridge = new Bridge('Test bridge', generate('bridge test')) - bridge.addBridgedAccessory(accessory); + bridge.addBridgedAccessory(accessory) - bridge.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback); - accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback); + bridge.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback) + accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, callback) - const service = accessory.addService(Service.Lightbulb, "Light"); - const on = service.getCharacteristic(Characteristic.On); + const service = accessory.addService(Service.Lightbulb, 'Light') + const on = service.getCharacteristic(Characteristic.On) - on.updateValue({}); - expect(callback).toHaveBeenCalledTimes(2); - }); + on.updateValue({}) + expect(callback).toHaveBeenCalledTimes(2) + }) - it("should run without characteristic warning handler", () => { - const service = accessory.addService(Service.Lightbulb, "Light"); - const on = service.getCharacteristic(Characteristic.On); + it('should run without characteristic warning handler', () => { + const service = accessory.addService(Service.Lightbulb, 'Light') + const on = service.getCharacteristic(Characteristic.On) - on.updateValue({}); - }); - }); + on.updateValue({}) + }) + }) - describe("serialize", () => { - test("serialize accessory", () => { - accessory.category = Categories.LIGHTBULB; + describe('serialize', () => { + it('serialize accessory', () => { + accessory.category = Categories.LIGHTBULB - const lightService = new Service.Lightbulb("TestLight", "subtype"); - const switchService = new Service.Switch("TestSwitch", "subtype"); - lightService.addLinkedService(switchService); + const lightService = new Service.Lightbulb('TestLight', 'subtype') + const switchService = new Service.Switch('TestSwitch', 'subtype') + lightService.addLinkedService(switchService) - accessory.addService(lightService); - accessory.addService(switchService); + accessory.addService(lightService) + accessory.addService(switchService) - const json = Accessory.serialize(accessory); - expect(json.displayName).toEqual(accessory.displayName); - expect(json.UUID).toEqual(accessory.UUID); - expect(json.category).toEqual(Categories.LIGHTBULB); + const json = Accessory.serialize(accessory) + expect(json.displayName).toEqual(accessory.displayName) + expect(json.UUID).toEqual(accessory.UUID) + expect(json.category).toEqual(Categories.LIGHTBULB) - expect(json.services).toBeDefined(); - expect(json.services.length).toEqual(3); // 2 above + accessory information service - expect(json.linkedServices).toBeDefined(); - expect(Object.keys(json.linkedServices!)).toEqual([lightService.UUID + "subtype"]); - expect(Object.values(json.linkedServices!)).toEqual([[switchService.UUID + "subtype"]]); - }); - }); + expect(json.services).toBeDefined() + expect(json.services.length).toEqual(3) // 2 above + accessory information service + expect(json.linkedServices).toBeDefined() + expect(Object.keys(json.linkedServices!)).toEqual([`${lightService.UUID}subtype`]) + expect(Object.values(json.linkedServices!)).toEqual([[`${switchService.UUID}subtype`]]) + }) + }) - describe("deserialize", () => { - let consoleWarnSpy: jest.SpyInstance; + describe('deserialize', () => { + let consoleWarnSpy: MockInstance beforeEach(() => { // Mock console.warn before each test that needs it - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) afterEach(() => { // Restore console.warn after each test - consoleWarnSpy.mockRestore(); - }); - - test("deserialize legacy json from homebridge", () => { - const json = JSON.parse("{\"plugin\":\"homebridge-samplePlatform\",\"platform\":\"SamplePlatform\"," + - "\"displayName\":\"2020-01-17T18:45:41.049Z\",\"UUID\":\"dc3951d8-662e-46f7-b6fe-d1b5b5e1a995\",\"category\":1," + - "\"context\":{},\"linkedServices\":{\"0000003E-0000-1000-8000-0026BB765291\":[],\"00000043-0000-1000-8000-0026BB765291\":[]}," + - "\"services\":[{\"UUID\":\"0000003E-0000-1000-8000-0026BB765291\",\"characteristics\":[" + - "{\"displayName\":\"Identify\",\"UUID\":\"00000014-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pw\"]}," + - "\"value\":false,\"eventOnlyCharacteristic\":false},{\"displayName\":\"Manufacturer\",\"UUID\":\"00000020-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Default-Manufacturer\",\"eventOnlyCharacteristic\":false},{\"displayName\":\"Model\"," + - "\"UUID\":\"00000021-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"Default-Model\",\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null," + - "\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"2020-01-17T18:45:41.049Z\"," + - "\"eventOnlyCharacteristic\":false},{\"displayName\":\"Serial Number\",\"UUID\":\"00000030-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Default-SerialNumber\",\"eventOnlyCharacteristic\":false},{\"displayName\":\"Firmware Revision\"," + - "\"UUID\":\"00000052-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"\",\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Product Data\",\"UUID\":\"00000220-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"data\"," + - "\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":null," + - "\"eventOnlyCharacteristic\":false}]},{\"displayName\":\"Test Light\",\"UUID\":\"00000043-0000-1000-8000-0026BB765291\"," + - "\"characteristics\":[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Test Light\",\"eventOnlyCharacteristic\":false},{\"displayName\":\"On\"," + - "\"UUID\":\"00000025-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\",\"pw\",\"ev\"]},\"value\":false,\"eventOnlyCharacteristic\":false}]}]}"); - - const accessory = Accessory.deserialize(json); - - expect(accessory.displayName).toEqual(json.displayName); - expect(accessory.UUID).toEqual(json.UUID); - expect(accessory.category).toEqual(json.category); - - expect(accessory.services).toBeDefined(); - expect(accessory.services.length).toEqual(2); - }); - - test("deserialize complete json", () => { + consoleWarnSpy.mockRestore() + }) + + it('deserialize legacy json from homebridge', () => { + const json = JSON.parse('{"plugin":"homebridge-samplePlatform","platform":"SamplePlatform",' + + '"displayName":"2020-01-17T18:45:41.049Z","UUID":"dc3951d8-662e-46f7-b6fe-d1b5b5e1a995","category":1,' + + '"context":{},"linkedServices":{"0000003E-0000-1000-8000-0026BB765291":[],"00000043-0000-1000-8000-0026BB765291":[]},' + + '"services":[{"UUID":"0000003E-0000-1000-8000-0026BB765291","characteristics":[' + + '{"displayName":"Identify","UUID":"00000014-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pw"]},' + + '"value":false,"eventOnlyCharacteristic":false},{"displayName":"Manufacturer","UUID":"00000020-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Default-Manufacturer","eventOnlyCharacteristic":false},{"displayName":"Model",' + + '"UUID":"00000021-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":"Default-Model","eventOnlyCharacteristic":false},' + + '{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,' + + '"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},"value":"2020-01-17T18:45:41.049Z",' + + '"eventOnlyCharacteristic":false},{"displayName":"Serial Number","UUID":"00000030-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Default-SerialNumber","eventOnlyCharacteristic":false},{"displayName":"Firmware Revision",' + + '"UUID":"00000052-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":"","eventOnlyCharacteristic":false},' + + '{"displayName":"Product Data","UUID":"00000220-0000-1000-8000-0026BB765291","props":{"format":"data",' + + '"unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},"value":null,' + + '"eventOnlyCharacteristic":false}]},{"displayName":"Test Light","UUID":"00000043-0000-1000-8000-0026BB765291",' + + '"characteristics":[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Test Light","eventOnlyCharacteristic":false},{"displayName":"On",' + + '"UUID":"00000025-0000-1000-8000-0026BB765291","props":{"format":"bool","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr","pw","ev"]},"value":false,"eventOnlyCharacteristic":false}]}]}') + + const accessory = Accessory.deserialize(json) + + expect(accessory.displayName).toEqual(json.displayName) + expect(accessory.UUID).toEqual(json.UUID) + expect(accessory.category).toEqual(json.category) + + expect(accessory.services).toBeDefined() + expect(accessory.services.length).toEqual(2) + }) + + it('deserialize complete json', () => { // json for a light accessory - const json = JSON.parse("{\"displayName\":\"TestAccessory\",\"UUID\":\"0beec7b5-ea3f-40fd-bc95-d0dd47f3c5bc\"," + - "\"category\":5,\"services\":[{\"UUID\":\"0000003E-0000-1000-8000-0026BB765291\",\"hiddenService\":false," + - "\"primaryService\":false,\"characteristics\":[{\"displayName\":\"Identify\",\"UUID\":\"00000014-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pw\"]}," + - "\"value\":false,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Manufacturer\",\"UUID\":\"00000020-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Default-Manufacturer\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Model\",\"UUID\":\"00000021-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Default-Model\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"TestAccessory\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Serial Number\",\"UUID\":\"00000030-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\"," + - "\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"Default-SerialNumber\"," + - "\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false},{\"displayName\":\"Firmware Revision\"," + - "\"UUID\":\"00000052-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"1.0\",\"accessRestrictedToAdmins\":[]," + - "\"eventOnlyCharacteristic\":false},{\"displayName\":\"Product Data\"," + - "\"UUID\":\"00000220-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"data\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":null,\"accessRestrictedToAdmins\":[]," + - "\"eventOnlyCharacteristic\":false}],\"optionalCharacteristics\":[{\"displayName\":\"Hardware Revision\"," + - "\"UUID\":\"00000053-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"\",\"accessRestrictedToAdmins\":[]," + - "\"eventOnlyCharacteristic\":false},{\"displayName\":\"Accessory Flags\",\"UUID\":\"000000A6-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"uint32\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\",\"ev\"]}," + - "\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]}," + - "{\"displayName\":\"TestLight\",\"UUID\":\"00000043-0000-1000-8000-0026BB765291\"," + - "\"subtype\":\"subtype\",\"hiddenService\":false,\"primaryService\":false," + - "\"characteristics\":[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"TestLight\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"On\",\"UUID\":\"00000025-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":false,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]," + - "\"optionalCharacteristics\":[{\"displayName\":\"Brightness\",\"UUID\":\"00000008-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"int\",\"unit\":\"percentage\",\"minValue\":0,\"maxValue\":100,\"minStep\":1," + - "\"perms\":[\"pr\",\"pw\",\"ev\"]},\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Hue\",\"UUID\":\"00000013-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"float\",\"unit\":\"arcdegrees\",\"minValue\":0,\"maxValue\":360,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Saturation\",\"UUID\":\"0000002F-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"float\"," + - "\"unit\":\"percentage\",\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]},\"value\":0," + - "\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false},{\"displayName\":\"Name\"," + - "\"UUID\":\"00000023-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null," + - "\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"\"," + - "\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Color Temperature\",\"UUID\":\"000000CE-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"uint32\",\"unit\":null,\"minValue\":140,\"maxValue\":500,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":140,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]}," + - "{\"displayName\":\"TestSwitch\",\"UUID\":\"00000049-0000-1000-8000-0026BB765291\",\"subtype\":\"subtype\"," + - "\"hiddenService\":false,\"primaryService\":false,\"characteristics\":[{\"displayName\":\"Name\"," + - "\"UUID\":\"00000023-0000-1000-8000-0026BB765291\",\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null," + - "\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]},\"value\":\"TestSwitch\",\"accessRestrictedToAdmins\":[]," + - "\"eventOnlyCharacteristic\":false},{\"displayName\":\"On\",\"UUID\":\"00000025-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null," + - "\"perms\":[\"pr\",\"pw\",\"ev\"]},\"value\":false,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]," + - "\"optionalCharacteristics\":[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]}]," + - "\"linkedServices\":{\"00000043-0000-1000-8000-0026BB765291subtype\":[\"00000049-0000-1000-8000-0026BB765291subtype\"]}}"); - - const accessory = Accessory.deserialize(json); - - expect(accessory.displayName).toEqual(json.displayName); - expect(accessory.UUID).toEqual(json.UUID); - expect(accessory.category).toEqual(json.category); - - expect(accessory.services).toBeDefined(); - expect(accessory.services.length).toEqual(3); - expect(accessory.getService(Service.Lightbulb)).toBeDefined(); - expect(accessory.getService(Service.Lightbulb)!.linkedServices.length).toEqual(1); - expect(accessory.getService(Service.Lightbulb)!.linkedServices[0].UUID).toEqual(Service.Switch.UUID); - }); - }); - - describe("parseBindOption", () => { + const json = JSON.parse('{"displayName":"TestAccessory","UUID":"0beec7b5-ea3f-40fd-bc95-d0dd47f3c5bc",' + + '"category":5,"services":[{"UUID":"0000003E-0000-1000-8000-0026BB765291","hiddenService":false,' + + '"primaryService":false,"characteristics":[{"displayName":"Identify","UUID":"00000014-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pw"]},' + + '"value":false,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Manufacturer","UUID":"00000020-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Default-Manufacturer","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Model","UUID":"00000021-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Default-Model","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"TestAccessory","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Serial Number","UUID":"00000030-0000-1000-8000-0026BB765291","props":{"format":"string",' + + '"unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},"value":"Default-SerialNumber",' + + '"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},{"displayName":"Firmware Revision",' + + '"UUID":"00000052-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":"1.0","accessRestrictedToAdmins":[],' + + '"eventOnlyCharacteristic":false},{"displayName":"Product Data",' + + '"UUID":"00000220-0000-1000-8000-0026BB765291","props":{"format":"data","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":null,"accessRestrictedToAdmins":[],' + + '"eventOnlyCharacteristic":false}],"optionalCharacteristics":[{"displayName":"Hardware Revision",' + + '"UUID":"00000053-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":"","accessRestrictedToAdmins":[],' + + '"eventOnlyCharacteristic":false},{"displayName":"Accessory Flags","UUID":"000000A6-0000-1000-8000-0026BB765291",' + + '"props":{"format":"uint32","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr","ev"]},' + + '"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}]},' + + '{"displayName":"TestLight","UUID":"00000043-0000-1000-8000-0026BB765291",' + + '"subtype":"subtype","hiddenService":false,"primaryService":false,' + + '"characteristics":[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"TestLight","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"On","UUID":"00000025-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr","pw","ev"]},' + + '"value":false,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}],' + + '"optionalCharacteristics":[{"displayName":"Brightness","UUID":"00000008-0000-1000-8000-0026BB765291",' + + '"props":{"format":"int","unit":"percentage","minValue":0,"maxValue":100,"minStep":1,' + + '"perms":["pr","pw","ev"]},"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Hue","UUID":"00000013-0000-1000-8000-0026BB765291",' + + '"props":{"format":"float","unit":"arcdegrees","minValue":0,"maxValue":360,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Saturation","UUID":"0000002F-0000-1000-8000-0026BB765291","props":{"format":"float",' + + '"unit":"percentage","minValue":0,"maxValue":100,"minStep":1,"perms":["pr","pw","ev"]},"value":0,' + + '"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},{"displayName":"Name",' + + '"UUID":"00000023-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,' + + '"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},"value":"",' + + '"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Color Temperature","UUID":"000000CE-0000-1000-8000-0026BB765291",' + + '"props":{"format":"uint32","unit":null,"minValue":140,"maxValue":500,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":140,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}]},' + + '{"displayName":"TestSwitch","UUID":"00000049-0000-1000-8000-0026BB765291","subtype":"subtype",' + + '"hiddenService":false,"primaryService":false,"characteristics":[{"displayName":"Name",' + + '"UUID":"00000023-0000-1000-8000-0026BB765291","props":{"format":"string","unit":null,"minValue":null,' + + '"maxValue":null,"minStep":null,"perms":["pr"]},"value":"TestSwitch","accessRestrictedToAdmins":[],' + + '"eventOnlyCharacteristic":false},{"displayName":"On","UUID":"00000025-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,' + + '"perms":["pr","pw","ev"]},"value":false,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}],' + + '"optionalCharacteristics":[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}]}],' + + '"linkedServices":{"00000043-0000-1000-8000-0026BB765291subtype":["00000049-0000-1000-8000-0026BB765291subtype"]}}') + + const accessory = Accessory.deserialize(json) + + expect(accessory.displayName).toEqual(json.displayName) + expect(accessory.UUID).toEqual(json.UUID) + expect(accessory.category).toEqual(json.category) + + expect(accessory.services).toBeDefined() + expect(accessory.services.length).toEqual(3) + expect(accessory.getService(Service.Lightbulb)).toBeDefined() + expect(accessory.getService(Service.Lightbulb)!.linkedServices.length).toEqual(1) + expect(accessory.getService(Service.Lightbulb)!.linkedServices[0].UUID).toEqual(Service.Switch.UUID) + }) + }) + + describe('parseBindOption', () => { const basePublishInfo: PublishInfo = { username: serverUsername, - pincode: "000-00-000", + pincode: '000-00-000', category: Categories.SWITCH, - }; + } const callParseBindOption = (bind: (InterfaceName | IPAddress) | (InterfaceName | IPAddress)[]): { - advertiserAddress?: string[], - serviceRestrictedAddress?: string[], - serviceDisableIpv6?: boolean, - serverAddress?: string, + advertiserAddress?: string[] + serviceRestrictedAddress?: string[] + serviceDisableIpv6?: boolean + serverAddress?: string } => { const info: PublishInfo = { ...basePublishInfo, - bind: bind, - }; + bind, + } // @ts-expect-error: private access - return Accessory.parseBindOption(info); - }; - - test("parse unspecified ipv6 address", () => { - expect(callParseBindOption("::")).toEqual({ - serverAddress: "::", - }); - }); - - test("parse unspecified ipv4 address", () => { - expect(callParseBindOption("0.0.0.0")).toEqual({ - serverAddress: "0.0.0.0", + return Accessory.parseBindOption(info) + } + + it('parse unspecified ipv6 address', () => { + expect(callParseBindOption('::')).toEqual({ + serverAddress: '::', + }) + }) + + it('parse unspecified ipv4 address', () => { + expect(callParseBindOption('0.0.0.0')).toEqual({ + serverAddress: '0.0.0.0', serviceDisableIpv6: true, - }); - }); - - test("parse interface names", () => { - expect(callParseBindOption(["en0", "lo0"])).toEqual({ - serverAddress: "::", - advertiserAddress: ["en0", "lo0"], - serviceRestrictedAddress: ["en0", "lo0"], - }); - }); - - test("parse interface names with explicit ipv6 support", () => { - expect(callParseBindOption(["en0", "lo0", "::"])).toEqual({ - serverAddress: "::", - advertiserAddress: ["en0", "lo0"], - serviceRestrictedAddress: ["en0", "lo0"], - }); - }); - - test("parse interface names ipv4 only", () => { - expect(callParseBindOption(["en0", "lo0", "0.0.0.0"])).toEqual({ - serverAddress: "0.0.0.0", - advertiserAddress: ["en0", "lo0"], - serviceRestrictedAddress: ["en0", "lo0"], + }) + }) + + it('parse interface names', () => { + expect(callParseBindOption(['en0', 'lo0'])).toEqual({ + serverAddress: '::', + advertiserAddress: ['en0', 'lo0'], + serviceRestrictedAddress: ['en0', 'lo0'], + }) + }) + + it('parse interface names with explicit ipv6 support', () => { + expect(callParseBindOption(['en0', 'lo0', '::'])).toEqual({ + serverAddress: '::', + advertiserAddress: ['en0', 'lo0'], + serviceRestrictedAddress: ['en0', 'lo0'], + }) + }) + + it('parse interface names ipv4 only', () => { + expect(callParseBindOption(['en0', 'lo0', '0.0.0.0'])).toEqual({ + serverAddress: '0.0.0.0', + advertiserAddress: ['en0', 'lo0'], + serviceRestrictedAddress: ['en0', 'lo0'], serviceDisableIpv6: true, - }); - }); - - test("parse ipv4 address", () => { - expect(callParseBindOption("169.254.104.90")).toEqual({ - serverAddress: "0.0.0.0", - advertiserAddress: ["169.254.104.90"], - serviceRestrictedAddress: ["169.254.104.90"], - }); - - expect(callParseBindOption(["169.254.104.90", "192.168.1.4"])).toEqual({ - serverAddress: "0.0.0.0", - advertiserAddress: ["169.254.104.90", "192.168.1.4"], - serviceRestrictedAddress: ["169.254.104.90", "192.168.1.4"], - }); - }); - - test("parse ipv6 address", () => { - expect(callParseBindOption("2001:db8::")).toEqual({ - serverAddress: "::", - advertiserAddress: ["2001:db8::"], - serviceRestrictedAddress: ["2001:db8::"], - }); - - expect(callParseBindOption(["2001:db8::", "2001:db8::1"])).toEqual({ - serverAddress: "::", - advertiserAddress: ["2001:db8::", "2001:db8::1"], - serviceRestrictedAddress: ["2001:db8::", "2001:db8::1"], - }); - }); - }); -}); + }) + }) + + it('parse ipv4 address', () => { + expect(callParseBindOption('169.254.104.90')).toEqual({ + serverAddress: '0.0.0.0', + advertiserAddress: ['169.254.104.90'], + serviceRestrictedAddress: ['169.254.104.90'], + }) + + expect(callParseBindOption(['169.254.104.90', '192.168.1.4'])).toEqual({ + serverAddress: '0.0.0.0', + advertiserAddress: ['169.254.104.90', '192.168.1.4'], + serviceRestrictedAddress: ['169.254.104.90', '192.168.1.4'], + }) + }) + + it('parse ipv6 address', () => { + expect(callParseBindOption('2001:db8::')).toEqual({ + serverAddress: '::', + advertiserAddress: ['2001:db8::'], + serviceRestrictedAddress: ['2001:db8::'], + }) + + expect(callParseBindOption(['2001:db8::', '2001:db8::1'])).toEqual({ + serverAddress: '::', + advertiserAddress: ['2001:db8::', '2001:db8::1'], + serviceRestrictedAddress: ['2001:db8::', '2001:db8::1'], + }) + }) + }) +}) class TestController implements Controller { controllerId(): ControllerIdentifier { - return "test-id"; + return 'test-id' } constructServices(): ControllerServiceMap { - const lightService = new Service.Lightbulb("", ""); - const switchService = new Service.Switch("", ""); + const lightService = new Service.Lightbulb('', '') + const switchService = new Service.Switch('', '') return { light: lightService, switch: switchService, - }; + } } initWithServices(serviceMap: ControllerServiceMap): void | ControllerServiceMap { // serviceMap will be altered here to test update procedure - delete serviceMap.switch; - serviceMap.light = new Service.LightSensor("", ""); - serviceMap.outlet = new Service.Outlet("", ""); + delete serviceMap.switch + serviceMap.light = new Service.LightSensor('', '') + serviceMap.outlet = new Service.Outlet('', '') - return serviceMap; + return serviceMap } configureServices(): void { diff --git a/src/lib/Accessory.ts b/src/lib/Accessory.ts index c67b724cf..c877a0242 100644 --- a/src/lib/Accessory.ts +++ b/src/lib/Accessory.ts @@ -1,9 +1,5 @@ -import assert from "assert"; -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import net from "net"; -import { +/* global NodeJS */ +import type { AccessoryJsonObject, CharacteristicId, CharacteristicReadData, @@ -23,67 +19,76 @@ import { PartialCharacteristicReadData, PartialCharacteristicWriteData, ResourceRequest, - ResourceRequestType, VoidCallback, WithUUID, -} from "../types"; -import { Advertiser, AdvertiserEvent, AvahiAdvertiser, BonjourHAPAdvertiser, CiaoAdvertiser, ResolvedAdvertiser } from "./Advertiser"; -// noinspection JSDeprecatedSymbols -import { - Access, - ChangeReason, - Characteristic, - CharacteristicEventTypes, - CharacteristicOperationContext, - CharacteristicSetCallback, - Perms, -} from "./Characteristic"; -import { - CameraController, +} from '../types' +import type { Advertiser } from './Advertiser' +import type { CharacteristicOperationContext, CharacteristicSetCallback } from './Characteristic' +import type { Controller, ControllerConstructor, ControllerIdentifier, ControllerServiceMap, - isSerializableController, -} from "./controller"; -import { +} from './controller' +import type { AccessoriesCallback, AddPairingCallback, - HAPHTTPCode, - HAPServer, - HAPServerEventTypes, - HAPStatus, IdentifyCallback, ListPairingsCallback, PairCallback, ReadCharacteristicsCallback, RemovePairingCallback, ResourceRequestCallback, - TLVErrorCode, WriteCharacteristicsCallback, -} from "./HAPServer"; -import { AccessoryInfo, PermissionTypes } from "./model/AccessoryInfo"; -import { ControllerStorage } from "./model/ControllerStorage"; -import { IdentifierCache } from "./model/IdentifierCache"; -import { SerializedService, Service, ServiceCharacteristicChange, ServiceEventTypes, ServiceId } from "./Service"; -import { clone } from "./util/clone"; -import { EventName, HAPConnection, HAPUsername } from "./util/eventedhttp"; -import { formatOutgoingCharacteristicValue } from "./util/request-util"; -import * as uuid from "./util/uuid"; -import { toShortForm } from "./util/uuid"; -import { checkName } from "./util/checkName"; - -const debug = createDebug("HAP-NodeJS:Accessory"); -const MAX_ACCESSORIES = 149; // Maximum number of bridged accessories per bridge. -const MAX_SERVICES = 100; +} from './HAPServer' +import type { SerializedService, ServiceCharacteristicChange, ServiceId } from './Service' +import type { EventName, HAPConnection, HAPUsername } from './util/eventedhttp' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { isIP } from 'node:net' + +import createDebug from 'debug' + +import { ResourceRequestType } from '../types.js' +import { AdvertiserEvent, AvahiAdvertiser, BonjourHAPAdvertiser, CiaoAdvertiser, ResolvedAdvertiser } from './Advertiser.js' +import { + Access, + ChangeReason, + Characteristic, + CharacteristicEventTypes, + Perms, +} from './Characteristic.js' +import { CameraController, isSerializableController } from './controller/index.js' +import { + HAPHTTPCode, + HAPServer, + HAPServerEventTypes, + HAPStatus, + TLVErrorCode, +} from './HAPServer.js' +import { AccessoryInfo, PermissionTypes } from './model/AccessoryInfo.js' +import { ControllerStorage } from './model/ControllerStorage.js' +import { IdentifierCache } from './model/IdentifierCache.js' +import { Service, ServiceEventTypes } from './Service.js' +import { checkName } from './util/checkName.js' +import { clone } from './util/clone.js' +import { formatOutgoingCharacteristicValue } from './util/request-util.js' +import { isValid, toShortForm } from './util/uuid.js' + +const debug = createDebug('HAP-NodeJS:Accessory') +const MAX_ACCESSORIES = 149 // Maximum number of bridged accessories per bridge. +const MAX_SERVICES = 100 /** * Known category values. Category is a hint to iOS clients about what "type" of Accessory this represents, for UI only. * * @group Accessory */ +// eslint-disable-next-line no-restricted-syntax export const enum Categories { - // noinspection JSUnusedGlobalSymbols OTHER = 1, BRIDGE = 2, FAN = 3, @@ -95,16 +100,18 @@ export const enum Categories { THERMOSTAT = 9, SENSOR = 10, ALARM_SYSTEM = 11, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - SECURITY_SYSTEM = 11, //Added to conform to HAP naming + + // eslint-disable-next-line ts/no-duplicate-enum-values + SECURITY_SYSTEM = 11, // Added to conform to HAP naming DOOR = 12, WINDOW = 13, WINDOW_COVERING = 14, PROGRAMMABLE_SWITCH = 15, RANGE_EXTENDER = 16, CAMERA = 17, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - IP_CAMERA = 17, //Added to conform to HAP naming + + // eslint-disable-next-line ts/no-duplicate-enum-values + IP_CAMERA = 17, // Added to conform to HAP naming VIDEO_DOORBELL = 18, AIR_PURIFIER = 19, AIR_HEATER = 20, @@ -130,67 +137,68 @@ export const enum Categories { * @group Accessory */ export interface SerializedAccessory { - displayName: string, - UUID: string, - lastKnownUsername?: MacAddress, - category: Categories, - - services: SerializedService[], - linkedServices?: Record, - controllers?: SerializedControllerContext[], + displayName: string + UUID: string + lastKnownUsername?: MacAddress + category: Categories + + services: SerializedService[] + linkedServices?: Record + controllers?: SerializedControllerContext[] } /** * @group Controller API */ export interface SerializedControllerContext { - type: ControllerIdentifier, // this field is called type out of history - services: SerializedServiceMap, + type: ControllerIdentifier // this field is called type out of history + services: SerializedServiceMap } /** * @group Controller API */ -export type SerializedServiceMap = Record; // maps controller defined name (from the ControllerServiceMap) to serviceId +export type SerializedServiceMap = Record // maps controller defined name (from the ControllerServiceMap) to serviceId /** * @group Controller API */ export interface ControllerContext { controller: Controller - serviceMap: ControllerServiceMap, + serviceMap: ControllerServiceMap } /** * @group Accessory */ +// eslint-disable-next-line no-restricted-syntax export const enum CharacteristicWarningType { - SLOW_WRITE = "slow-write", - TIMEOUT_WRITE = "timeout-write", - SLOW_READ = "slow-read", - TIMEOUT_READ = "timeout-read", - WARN_MESSAGE = "warn-message", - ERROR_MESSAGE = "error-message", - DEBUG_MESSAGE = "debug-message", + SLOW_WRITE = 'slow-write', + TIMEOUT_WRITE = 'timeout-write', + SLOW_READ = 'slow-read', + TIMEOUT_READ = 'timeout-read', + WARN_MESSAGE = 'warn-message', + ERROR_MESSAGE = 'error-message', + DEBUG_MESSAGE = 'debug-message', } /** * @group Accessory */ export interface CharacteristicWarning { - characteristic: Characteristic, - type: CharacteristicWarningType, - message: string, - originatorChain: string[], - stack?: string, + characteristic: Characteristic + type: CharacteristicWarningType + message: string + originatorChain: string[] + stack?: string } /** * @group Accessory */ export interface PublishInfo { - username: MacAddress; - pincode: HAPPincode; + username: MacAddress + pincode: HAPPincode /** * Specify the category for the HomeKit accessory. * The category is used only in the mdns advertisement and specifies the devices type @@ -199,8 +207,8 @@ export interface PublishInfo { * For the Television and Smart Speaker service it also affects the icon shown in * the Home app when paired. */ - category?: Categories; - setupID?: string; + category?: Categories + setupID?: string /** * Defines the host where the HAP server will be bound to. * When undefined the HAP server will bind to all available interfaces @@ -252,40 +260,41 @@ export interface PublishInfo { * So it is advised to specify an interface name instead of a specific address. * */ - bind?: (InterfaceName | IPAddress) | (InterfaceName | IPAddress)[]; + bind?: (InterfaceName | IPAddress) | (InterfaceName | IPAddress)[] /** * Defines the port where the HAP server will be bound to. * When undefined port 0 will be used resulting in a random port. */ - port?: number; + port?: number /** * If this option is set to true, HAP-NodeJS will add identifying material (based on {@link username}) * to the end of the accessory display name (and bonjour instance name). * Default: true */ - addIdentifyingMaterial?: boolean; + addIdentifyingMaterial?: boolean /** * Defines the advertiser used with the published Accessory. */ - advertiser?: MDNSAdvertiser; + advertiser?: MDNSAdvertiser } /** * @group Accessory */ +// eslint-disable-next-line no-restricted-syntax export const enum MDNSAdvertiser { /** * Use the `@homebridge/ciao` module as advertiser. */ - CIAO = "ciao", + CIAO = 'ciao', /** * Use the `bonjour-hap` module as advertiser. */ - BONJOUR = "bonjour-hap", + BONJOUR = 'bonjour-hap', /** * Use Avahi/D-Bus as advertiser. */ - AVAHI = "avahi", + AVAHI = 'avahi', /** * Use systemd-resolved/D-Bus as advertiser. * @@ -293,32 +302,34 @@ export const enum MDNSAdvertiser { * Therefore, we can't detect if our advertisement might be lost due to a restart of the systemd-resolved daemon restart. * Consequentially, treat this feature as an experimental feature. */ - RESOLVED = "resolved", + RESOLVED = 'resolved', } /** * @group Accessory */ -export type AccessoryCharacteristicChange = ServiceCharacteristicChange & { - service: Service; -}; +export type AccessoryCharacteristicChange = ServiceCharacteristicChange & { + service: Service +} /** * @group Service */ export interface ServiceConfigurationChange { - service: Service; + service: Service } +// eslint-disable-next-line no-restricted-syntax const enum WriteRequestState { REGULAR_REQUEST, TIMED_WRITE_AUTHENTICATED, - TIMED_WRITE_REJECTED + TIMED_WRITE_REJECTED, } /** * @group Accessory */ +// eslint-disable-next-line no-restricted-syntax export const enum AccessoryEventTypes { /** * Emitted when an iOS device wishes for this Accessory to identify itself. If `paired` is false, then @@ -328,57 +339,51 @@ export const enum AccessoryEventTypes { * `getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Identify).on('set', ...)` * You must call the callback for identification to be successful. */ - IDENTIFY = "identify", + IDENTIFY = 'identify', /** * This event is emitted once the HAP TCP socket is bound. * At this point the mdns advertisement isn't yet available. Use the {@link ADVERTISED} if you require the accessory to be discoverable. */ - LISTENING = "listening", + LISTENING = 'listening', /** * This event is emitted once the mDNS suite has fully advertised the presence of the accessory. * This event is guaranteed to be called after {@link LISTENING}. */ - ADVERTISED = "advertised", - SERVICE_CONFIGURATION_CHANGE = "service-configurationChange", + ADVERTISED = 'advertised', + SERVICE_CONFIGURATION_CHANGE = 'service-configurationChange', /** * Emitted after a change in the value of one of the provided Service's Characteristics. */ - SERVICE_CHARACTERISTIC_CHANGE = "service-characteristic-change", - PAIRED = "paired", - UNPAIRED = "unpaired", + SERVICE_CHARACTERISTIC_CHANGE = 'service-characteristic-change', + PAIRED = 'paired', + UNPAIRED = 'unpaired', - CHARACTERISTIC_WARNING = "characteristic-warning", + CHARACTERISTIC_WARNING = 'characteristic-warning', } /** * @group Accessory */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface Accessory { - on(event: "identify", listener: (paired: boolean, callback: VoidCallback) => void): this; - on(event: "listening", listener: (port: number, address: string) => void): this; - on(event: "advertised", listener: () => void): this; - - on(event: "service-configurationChange", listener: (change: ServiceConfigurationChange) => void): this; - on(event: "service-characteristic-change", listener: (change: AccessoryCharacteristicChange) => void): this; - - on(event: "paired", listener: () => void): this; - on(event: "unpaired", listener: () => void): this; - - on(event: "characteristic-warning", listener: (warning: CharacteristicWarning) => void): this; - - - emit(event: "identify", paired: boolean, callback: VoidCallback): boolean; - emit(event: "listening", port: number, address: string): boolean; - emit(event: "advertised"): boolean; - - emit(event: "service-configurationChange", change: ServiceConfigurationChange): boolean; - emit(event: "service-characteristic-change", change: AccessoryCharacteristicChange): boolean; - - emit(event: "paired"): boolean; - emit(event: "unpaired"): boolean; - - emit(event: "characteristic-warning", warning: CharacteristicWarning): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'identify', listener: (paired: boolean, callback: VoidCallback) => void): this + on(event: 'listening', listener: (port: number, address: string) => void): this + on(event: 'advertised', listener: () => void): this + on(event: 'service-configurationChange', listener: (change: ServiceConfigurationChange) => void): this + on(event: 'service-characteristic-change', listener: (change: AccessoryCharacteristicChange) => void): this + on(event: 'paired', listener: () => void): this + on(event: 'unpaired', listener: () => void): this + on(event: 'characteristic-warning', listener: (warning: CharacteristicWarning) => void): this + emit(event: 'identify', paired: boolean, callback: VoidCallback): boolean + emit(event: 'listening', port: number, address: string): boolean + emit(event: 'advertised'): boolean + emit(event: 'service-configurationChange', change: ServiceConfigurationChange): boolean + emit(event: 'service-characteristic-change', change: AccessoryCharacteristicChange): boolean + emit(event: 'paired'): boolean + emit(event: 'unpaired'): boolean + emit(event: 'characteristic-warning', warning: CharacteristicWarning): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -392,83 +397,83 @@ export declare interface Accessory { * * @group Accessory */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class Accessory extends EventEmitter { // Timeout in milliseconds until a characteristic warning is issue - private static readonly TIMEOUT_WARNING = 3000; + private static readonly TIMEOUT_WARNING = 3000 // Timeout in milliseconds after `TIMEOUT_WARNING` until the operation on the characteristic is considered timed out. - private static readonly TIMEOUT_AFTER_WARNING = 6000; + private static readonly TIMEOUT_AFTER_WARNING = 6000 // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions - aid: Nullable = null; // assigned by us in assignIDs() or by a Bridge - _isBridge = false; // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true) - bridged = false; // true if we are hosted "behind" a Bridge Accessory - bridge?: Accessory; // if accessory is bridged, this property points to the bridge which bridges this accessory - bridgedAccessories: Accessory[] = []; // If we are a Bridge, these are the Accessories we are bridging - reachable = true; - lastKnownUsername?: MacAddress; - category: Categories = Categories.OTHER; - services: Service[] = []; - private primaryService?: Service; - shouldPurgeUnusedIDs = true; // Purge unused ids by default + aid: Nullable = null // assigned by us in assignIDs() or by a Bridge + _isBridge = false // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true) + bridged = false // true if we are hosted "behind" a Bridge Accessory + bridge?: Accessory // if accessory is bridged, this property points to the bridge which bridges this accessory + bridgedAccessories: Accessory[] = [] // If we are a Bridge, these are the Accessories we are bridging + reachable = true + lastKnownUsername?: MacAddress + category: Categories = Categories.OTHER + services: Service[] = [] + private primaryService?: Service + shouldPurgeUnusedIDs = true // Purge unused ids by default /** * Captures if initialization steps inside {@link publish} have been called. * This is important when calling {@link publish} multiple times (e.g. after calling {@link unpublish}). - * @private Private API + * @private */ - private initialized = false; + private initialized = false - private controllers: Record = {}; - private serializedControllers?: Record; // store uninitialized controller data after a Accessory.deserialize call - private activeCameraController?: CameraController; + private controllers: Record = {} + private serializedControllers?: Record // store uninitialized controller data after a Accessory.deserialize call + private activeCameraController?: CameraController /** - * @private Private API. + * @private */ - _accessoryInfo?: Nullable; + _accessoryInfo?: Nullable /** - * @private Private API. + * @private */ - _setupID: Nullable = null; + _setupID: Nullable = null /** - * @private Private API. + * @private */ - _identifierCache?: Nullable; + _identifierCache?: Nullable /** - * @private Private API. + * @private */ - controllerStorage: ControllerStorage = new ControllerStorage(this); + controllerStorage: ControllerStorage = new ControllerStorage(this) /** - * @private Private API. + * @private */ - _advertiser?: Advertiser; + _advertiser?: Advertiser /** - * @private Private API. + * @private */ - _server?: HAPServer; + _server?: HAPServer /** - * @private Private API. + * @private */ - _setupURI?: string; + _setupURI?: string - private configurationChangeDebounceTimeout?: NodeJS.Timeout; + private configurationChangeDebounceTimeout?: NodeJS.Timeout /** * This property captures the time when we last served a /accessories request. * For multiple bursts of /accessories request we don't want to always contact GET handlers */ - private lastAccessoriesRequest = 0; + private lastAccessoriesRequest = 0 constructor(public displayName: string, public UUID: string) { - super(); - assert(displayName, "Accessories must be created with a non-empty displayName."); - assert(UUID, "Accessories must be created with a valid UUID."); - assert(uuid.isValid(UUID), "UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a " + - "valid UUID from any arbitrary string, like a serial number."); + super() + assert(displayName, 'Accessories must be created with a non-empty displayName.') + assert(UUID, 'Accessories must be created with a valid UUID.') + assert(isValid(UUID), `UUID '${UUID}' is not a valid UUID. Try using the provided 'generateUUID' function to create a ` + + `valid UUID from any arbitrary string, like a serial number.`) // create our initial "Accessory Information" Service that all Accessories are expected to have - checkName(this.displayName, "Name", displayName); + checkName(this.displayName, 'Name', displayName) this.addService(Service.AccessoryInformation) - .setCharacteristic(Characteristic.Name, displayName); + .setCharacteristic(Characteristic.Name, displayName) // sign up for when iOS attempts to "set" the `Identify` characteristic - this means a paired device wishes // for us to identify ourselves (as opposed to an unpaired device - that case is handled by HAPServer 'identify' event) @@ -476,22 +481,22 @@ export class Accessory extends EventEmitter { .getCharacteristic(Characteristic.Identify)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { if (value) { - const paired = true; - this.identificationRequest(paired, callback); + const paired = true + this.identificationRequest(paired, callback) } - }); + }) } private identificationRequest(paired: boolean, callback: IdentifyCallback) { - debug("[%s] Identification request", this.displayName); + debug('[%s] Identification request', this.displayName) if (this.listeners(AccessoryEventTypes.IDENTIFY).length > 0) { // allow implementors to identify this Accessory in whatever way is appropriate, and pass along // the standard callback for completion. - this.emit(AccessoryEventTypes.IDENTIFY, paired, callback); + this.emit(AccessoryEventTypes.IDENTIFY, paired, callback) } else { - debug("[%s] Identification request ignored; no listeners to 'identify' event", this.displayName); - callback(); + debug('[%s] Identification request ignored; no listeners to \'identify\' event', this.displayName) + callback() } } @@ -509,110 +514,109 @@ export class Accessory extends EventEmitter { * @returns Returns the constructed service instance. */ public addService(serviceConstructor: S, ...constructorArgs: ConstructorArgs): Service - // eslint-disable-next-line @typescript-eslint/no-explicit-any public addService(serviceParam: Service | typeof Service, ...constructorArgs: any[]): Service { // service might be a constructor like `Service.AccessoryInformation` instead of an instance // of Service. Coerce if necessary. - const service: Service = typeof serviceParam === "function" - ? new serviceParam(constructorArgs[0], constructorArgs[1], constructorArgs[2]) - : serviceParam; + const service: Service = typeof serviceParam === 'function' + ? new serviceParam(constructorArgs[0], constructorArgs[1], constructorArgs[2])// eslint-disable-line new-cap + : serviceParam // check for UUID+subtype conflict for (const existing of this.services) { if (existing.UUID === service.UUID) { // OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique. if (!service.subtype) { - throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + - "' as another Service in this Accessory without also defining a unique 'subtype' property."); + throw new Error(`Cannot add a Service with the same UUID '${existing.UUID + }' as another Service in this Accessory without also defining a unique 'subtype' property.`) } if (service.subtype === existing.subtype) { - throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + - "' and subtype '" + existing.subtype + "' as another Service in this Accessory."); + throw new Error(`Cannot add a Service with the same UUID '${existing.UUID + }' and subtype '${existing.subtype}' as another Service in this Accessory.`) } } } if (this.services.length >= MAX_SERVICES) { - throw new Error("Cannot add more than " + MAX_SERVICES + " services to a single accessory!"); + throw new Error(`Cannot add more than ${MAX_SERVICES} services to a single accessory!`) } - this.services.push(service); + this.services.push(service) if (service.isPrimaryService) { // check if a primary service was added if (this.primaryService !== undefined) { - this.primaryService.isPrimaryService = false; + this.primaryService.isPrimaryService = false } - this.primaryService = service; + this.primaryService = service } if (!this.bridged) { - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } else { - this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service }); + this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service }) } - this.setupServiceEventHandlers(service); + this.setupServiceEventHandlers(service) - return service; + return service } public removeService(service: Service): void { - const index = this.services.indexOf(service); + const index = this.services.indexOf(service) if (index >= 0) { - this.services.splice(index, 1); + this.services.splice(index, 1) if (this.primaryService === service) { // check if we are removing out primary service - this.primaryService = undefined; + this.primaryService = undefined } - this.removeLinkedService(service); // remove it from linked service entries on the local accessory + this.removeLinkedService(service) // remove it from linked service entries on the local accessory if (!this.bridged) { - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } else { - this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service }); + this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service }) } - service.removeAllListeners(); + service.removeAllListeners() } } private removeLinkedService(removed: Service) { for (const service of this.services) { - service.removeLinkedService(removed); + service.removeLinkedService(removed) } } public getService>(name: string | T): Service | undefined { for (const service of this.services) { - if (typeof name === "string" && (service.displayName === name || service.name === name || service.subtype === name)) { - return service; + if (typeof name === 'string' && (service.displayName === name || service.name === name || service.subtype === name)) { + return service } else { // @ts-expect-error ('UUID' does not exist on type 'never') - if (typeof name === "function" && ((service instanceof name) || (name.UUID === service.UUID))) { - return service; + if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID))) { + return service } } } - return undefined; + return undefined } public getServiceById>(uuid: string | T, subType: string): Service | undefined { for (const service of this.services) { - if (typeof uuid === "string" && (service.displayName === uuid || service.name === uuid) && service.subtype === subType) { - return service; + if (typeof uuid === 'string' && (service.displayName === uuid || service.name === uuid) && service.subtype === subType) { + return service } else { // @ts-expect-error ('UUID' does not exist on type 'never') - if (typeof uuid === "function" && ((service instanceof uuid) || (uuid.UUID === service.UUID)) && service.subtype === subType) { - return service; + if (typeof uuid === 'function' && ((service instanceof uuid) || (uuid.UUID === service.UUID)) && service.subtype === subType) { + return service } } } - return undefined; + return undefined } /** @@ -622,108 +626,108 @@ export class Accessory extends EventEmitter { * @returns the primary accessory */ public getPrimaryAccessory = (): Accessory => { - return this.bridged? this.bridge!: this; - }; + return this.bridged ? this.bridge! : this + } public addBridgedAccessory(accessory: Accessory, deferUpdate = false): Accessory { if (accessory._isBridge || accessory === this) { - throw new Error("Illegal state: either trying to bridge a bridge or trying to bridge itself!"); + throw new Error('Illegal state: either trying to bridge a bridge or trying to bridge itself!') } if (accessory.initialized) { - throw new Error("Tried to bridge an accessory which was already published once!"); + throw new Error('Tried to bridge an accessory which was already published once!') } if (accessory.bridge != null) { // this also prevents that we bridge the same accessory twice! - throw new Error("Tried to bridge " + accessory.displayName + " while it was already bridged by " + accessory.bridge.displayName); + throw new Error(`Tried to bridge ${accessory.displayName} while it was already bridged by ${accessory.bridge.displayName}`) } if (this.bridgedAccessories.length >= MAX_ACCESSORIES) { - throw new Error("Cannot Bridge more than " + MAX_ACCESSORIES + " Accessories"); + throw new Error(`Cannot Bridge more than ${MAX_ACCESSORIES} Accessories`) } // listen for changes in ANY characteristics of ANY services on this Accessory - accessory.on(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, change => this.handleCharacteristicChangeEvent(accessory, change.service, change)); - accessory.on(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, this.enqueueConfigurationUpdate.bind(this)); - accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this)); + accessory.on(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, change => this.handleCharacteristicChangeEvent(accessory, change.service, change)) + accessory.on(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, this.enqueueConfigurationUpdate.bind(this)) + accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this)) - accessory.bridged = true; - accessory.bridge = this; + accessory.bridged = true + accessory.bridge = this - this.bridgedAccessories.push(accessory); + this.bridgedAccessories.push(accessory) - this.controllerStorage.linkAccessory(accessory); // init controllers of bridged accessory + this.controllerStorage.linkAccessory(accessory) // init controllers of bridged accessory - if(!deferUpdate) { - this.enqueueConfigurationUpdate(); + if (!deferUpdate) { + this.enqueueConfigurationUpdate() } - return accessory; + return accessory } public addBridgedAccessories(accessories: Accessory[]): void { for (const accessory of accessories) { - this.addBridgedAccessory(accessory, true); + this.addBridgedAccessory(accessory, true) } - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } public removeBridgedAccessory(accessory: Accessory, deferUpdate = false): void { // check for UUID conflict - const accessoryIndex = this.bridgedAccessories.indexOf(accessory); + const accessoryIndex = this.bridgedAccessories.indexOf(accessory) if (accessoryIndex === -1) { - throw new Error("Cannot find the bridged Accessory to remove."); + throw new Error('Cannot find the bridged Accessory to remove.') } - this.bridgedAccessories.splice(accessoryIndex, 1); + this.bridgedAccessories.splice(accessoryIndex, 1) - accessory.bridged = false; - accessory.bridge = undefined; - accessory.removeAllListeners(); + accessory.bridged = false + accessory.bridge = undefined + accessory.removeAllListeners() - if(!deferUpdate) { - this.enqueueConfigurationUpdate(); + if (!deferUpdate) { + this.enqueueConfigurationUpdate() } } public removeBridgedAccessories(accessories: Accessory[]): void { for (const accessory of accessories) { - this.removeBridgedAccessory(accessory, true); + this.removeBridgedAccessory(accessory, true) } - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } public removeAllBridgedAccessories(): void { - for (let i = this.bridgedAccessories.length - 1; i >= 0; i --) { - this.removeBridgedAccessory(this.bridgedAccessories[i], true); + for (let i = this.bridgedAccessories.length - 1; i >= 0; i--) { + this.removeBridgedAccessory(this.bridgedAccessories[i], true) } - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } private getCharacteristicByIID(iid: number): Characteristic | undefined { for (const service of this.services) { - const characteristic = service.getCharacteristicByIID(iid); + const characteristic = service.getCharacteristicByIID(iid) if (characteristic) { - return characteristic; + return characteristic } } } protected getAccessoryByAID(aid: number): Accessory | undefined { if (this.aid === aid) { - return this; + return this } - return this.bridgedAccessories.find(value => value.aid === aid); + return this.bridgedAccessories.find(value => value.aid === aid) } protected findCharacteristic(aid: number, iid: number): Characteristic | undefined { - const accessory = this.getAccessoryByAID(aid); - return accessory && accessory.getCharacteristicByIID(iid); + const accessory = this.getAccessoryByAID(aid) + return accessory && accessory.getCharacteristicByIID(iid) } /** @@ -741,61 +745,61 @@ export class Accessory extends EventEmitter { * @param controllerConstructor - The Controller instance or constructor to the Controller with no required arguments. */ public configureController(controllerConstructor: Controller | ControllerConstructor): void { - const controller = typeof controllerConstructor === "function" - ? new controllerConstructor() // any custom constructor arguments should be passed before using .bind(...) - : controllerConstructor; - const id = controller.controllerId(); + // any custom constructor arguments should be passed before using .bind(...) + const controller = typeof controllerConstructor === 'function' + ? new controllerConstructor() // eslint-disable-line new-cap + : controllerConstructor + const id = controller.controllerId() if (this.controllers[id]) { - throw new Error(`A Controller with the type/id '${id}' was already added to the accessory ${this.displayName}`); + throw new Error(`A Controller with the type/id '${id}' was already added to the accessory ${this.displayName}`) } - const savedServiceMap = this.serializedControllers && this.serializedControllers[id]; - let serviceMap: ControllerServiceMap; + const savedServiceMap = this.serializedControllers && this.serializedControllers[id] + let serviceMap: ControllerServiceMap if (savedServiceMap) { // we found data to restore from - const clonedServiceMap = clone(savedServiceMap); - const updatedServiceMap = controller.initWithServices(savedServiceMap); // init controller with existing services - serviceMap = updatedServiceMap || savedServiceMap; // initWithServices could return an updated serviceMap, otherwise just use the existing one + const clonedServiceMap = clone(savedServiceMap) + const updatedServiceMap = controller.initWithServices(savedServiceMap) // init controller with existing services + serviceMap = updatedServiceMap || savedServiceMap // initWithServices could return an updated serviceMap, otherwise just use the existing one if (updatedServiceMap) { // controller returned a ServiceMap and thus signaled an updated set of services // clonedServiceMap is altered by this method, should not be touched again after this call (for the future people) - this.handleUpdatedControllerServiceMap(clonedServiceMap, updatedServiceMap); + this.handleUpdatedControllerServiceMap(clonedServiceMap, updatedServiceMap) } - controller.configureServices(); // let the controller setup all its handlers + controller.configureServices() // let the controller setup all its handlers // remove serialized data from our dictionary: - delete this.serializedControllers![id]; + delete this.serializedControllers![id] if (Object.entries(this.serializedControllers!).length === 0) { - this.serializedControllers = undefined; + this.serializedControllers = undefined } } else { - serviceMap = controller.constructServices(); // let the controller create his services - controller.configureServices(); // let the controller setup all its handlers + serviceMap = controller.constructServices() // let the controller create his services + controller.configureServices() // let the controller setup all its handlers - Object.values(serviceMap).forEach(service => { + Object.values(serviceMap).forEach((service) => { if (service && !this.services.includes(service)) { - this.addService(service); + this.addService(service) } - }); + }) } - // --- init handlers and setup context --- const context: ControllerContext = { - controller: controller, - serviceMap: serviceMap, - }; + controller, + serviceMap, + } if (isSerializableController(controller)) { - this.controllerStorage.trackController(controller); + this.controllerStorage.trackController(controller) } - this.controllers[id] = context; + this.controllers[id] = context if (controller instanceof CameraController) { // save CameraController for Snapshot handling - this.activeCameraController = controller; + this.activeCameraController = controller } } @@ -807,119 +811,119 @@ export class Accessory extends EventEmitter { * @param controller - The controller which should be removed from the accessory. */ public removeController(controller: Controller): void { - const id = controller.controllerId(); + const id = controller.controllerId() - const storedController = this.controllers[id]; + const storedController = this.controllers[id] if (storedController) { if (storedController.controller !== controller) { - throw new Error("[" + this.displayName + "] tried removing a controller with the id/type '" + id + - "' though provided controller isn't the same instance that is registered!"); + throw new Error(`[${this.displayName}] tried removing a controller with the id/type '${id + }' though provided controller isn't the same instance that is registered!`) } if (isSerializableController(controller)) { // this will reset the state change delegate before we call handleControllerRemoved() - this.controllerStorage.untrackController(controller); + this.controllerStorage.untrackController(controller) } if (controller.handleFactoryReset) { - controller.handleFactoryReset(); + controller.handleFactoryReset() } - controller.handleControllerRemoved(); + controller.handleControllerRemoved() - delete this.controllers[id]; + delete this.controllers[id] if (this.activeCameraController === controller) { - this.activeCameraController = undefined; + this.activeCameraController = undefined } - Object.values(storedController.serviceMap).forEach(service => { + Object.values(storedController.serviceMap).forEach((service) => { if (service) { - this.removeService(service); + this.removeService(service) } - }); + }) } if (this.serializedControllers) { - delete this.serializedControllers[id]; + delete this.serializedControllers[id] } } private handleAccessoryUnpairedForControllers(): void { for (const context of Object.values(this.controllers)) { - const controller = context.controller; + const controller = context.controller if (controller.handleFactoryReset) { // if the controller implements handleFactoryReset, setup event handlers for this controller - controller.handleFactoryReset(); + controller.handleFactoryReset() } if (isSerializableController(controller)) { - this.controllerStorage.purgeControllerData(controller); + this.controllerStorage.purgeControllerData(controller) } } } private handleUpdatedControllerServiceMap(originalServiceMap: ControllerServiceMap, updatedServiceMap: ControllerServiceMap) { - updatedServiceMap = clone(updatedServiceMap); // clone it so we can alter it + updatedServiceMap = clone(updatedServiceMap) // clone it so we can alter it - Object.keys(originalServiceMap).forEach(name => { // this loop removed any services contained in both ServiceMaps - const service = originalServiceMap[name]; - const updatedService = updatedServiceMap[name]; + Object.keys(originalServiceMap).forEach((name) => { // this loop removed any services contained in both ServiceMaps + const service = originalServiceMap[name] + const updatedService = updatedServiceMap[name] if (service && updatedService) { // we check all names contained in both ServiceMaps for changes - delete originalServiceMap[name]; // delete from original ServiceMap, so it will only contain deleted services at the end - delete updatedServiceMap[name]; // delete from updated ServiceMap, so it will only contain added services at the end + delete originalServiceMap[name] // delete from original ServiceMap, so it will only contain deleted services at the end + delete updatedServiceMap[name] // delete from updated ServiceMap, so it will only contain added services at the end if (service !== updatedService) { - this.removeService(service); - this.addService(updatedService); + this.removeService(service) + this.addService(updatedService) } } - }); + }) // now originalServiceMap contains only deleted services and updateServiceMap only added services - Object.values(originalServiceMap).forEach(service => { + Object.values(originalServiceMap).forEach((service) => { if (service) { - this.removeService(service); + this.removeService(service) } - }); - Object.values(updatedServiceMap).forEach(service => { + }) + Object.values(updatedServiceMap).forEach((service) => { if (service) { - this.addService(service); + this.addService(service) } - }); + }) } setupURI(): string { if (this._setupURI) { - return this._setupURI; + return this._setupURI } - assert(!!this._accessoryInfo, "Cannot generate setupURI on an accessory that isn't published yet!"); + assert(!!this._accessoryInfo, 'Cannot generate setupURI on an accessory that isn\'t published yet!') - const buffer = Buffer.alloc(8); - let value_low = parseInt(this._accessoryInfo.pincode.replace(/-/g, ""), 10); - const value_high = this._accessoryInfo.category >> 1; + const buffer = Buffer.alloc(8) + let value_low = Number.parseInt(this._accessoryInfo.pincode.replace(/-/g, ''), 10) + const value_high = this._accessoryInfo.category >> 1 - value_low |= 1 << 28; // Supports IP; + value_low |= 1 << 28 // Supports IP; - buffer.writeUInt32BE(value_low, 4); + buffer.writeUInt32BE(value_low, 4) - if (this._accessoryInfo.category & 1) { - buffer[4] = buffer[4] | 1 << 7; + if (this._accessoryInfo.category % 2 !== 0) { // check if the category is odd + buffer[4] += 128 // set the 7th bit by adding 128 } - buffer.writeUInt32BE(value_high, 0); + buffer.writeUInt32BE(value_high, 0) - let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 0x100000000)).toString(36).toUpperCase(); + let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 0x100000000)).toString(36).toUpperCase() if (encodedPayload.length !== 9) { for (let i = 0; i <= 9 - encodedPayload.length; i++) { - encodedPayload = "0" + encodedPayload; + encodedPayload = `0${encodedPayload}` } } - this._setupURI = "X-HM://" + encodedPayload + this._setupID; - return this._setupURI; + this._setupURI = `X-HM://${encodedPayload}${this._setupID}` + return this._setupURI } /** @@ -928,92 +932,92 @@ export class Accessory extends EventEmitter { * If it is called on a bridge it will call this method for all bridged accessories. */ private validateAccessory(mainAccessory?: boolean) { - const service = this.getService(Service.AccessoryInformation); + const service = this.getService(Service.AccessoryInformation) if (!service) { - console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published without a AccessoryInformation service. " + - "This might prevent the accessory from being added to the Home app or leading to the accessory being unresponsive!"); + // eslint-disable-next-line no-console + console.log(`HAP-NodeJS WARNING: The accessory '${this.displayName}' is getting published without a AccessoryInformation service. ` + + `This might prevent the accessory from being added to the Home app or leading to the accessory being unresponsive!`) } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const checkValue = (name: string, value?: any) => { if (!value) { - console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published with the characteristic '" + name + "'" + - " (of the AccessoryInformation service) not having a value set. " + - "This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!"); + // eslint-disable-next-line no-console + console.log(`HAP-NodeJS WARNING: The accessory '${this.displayName}' is getting published with the characteristic '${name}'` + + ` (of the AccessoryInformation service) not having a value set. ` + + `This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!`) } - }; + } - checkName(this.displayName, "Name", service.getCharacteristic(Characteristic.Name).value); - checkValue("FirmwareRevision", service.getCharacteristic(Characteristic.FirmwareRevision).value); - checkValue("Manufacturer", service.getCharacteristic(Characteristic.Manufacturer).value); - checkValue("Model", service.getCharacteristic(Characteristic.Model).value); - checkValue("Name", service.getCharacteristic(Characteristic.Name).value); - checkValue("SerialNumber", service.getCharacteristic(Characteristic.SerialNumber).value); + checkName(this.displayName, 'Name', service.getCharacteristic(Characteristic.Name).value) + checkValue('FirmwareRevision', service.getCharacteristic(Characteristic.FirmwareRevision).value) + checkValue('Manufacturer', service.getCharacteristic(Characteristic.Manufacturer).value) + checkValue('Model', service.getCharacteristic(Characteristic.Model).value) + checkValue('Name', service.getCharacteristic(Characteristic.Name).value) + checkValue('SerialNumber', service.getCharacteristic(Characteristic.SerialNumber).value) } if (mainAccessory) { // the main accessory which is advertised via bonjour must have a name with length <= 63 (limitation of DNS FQDN names) - assert(Buffer.from(this.displayName, "utf8").length <= 63, "Accessory displayName cannot be longer than 63 bytes!"); + assert(Buffer.from(this.displayName, 'utf8').length <= 63, 'Accessory displayName cannot be longer than 63 bytes!') } if (this.bridged) { - this.bridgedAccessories.forEach(accessory => accessory.validateAccessory()); + this.bridgedAccessories.forEach(accessory => accessory.validateAccessory()) } } /** * Assigns aid/iid to ourselves, any Accessories we are bridging, and all associated Services+Characteristics. Uses * the provided identifierCache to keep IDs stable. - * @private Private API + * @private */ _assignIDs(identifierCache: IdentifierCache): void { - // if we are responsible for our own identifierCache, start the expiration process // also check weather we want to have an expiration process if (this._identifierCache && this.shouldPurgeUnusedIDs) { - this._identifierCache.startTrackingUsage(); + this._identifierCache.startTrackingUsage() } if (this.bridged) { // This Accessory is bridged, so it must have an aid > 1. Use the provided identifierCache to // fetch or assign one based on our UUID. - this.aid = identifierCache.getAID(this.UUID); + this.aid = identifierCache.getAID(this.UUID) } else { // Since this Accessory is the server (as opposed to any Accessories that may be bridged behind us), // we must have aid = 1 - this.aid = 1; + this.aid = 1 } for (const service of this.services) { if (this._isBridge) { - service._assignIDs(identifierCache, this.UUID, 2000000000); + service._assignIDs(identifierCache, this.UUID, 2000000000) } else { - service._assignIDs(identifierCache, this.UUID); + service._assignIDs(identifierCache, this.UUID) } } // now assign IDs for any Accessories we are bridging for (const accessory of this.bridgedAccessories) { - accessory._assignIDs(identifierCache); + accessory._assignIDs(identifierCache) } // expire any now-unused cache keys (for Accessories, Services, or Characteristics // that have been removed since the last call to assignIDs()) if (this._identifierCache) { - //Check weather we want to purge the unused ids + // Check weather we want to purge the unused ids if (this.shouldPurgeUnusedIDs) { - this._identifierCache.stopTrackingUsageAndExpireUnused(); + this._identifierCache.stopTrackingUsageAndExpireUnused() } - //Save in case we have new ones - this._identifierCache.save(); + // Save in case we have new ones + this._identifierCache.save() } } disableUnusedIDPurge(): void { - this.shouldPurgeUnusedIDs = false; + this.shouldPurgeUnusedIDs = false } enableUnusedIDPurge(): void { - this.shouldPurgeUnusedIDs = true; + this.shouldPurgeUnusedIDs = true } /** @@ -1021,39 +1025,39 @@ export class Accessory extends EventEmitter { * when you have disabled auto purge, so you can do it manually */ purgeUnusedIDs(): void { - //Cache the state of the purge mechanism and set it to true - const oldValue = this.shouldPurgeUnusedIDs; - this.shouldPurgeUnusedIDs = true; + // Cache the state of the purge mechanism and set it to true + const oldValue = this.shouldPurgeUnusedIDs + this.shouldPurgeUnusedIDs = true - //Reassign all ids - this._assignIDs(this._identifierCache!); + // Reassign all ids + this._assignIDs(this._identifierCache!) // Revert the purge mechanism state - this.shouldPurgeUnusedIDs = oldValue; + this.shouldPurgeUnusedIDs = oldValue } /** * Returns a JSON representation of this accessory suitable for delivering to HAP clients. */ private async toHAP(connection: HAPConnection, contactGetHandlers = true): Promise { - assert(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'"); - assert(this.services.length, "accessory '" + this.displayName + "' does not have any services!"); + assert(this.aid, `aid cannot be undefined for accessory '${this.displayName}'`) + assert(this.services.length, `accessory '${this.displayName}' does not have any services!`) const accessory: AccessoryJsonObject = { aid: this.aid!, services: await Promise.all(this.services.map(service => service.toHAP(connection, contactGetHandlers))), - }; + } - const accessories: AccessoryJsonObject[] = [accessory]; + const accessories: AccessoryJsonObject[] = [accessory] if (!this.bridged) { - accessories.push(... await Promise.all( + accessories.push(...await Promise.all( this.bridgedAccessories .map(accessory => accessory.toHAP(connection, contactGetHandlers).then(value => value[0])), - )); + )) } - return accessories; + return accessories } /** @@ -1061,25 +1065,25 @@ export class Accessory extends EventEmitter { */ private internalHAPRepresentation(assignIds = true): AccessoryJsonObject[] { if (assignIds) { - this._assignIDs(this._identifierCache!); // make sure our aid/iid's are all assigned + this._assignIDs(this._identifierCache!) // make sure our aid/iid's are all assigned } - assert(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'"); - assert(this.services.length, "accessory '" + this.displayName + "' does not have any services!"); + assert(this.aid, `aid cannot be undefined for accessory '${this.displayName}'`) + assert(this.services.length, `accessory '${this.displayName}' does not have any services!`) const accessory: AccessoryJsonObject = { aid: this.aid!, services: this.services.map(service => service.internalHAPRepresentation()), - }; + } - const accessories: AccessoryJsonObject[] = [accessory]; + const accessories: AccessoryJsonObject[] = [accessory] if (!this.bridged) { for (const accessory of this.bridgedAccessories) { - accessories.push(accessory.internalHAPRepresentation(false)[0]); + accessories.push(accessory.internalHAPRepresentation(false)[0]) } } - return accessories; + return accessories } /** @@ -1101,60 +1105,60 @@ export class Accessory extends EventEmitter { */ public async publish(info: PublishInfo, allowInsecureRequest?: boolean): Promise { if (this.bridged) { - throw new Error("Can't publish in accessory which is bridged by another accessory. Bridged by " + this.bridge?.displayName); + throw new Error(`Can't publish in accessory which is bridged by another accessory. Bridged by ${this.bridge?.displayName}`) } - let service = this.getService(Service.ProtocolInformation); + let service = this.getService(Service.ProtocolInformation) if (!service) { - service = this.addService(Service.ProtocolInformation); // add the protocol information service to the primary accessory + service = this.addService(Service.ProtocolInformation) // add the protocol information service to the primary accessory } - service.setCharacteristic(Characteristic.Version, CiaoAdvertiser.protocolVersionService); + service.setCharacteristic(Characteristic.Version, CiaoAdvertiser.protocolVersionService) if (this.lastKnownUsername && this.lastKnownUsername !== info.username) { // username changed since last publish - Accessory.cleanupAccessoryData(this.lastKnownUsername); // delete old Accessory data + Accessory.cleanupAccessoryData(this.lastKnownUsername) // delete old Accessory data } if (!this.initialized && (info.addIdentifyingMaterial ?? true)) { // adding some identifying material to our displayName if it's our first publish() call - this.displayName = this.displayName + " " + crypto.createHash("sha512") - .update(info.username, "utf8") - .digest("hex").slice(0, 4).toUpperCase(); - this.getService(Service.AccessoryInformation)!.updateCharacteristic(Characteristic.Name, this.displayName); + this.displayName = `${this.displayName} ${createHash('sha512') + .update(info.username, 'utf8') + .digest('hex').slice(0, 4).toUpperCase()}` + this.getService(Service.AccessoryInformation)!.updateCharacteristic(Characteristic.Name, this.displayName) } // attempt to load existing AccessoryInfo from disk - this._accessoryInfo = AccessoryInfo.load(info.username); + this._accessoryInfo = AccessoryInfo.load(info.username) // if we don't have one, create a new one. if (!this._accessoryInfo) { - debug("[%s] Creating new AccessoryInfo for our HAP server", this.displayName); - this._accessoryInfo = AccessoryInfo.create(info.username); + debug('[%s] Creating new AccessoryInfo for our HAP server', this.displayName) + this._accessoryInfo = AccessoryInfo.create(info.username) } if (info.setupID) { - this._setupID = info.setupID; - } else if (this._accessoryInfo.setupID === undefined || this._accessoryInfo.setupID === "") { - this._setupID = Accessory._generateSetupID(); + this._setupID = info.setupID + } else if (this._accessoryInfo.setupID === undefined || this._accessoryInfo.setupID === '') { + this._setupID = Accessory._generateSetupID() } else { - this._setupID = this._accessoryInfo.setupID; + this._setupID = this._accessoryInfo.setupID } - this._accessoryInfo.setupID = this._setupID; + this._accessoryInfo.setupID = this._setupID // make sure we have up-to-date values in AccessoryInfo, then save it in case they changed (or if we just created it) - this._accessoryInfo.displayName = this.displayName; - this._accessoryInfo.model = this.getService(Service.AccessoryInformation)!.getCharacteristic(Characteristic.Model).value as string; - this._accessoryInfo.category = info.category || Categories.OTHER; - this._accessoryInfo.pincode = info.pincode; - this._accessoryInfo.save(); + this._accessoryInfo.displayName = this.displayName + this._accessoryInfo.model = this.getService(Service.AccessoryInformation)!.getCharacteristic(Characteristic.Model).value as string + this._accessoryInfo.category = info.category || Categories.OTHER + this._accessoryInfo.pincode = info.pincode + this._accessoryInfo.save() // create our IdentifierCache, so we can provide clients with stable aid/iid's - this._identifierCache = IdentifierCache.load(info.username); + this._identifierCache = IdentifierCache.load(info.username) // if we don't have one, create a new one. if (!this._identifierCache) { - debug("[%s] Creating new IdentifierCache", this.displayName); - this._identifierCache = new IdentifierCache(info.username); + debug('[%s] Creating new IdentifierCache', this.displayName) + this._identifierCache = new IdentifierCache(info.username) } // If it's bridge and there are no accessories already assigned to the bridge @@ -1162,29 +1166,31 @@ export class Accessory extends EventEmitter { // of accessories that might be added later. Useful when dynamically adding // accessories. if (this._isBridge && this.bridgedAccessories.length === 0) { - this.disableUnusedIDPurge(); - this.controllerStorage.purgeUnidentifiedAccessoryData = false; + this.disableUnusedIDPurge() + this.controllerStorage.purgeUnidentifiedAccessoryData = false } if (!this.initialized) { // controller storage is only loaded from disk the first time we publish! - this.controllerStorage.load(info.username); // initializing controller data + this.controllerStorage.load(info.username) // initializing controller data } // assign aid/iid - this._assignIDs(this._identifierCache); + this._assignIDs(this._identifierCache) // get our accessory information in HAP format and determine if our configuration (that is, our // Accessories/Services/Characteristics) has changed since the last time we were published. make // sure to omit actual values since these are not part of the "configuration". - const config = this.internalHAPRepresentation(false); // TODO ensure this stuff is ordered + const config = this.internalHAPRepresentation(false) // TODO ensure this stuff is ordered // TODO queue this check until about 5 seconds after startup, allowing some last changes after the publish call // without constantly incrementing the current config number - this._accessoryInfo.checkForCurrentConfigurationNumberIncrement(config, true); + this._accessoryInfo.checkForCurrentConfigurationNumberIncrement(config, true) - this.validateAccessory(true); + this.validateAccessory(true) // create our Advertiser which broadcasts our presence over mdns - const parsed = Accessory.parseBindOption(info); + const parsed = Accessory.parseBindOption(info) + + debug('[%s] Starting HAP server and publishing Accessory...', this.displayName) // Select the advertiser to use based on the user's choice and availability // 1. Check if info.advertiser is set by the user. @@ -1198,84 +1204,87 @@ export class Accessory extends EventEmitter { // > If not, use ciao. if (info.advertiser) { + debug('[%s] Advertiser set to %s', this.displayName, info.advertiser) if ( - (info.advertiser === MDNSAdvertiser.AVAHI && await AvahiAdvertiser.isAvailable()) || - (info.advertiser === MDNSAdvertiser.RESOLVED && await ResolvedAdvertiser.isAvailable()) || - (info.advertiser === MDNSAdvertiser.BONJOUR) || - (info.advertiser === MDNSAdvertiser.CIAO) + (info.advertiser === MDNSAdvertiser.AVAHI && await AvahiAdvertiser.isAvailable()) + || (info.advertiser === MDNSAdvertiser.RESOLVED && await ResolvedAdvertiser.isAvailable()) + || (info.advertiser === MDNSAdvertiser.BONJOUR) + || (info.advertiser === MDNSAdvertiser.CIAO) ) { // User chosen advertiser is available, use it + debug('[%s] Using advertiser %s', this.displayName, info.advertiser) } else { if (await AvahiAdvertiser.isAvailable()) { - info.advertiser = MDNSAdvertiser.AVAHI; + info.advertiser = MDNSAdvertiser.AVAHI } else { - info.advertiser = MDNSAdvertiser.CIAO; + info.advertiser = MDNSAdvertiser.CIAO } console.error( - `[${this.displayName}] The selected advertiser, "${info.advertiser}", isn't available on this platform. ` + - `Reverting to ${info.advertiser === MDNSAdvertiser.AVAHI ? "avahi" : "ciao"}.`, - ); + `[${this.displayName}] The selected advertiser, "${info.advertiser}", isn't available on this platform. ` + + `Reverting to ${info.advertiser === MDNSAdvertiser.AVAHI ? 'avahi' : 'ciao'}.`, + ) } } else { if (await AvahiAdvertiser.isAvailable()) { - info.advertiser = MDNSAdvertiser.AVAHI; + info.advertiser = MDNSAdvertiser.AVAHI } else { - info.advertiser = MDNSAdvertiser.CIAO; + info.advertiser = MDNSAdvertiser.CIAO } } switch (info.advertiser) { - case MDNSAdvertiser.CIAO: - this._advertiser = new CiaoAdvertiser(this._accessoryInfo, { - interface: parsed.advertiserAddress, - }, { - restrictedAddresses: parsed.serviceRestrictedAddress, - disabledIpv6: parsed.serviceDisableIpv6, - }); - break; - case MDNSAdvertiser.BONJOUR: - this._advertiser = new BonjourHAPAdvertiser(this._accessoryInfo, { - restrictedAddresses: parsed.serviceRestrictedAddress, - disabledIpv6: parsed.serviceDisableIpv6, - }); - break; - case MDNSAdvertiser.AVAHI: - this._advertiser = new AvahiAdvertiser(this._accessoryInfo); - break; - case MDNSAdvertiser.RESOLVED: - this._advertiser = new ResolvedAdvertiser(this._accessoryInfo); - break; - } - this._advertiser.on(AdvertiserEvent.UPDATED_NAME, name => { - this.displayName = name; + case MDNSAdvertiser.CIAO: + this._advertiser = new CiaoAdvertiser(this._accessoryInfo, { + interface: parsed.advertiserAddress, + }, { + restrictedAddresses: parsed.serviceRestrictedAddress, + disabledIpv6: parsed.serviceDisableIpv6, + }) + break + case MDNSAdvertiser.BONJOUR: + this._advertiser = new BonjourHAPAdvertiser(this._accessoryInfo, { + restrictedAddresses: parsed.serviceRestrictedAddress, + disabledIpv6: parsed.serviceDisableIpv6, + }) + break + case MDNSAdvertiser.AVAHI: + this._advertiser = new AvahiAdvertiser(this._accessoryInfo) + break + case MDNSAdvertiser.RESOLVED: + this._advertiser = new ResolvedAdvertiser(this._accessoryInfo) + break + } + debug('[%s] Advertiser created', this.displayName) + this._advertiser.on(AdvertiserEvent.UPDATED_NAME, (name) => { + this.displayName = name if (this._accessoryInfo) { - this._accessoryInfo.displayName = name; - this._accessoryInfo.save(); + this._accessoryInfo.displayName = name + this._accessoryInfo.save() } // bonjour service name MUST match the name in the accessory information service this.getService(Service.AccessoryInformation)! - .updateCharacteristic(Characteristic.Name, name); - }); + .updateCharacteristic(Characteristic.Name, name) + }) // create our HAP server which handles all communication between iOS devices and us - this._server = new HAPServer(this._accessoryInfo); - this._server.allowInsecureRequest = !!allowInsecureRequest; - this._server.on(HAPServerEventTypes.LISTENING, this.onListening.bind(this)); - this._server.on(HAPServerEventTypes.IDENTIFY, this.identificationRequest.bind(this, false)); - this._server.on(HAPServerEventTypes.PAIR, this.handleInitialPairSetupFinished.bind(this)); - this._server.on(HAPServerEventTypes.ADD_PAIRING, this.handleAddPairing.bind(this)); - this._server.on(HAPServerEventTypes.REMOVE_PAIRING, this.handleRemovePairing.bind(this)); - this._server.on(HAPServerEventTypes.LIST_PAIRINGS, this.handleListPairings.bind(this)); - this._server.on(HAPServerEventTypes.ACCESSORIES, this.handleAccessories.bind(this)); - this._server.on(HAPServerEventTypes.GET_CHARACTERISTICS, this.handleGetCharacteristics.bind(this)); - this._server.on(HAPServerEventTypes.SET_CHARACTERISTICS, this.handleSetCharacteristics.bind(this)); - this._server.on(HAPServerEventTypes.CONNECTION_CLOSED, this.handleHAPConnectionClosed.bind(this)); - this._server.on(HAPServerEventTypes.REQUEST_RESOURCE, this.handleResource.bind(this)); - - this._server.listen(info.port, parsed.serverAddress); - - this.initialized = true; + this._server = new HAPServer(this._accessoryInfo) + this._server.allowInsecureRequest = !!allowInsecureRequest + this._server.on(HAPServerEventTypes.LISTENING, this.onListening.bind(this)) + this._server.on(HAPServerEventTypes.IDENTIFY, this.identificationRequest.bind(this, false)) + this._server.on(HAPServerEventTypes.PAIR, this.handleInitialPairSetupFinished.bind(this)) + this._server.on(HAPServerEventTypes.ADD_PAIRING, this.handleAddPairing.bind(this)) + this._server.on(HAPServerEventTypes.REMOVE_PAIRING, this.handleRemovePairing.bind(this)) + this._server.on(HAPServerEventTypes.LIST_PAIRINGS, this.handleListPairings.bind(this)) + this._server.on(HAPServerEventTypes.ACCESSORIES, this.handleAccessories.bind(this)) + this._server.on(HAPServerEventTypes.GET_CHARACTERISTICS, this.handleGetCharacteristics.bind(this)) + this._server.on(HAPServerEventTypes.SET_CHARACTERISTICS, this.handleSetCharacteristics.bind(this)) + this._server.on(HAPServerEventTypes.CONNECTION_CLOSED, this.handleHAPConnectionClosed.bind(this)) + this._server.on(HAPServerEventTypes.REQUEST_RESOURCE, this.handleResource.bind(this)) + + this._server.listen(info.port, parsed.serverAddress) + + this.initialized = true } /** @@ -1284,257 +1293,256 @@ export class Accessory extends EventEmitter { * Trying to invoke publish() on the object will result undefined behavior */ public destroy(): Promise { - const promise = this.unpublish(); + const promise = this.unpublish() if (this._accessoryInfo) { - Accessory.cleanupAccessoryData(this._accessoryInfo.username); + Accessory.cleanupAccessoryData(this._accessoryInfo.username) - this._accessoryInfo = undefined; - this._identifierCache = undefined; - this.controllerStorage = new ControllerStorage(this); + this._accessoryInfo = undefined + this._identifierCache = undefined + this.controllerStorage = new ControllerStorage(this) } - this.removeAllListeners(); + this.removeAllListeners() - return promise; + return promise } public async unpublish(): Promise { if (this._server) { - this._server.destroy(); - this._server = undefined; + this._server.destroy() + this._server = undefined } if (this._advertiser) { - // noinspection JSIgnoredPromiseFromCall - await this._advertiser.destroy(); - this._advertiser = undefined; + this._advertiser.destroy() + this._advertiser = undefined } } private enqueueConfigurationUpdate(): void { if (this.configurationChangeDebounceTimeout) { - return; // already enqueued + return // already enqueued } this.configurationChangeDebounceTimeout = setTimeout(() => { - this.configurationChangeDebounceTimeout = undefined; + this.configurationChangeDebounceTimeout = undefined if (this._advertiser && this._advertiser) { // get our accessory information in HAP format and determine if our configuration (that is, our // Accessories/Services/Characteristics) has changed since the last time we were published. make // sure to omit actual values since these are not part of the "configuration". - const config = this.internalHAPRepresentation(); // TODO ensure this stuff is ordered + const config = this.internalHAPRepresentation() // TODO ensure this stuff is ordered if (this._accessoryInfo?.checkForCurrentConfigurationNumberIncrement(config)) { - this._advertiser.updateAdvertisement(); + this._advertiser.updateAdvertisement() } } - }, 1000); - this.configurationChangeDebounceTimeout.unref(); + }, 1000) + this.configurationChangeDebounceTimeout.unref() // 1s is fine, HomeKit is built that with configuration updates no iid or aid conflicts occur. // Thus, the only thing happening when the txt update arrives late is already removed accessories/services // not responding or new accessories/services not yet shown } private onListening(port: number, hostname: string): void { - assert(this._advertiser, "Advertiser wasn't created at onListening!"); + assert(this._advertiser, 'Advertiser wasn\'t created at onListening!') // the HAP server is listening, so we can now start advertising our presence. - this._advertiser!.initPort(port); + this._advertiser!.initPort(port) this._advertiser!.startAdvertising() .then(() => this.emit(AccessoryEventTypes.ADVERTISED)) - .catch(reason => { - console.error("Could not create mDNS advertisement. The HAP-Server won't be discoverable: " + reason); + .catch((reason) => { + console.error(`Could not create mDNS advertisement. The HAP-Server won't be discoverable: ${reason}`) if (reason.stack) { - debug("Detailed error: " + reason.stack); + debug(`Detailed error: ${reason.stack}`) } - }); + }) - this.emit(AccessoryEventTypes.LISTENING, port, hostname); + this.emit(AccessoryEventTypes.LISTENING, port, hostname) } private handleInitialPairSetupFinished(username: string, publicKey: Buffer, callback: PairCallback): void { - debug("[%s] Paired with client %s", this.displayName, username); + debug('[%s] Paired with client %s', this.displayName, username) - this._accessoryInfo?.addPairedClient(username, publicKey, PermissionTypes.ADMIN); - this._accessoryInfo?.save(); + this._accessoryInfo?.addPairedClient(username, publicKey, PermissionTypes.ADMIN) + this._accessoryInfo?.save() // update our advertisement, so it can pick up on the paired status of AccessoryInfo - this._advertiser?.updateAdvertisement(); + this._advertiser?.updateAdvertisement() - callback(); + callback() - this.emit(AccessoryEventTypes.PAIRED); + this.emit(AccessoryEventTypes.PAIRED) } private handleAddPairing(connection: HAPConnection, username: string, publicKey: Buffer, permission: PermissionTypes, callback: AddPairingCallback): void { if (!this._accessoryInfo) { - callback(TLVErrorCode.UNAVAILABLE); - return; + callback(TLVErrorCode.UNAVAILABLE) + return } if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) { - callback(TLVErrorCode.AUTHENTICATION); - return; + callback(TLVErrorCode.AUTHENTICATION) + return } - const existingKey = this._accessoryInfo.getClientPublicKey(username); + const existingKey = this._accessoryInfo.getClientPublicKey(username) if (existingKey) { if (existingKey.toString() !== publicKey.toString()) { - callback(TLVErrorCode.UNKNOWN); - return; + callback(TLVErrorCode.UNKNOWN) + return } - this._accessoryInfo.updatePermission(username, permission); + this._accessoryInfo.updatePermission(username, permission) } else { - this._accessoryInfo.addPairedClient(username, publicKey, permission); + this._accessoryInfo.addPairedClient(username, publicKey, permission) } - this._accessoryInfo.save(); + this._accessoryInfo.save() // there should be no need to update advertisement - callback(0); + callback(0) } private handleRemovePairing(connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback): void { if (!this._accessoryInfo) { - callback(TLVErrorCode.UNAVAILABLE); - return; + callback(TLVErrorCode.UNAVAILABLE) + return } if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) { - callback(TLVErrorCode.AUTHENTICATION); - return; + callback(TLVErrorCode.AUTHENTICATION) + return } - this._accessoryInfo.removePairedClient(connection, username); - this._accessoryInfo.save(); + this._accessoryInfo.removePairedClient(connection, username) + this._accessoryInfo.save() - callback(0); // first of all ensure the pairing is removed before we advertise availability again + callback(0) // first of all ensure the pairing is removed before we advertise availability again if (!this._accessoryInfo.paired()) { - this._advertiser?.updateAdvertisement(); - this.emit(AccessoryEventTypes.UNPAIRED); + this._advertiser?.updateAdvertisement() + this.emit(AccessoryEventTypes.UNPAIRED) - this.handleAccessoryUnpairedForControllers(); + this.handleAccessoryUnpairedForControllers() for (const accessory of this.bridgedAccessories) { - accessory.handleAccessoryUnpairedForControllers(); + accessory.handleAccessoryUnpairedForControllers() } } } private handleListPairings(connection: HAPConnection, callback: ListPairingsCallback): void { if (!this._accessoryInfo) { - callback(TLVErrorCode.UNAVAILABLE); - return; + callback(TLVErrorCode.UNAVAILABLE) + return } if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) { - callback(TLVErrorCode.AUTHENTICATION); - return; + callback(TLVErrorCode.AUTHENTICATION) + return } - callback(0, this._accessoryInfo.listPairings()); + callback(0, this._accessoryInfo.listPairings()) } private handleAccessories(connection: HAPConnection, callback: AccessoriesCallback): void { - this._assignIDs(this._identifierCache!); // make sure our aid/iid's are all assigned + this._assignIDs(this._identifierCache!) // make sure our aid/iid's are all assigned - const now = Date.now(); - const contactGetHandlers = now - this.lastAccessoriesRequest > 5_000; // we query the latest value if last /accessories was more than 5s ago - this.lastAccessoriesRequest = now; + const now = Date.now() + const contactGetHandlers = now - this.lastAccessoriesRequest > 5_000 // we query the latest value if last /accessories was more than 5s ago + this.lastAccessoriesRequest = now - this.toHAP(connection, contactGetHandlers).then(value => { + this.toHAP(connection, contactGetHandlers).then((value) => { callback(undefined, { accessories: value, - }); - }, reason => { - console.error("[" + this.displayName + "] /accessories request error with: " + reason.stack); - callback({ httpCode: HAPHTTPCode.INTERNAL_SERVER_ERROR, status: HAPStatus.SERVICE_COMMUNICATION_FAILURE }); - }); + }) + }, (reason) => { + console.error(`[${this.displayName}] /accessories request error with: ${reason.stack}`) + callback({ httpCode: HAPHTTPCode.INTERNAL_SERVER_ERROR, status: HAPStatus.SERVICE_COMMUNICATION_FAILURE }) + }) } private handleGetCharacteristics(connection: HAPConnection, request: CharacteristicsReadRequest, callback: ReadCharacteristicsCallback): void { - const characteristics: CharacteristicReadData[] = []; - const response: CharacteristicsReadResponse = { characteristics: characteristics }; + const characteristics: CharacteristicReadData[] = [] + const response: CharacteristicsReadResponse = { characteristics } - const missingCharacteristics: Set = new Set(request.ids.map(id => id.aid + "." + id.iid)); + const missingCharacteristics: Set = new Set(request.ids.map(id => `${id.aid}.${id.iid}`)) if (missingCharacteristics.size !== request.ids.length) { // if those sizes differ, we have duplicates and can't properly handle that - callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST }); - return; + callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST }) + return } let timeout: NodeJS.Timeout | undefined = setTimeout(() => { for (const id of missingCharacteristics) { - const split = id.split("."); - const aid = parseInt(split[0], 10); - const iid = parseInt(split[1], 10); - - const accessory = this.getAccessoryByAID(aid)!; - const characteristic = accessory.getCharacteristicByIID(iid)!; - this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_READ, "The read handler for the characteristic '" + - characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!"); + const split = id.split('.') + const aid = Number.parseInt(split[0], 10) + const iid = Number.parseInt(split[1], 10) + + const accessory = this.getAccessoryByAID(aid)! + const characteristic = accessory.getCharacteristicByIID(iid)! + this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_READ, `The read handler for the characteristic '${ + characteristic.displayName}' on the accessory '${accessory.displayName}' was slow to respond!`) } // after a total of 10s we do no longer wait for a request to appear and just return status code timeout timeout = setTimeout(() => { - timeout = undefined; + timeout = undefined for (const id of missingCharacteristics) { - const split = id.split("."); - const aid = parseInt(split[0], 10); - const iid = parseInt(split[1], 10); + const split = id.split('.') + const aid = Number.parseInt(split[0], 10) + const iid = Number.parseInt(split[1], 10) - const accessory = this.getAccessoryByAID(aid)!; - const characteristic = accessory.getCharacteristicByIID(iid)!; - this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_READ, "The read handler for the characteristic '" + - characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " + - "Please check that you properly call the callback!"); + const accessory = this.getAccessoryByAID(aid)! + const characteristic = accessory.getCharacteristicByIID(iid)! + this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_READ, `The read handler for the characteristic '${ + characteristic.displayName}' on the accessory '${accessory.displayName}' didn't respond at all!. ` + + `Please check that you properly call the callback!`) characteristics.push({ - aid: aid, - iid: iid, + aid, + iid, status: HAPStatus.OPERATION_TIMED_OUT, - }); + }) } - missingCharacteristics.clear(); + missingCharacteristics.clear() - callback(undefined, response); - }, Accessory.TIMEOUT_AFTER_WARNING); - timeout.unref(); - }, Accessory.TIMEOUT_WARNING); - timeout.unref(); + callback(undefined, response) + }, Accessory.TIMEOUT_AFTER_WARNING) + timeout.unref() + }, Accessory.TIMEOUT_WARNING) + timeout.unref() for (const id of request.ids) { - const name = id.aid + "." + id.iid; - this.handleCharacteristicRead(connection, id, request).then(value => { + const name = `${id.aid}.${id.iid}` + this.handleCharacteristicRead(connection, id, request).then((value) => { return { aid: id.aid, iid: id.iid, ...value, - }; - }, reason => { // this error block is only called if hap-nodejs itself messed up - console.error(`[${this.displayName}] Read request for characteristic ${name} encountered an error: ${reason.stack}`); + } + }, (reason) => { // this error block is only called if hap-nodejs itself messed up + console.error(`[${this.displayName}] Read request for characteristic ${name} encountered an error: ${reason.stack}`) return { aid: id.aid, iid: id.iid, status: HAPStatus.SERVICE_COMMUNICATION_FAILURE, - }; - }).then(value => { + } + }).then((value) => { if (!timeout) { - return; // if timeout is undefined, response was already sent out + return // if timeout is undefined, response was already sent out } - missingCharacteristics.delete(name); - characteristics.push(value); + missingCharacteristics.delete(name) + characteristics.push(value) if (missingCharacteristics.size === 0) { if (timeout) { - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout) + timeout = undefined } - callback(undefined, response); + callback(undefined, response) } - }); + }) } } @@ -1543,168 +1551,166 @@ export class Accessory extends EventEmitter { id: CharacteristicId, request: CharacteristicsReadRequest, ): Promise { - const characteristic = this.findCharacteristic(id.aid, id.iid); + const characteristic = this.findCharacteristic(id.aid, id.iid) if (!characteristic) { - debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, id.aid, id.iid); - return { status: HAPStatus.INVALID_VALUE_IN_REQUEST }; + debug('[%s] Could not find a Characteristic with aid of %s and iid of %s', this.displayName, id.aid, id.iid) + return { status: HAPStatus.INVALID_VALUE_IN_REQUEST } } if (!characteristic.props.perms.includes(Perms.PAIRED_READ)) { // check if read is allowed for this characteristic - debug("[%s] Tried reading from characteristic which does not allow reading (aid of %s and iid of %s)", this.displayName, id.aid, id.iid); - return { status: HAPStatus.WRITE_ONLY_CHARACTERISTIC }; + debug('[%s] Tried reading from characteristic which does not allow reading (aid of %s and iid of %s)', this.displayName, id.aid, id.iid) + return { status: HAPStatus.WRITE_ONLY_CHARACTERISTIC } } if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.READ)) { - const verifiable = this._accessoryInfo && connection.username; + const verifiable = this._accessoryInfo && connection.username if (!verifiable) { - debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for reading (aid of %s and iid of %s)", - this.displayName, id.aid, id.iid); + debug('[%s] Could not verify admin permissions for Characteristic which requires admin permissions for reading (aid of %s and iid of %s)', this.displayName, id.aid, id.iid) } if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) { - return { status: HAPStatus.INSUFFICIENT_PRIVILEGES }; + return { status: HAPStatus.INSUFFICIENT_PRIVILEGES } } } - return characteristic.handleGetRequest(connection).then(value => { - value = formatOutgoingCharacteristicValue(value, characteristic.props); - debug("[%s] Got Characteristic \"%s\" value: \"%s\"", this.displayName, characteristic!.displayName, value); + return characteristic.handleGetRequest(connection).then((value) => { + value = formatOutgoingCharacteristicValue(value, characteristic.props) + debug('[%s] Got Characteristic "%s" value: "%s"', this.displayName, characteristic!.displayName, value) const data: PartialCharacteristicReadData = { - value: value == null ? null: value, - }; + value: value == null ? null : value, + } if (request.includeMeta) { - data.format = characteristic.props.format; - data.unit = characteristic.props.unit; - data.minValue = characteristic.props.minValue; - data.maxValue = characteristic.props.maxValue; - data.minStep = characteristic.props.minStep; - data.maxLen = characteristic.props.maxLen || characteristic.props.maxDataLen; + data.format = characteristic.props.format + data.unit = characteristic.props.unit + data.minValue = characteristic.props.minValue + data.maxValue = characteristic.props.maxValue + data.minStep = characteristic.props.minStep + data.maxLen = characteristic.props.maxLen || characteristic.props.maxDataLen } if (request.includePerms) { - data.perms = characteristic.props.perms; + data.perms = characteristic.props.perms } if (request.includeType) { - data.type = toShortForm(characteristic.UUID); + data.type = toShortForm(characteristic.UUID) } if (request.includeEvent) { - data.ev = connection.hasEventNotifications(id.aid, id.iid); + data.ev = connection.hasEventNotifications(id.aid, id.iid) } - return data; + return data }, (reason: HAPStatus) => { - // @ts-expect-error: preserveConstEnums compiler option - debug("[%s] Error getting value for characteristic \"%s\": %s", this.displayName, characteristic.displayName, HAPStatus[reason]); - return { status: reason }; - }); + debug('[%s] Error getting value for characteristic "%s": %s', this.displayName, characteristic.displayName, HAPStatus[reason]) + return { status: reason } + }) } private handleSetCharacteristics(connection: HAPConnection, writeRequest: CharacteristicsWriteRequest, callback: WriteCharacteristicsCallback): void { - debug("[%s] Processing characteristic set: %s", this.displayName, JSON.stringify(writeRequest)); + debug('[%s] Processing characteristic set: %s', this.displayName, JSON.stringify(writeRequest)) - let writeState: WriteRequestState = WriteRequestState.REGULAR_REQUEST; + let writeState: WriteRequestState = WriteRequestState.REGULAR_REQUEST if (writeRequest.pid !== undefined) { // check for timed writes if (connection.timedWritePid === writeRequest.pid) { - writeState = WriteRequestState.TIMED_WRITE_AUTHENTICATED; - clearTimeout(connection.timedWriteTimeout!); - connection.timedWritePid = undefined; - connection.timedWriteTimeout = undefined; + writeState = WriteRequestState.TIMED_WRITE_AUTHENTICATED + clearTimeout(connection.timedWriteTimeout!) + connection.timedWritePid = undefined + connection.timedWriteTimeout = undefined - debug("[%s] Timed write request got acknowledged for pid %d", this.displayName, writeRequest.pid); + debug('[%s] Timed write request got acknowledged for pid %d', this.displayName, writeRequest.pid) } else { - writeState = WriteRequestState.TIMED_WRITE_REJECTED; - debug("[%s] TTL for timed write request has probably expired for pid %d", this.displayName, writeRequest.pid); + writeState = WriteRequestState.TIMED_WRITE_REJECTED + debug('[%s] TTL for timed write request has probably expired for pid %d', this.displayName, writeRequest.pid) } } - const characteristics: CharacteristicWriteData[] = []; - const response: CharacteristicsWriteResponse = { characteristics: characteristics }; + const characteristics: CharacteristicWriteData[] = [] + const response: CharacteristicsWriteResponse = { characteristics } const missingCharacteristics: Set = new Set( writeRequest.characteristics - .map(characteristic => characteristic.aid + "." + characteristic.iid), - ); + .map(characteristic => `${characteristic.aid}.${characteristic.iid}`), + ) if (missingCharacteristics.size !== writeRequest.characteristics.length) { // if those sizes differ, we have duplicates and can't properly handle that - callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST }); - return; + callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST }) + return } let timeout: NodeJS.Timeout | undefined = setTimeout(() => { for (const id of missingCharacteristics) { - const split = id.split("."); - const aid = parseInt(split[0], 10); - const iid = parseInt(split[1], 10); - - const accessory = this.getAccessoryByAID(aid)!; - const characteristic = accessory.getCharacteristicByIID(iid)!; - this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_WRITE, "The write handler for the characteristic '" + - characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!"); + const split = id.split('.') + const aid = Number.parseInt(split[0], 10) + const iid = Number.parseInt(split[1], 10) + + const accessory = this.getAccessoryByAID(aid)! + const characteristic = accessory.getCharacteristicByIID(iid)! + this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_WRITE, `The write handler for the characteristic '${ + characteristic.displayName}' on the accessory '${accessory.displayName}' was slow to respond!`) } // after a total of 10s we do no longer wait for a request to appear and just return status code timeout timeout = setTimeout(() => { - timeout = undefined; + timeout = undefined for (const id of missingCharacteristics) { - const split = id.split("."); - const aid = parseInt(split[0], 10); - const iid = parseInt(split[1], 10); + const split = id.split('.') + const aid = Number.parseInt(split[0], 10) + const iid = Number.parseInt(split[1], 10) - const accessory = this.getAccessoryByAID(aid)!; - const characteristic = accessory.getCharacteristicByIID(iid)!; - this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_WRITE, "The write handler for the characteristic '" + - characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " + - "Please check that you properly call the callback!"); + const accessory = this.getAccessoryByAID(aid)! + const characteristic = accessory.getCharacteristicByIID(iid)! + this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_WRITE, `The write handler for the characteristic '${ + characteristic.displayName}' on the accessory '${accessory.displayName}' didn't respond at all!. ` + + `Please check that you properly call the callback!`) characteristics.push({ - aid: aid, - iid: iid, + aid, + iid, status: HAPStatus.OPERATION_TIMED_OUT, - }); + }) } - missingCharacteristics.clear(); + missingCharacteristics.clear() - callback(undefined, response); - }, Accessory.TIMEOUT_AFTER_WARNING); - timeout.unref(); - }, Accessory.TIMEOUT_WARNING); - timeout.unref(); + callback(undefined, response) + }, Accessory.TIMEOUT_AFTER_WARNING) + timeout.unref() + }, Accessory.TIMEOUT_WARNING) + timeout.unref() for (const data of writeRequest.characteristics) { - const name = data.aid + "." + data.iid; - this.handleCharacteristicWrite(connection, data, writeState).then(value => { + const name = `${data.aid}.${data.iid}` + this.handleCharacteristicWrite(connection, data, writeState).then((value) => { return { aid: data.aid, iid: data.iid, ...value, - }; - }, reason => { // this error block is only called if hap-nodejs itself messed up - console.error(`[${this.displayName}] Write request for characteristic ${name} encountered an error: ${reason.stack}`); + } + }, (reason) => { // this error block is only called if hap-nodejs itself messed up + console.error(`[${this.displayName}] Write request for characteristic ${name} encountered an error: ${reason.stack}`) return { aid: data.aid, iid: data.iid, status: HAPStatus.SERVICE_COMMUNICATION_FAILURE, - }; - }).then(value => { + } + }).then((value) => { if (!timeout) { - return; // if timeout is undefined, response was already sent out + return // if timeout is undefined, response was already sent out } - missingCharacteristics.delete(name); - characteristics.push(value); + missingCharacteristics.delete(name) + characteristics.push(value) if (missingCharacteristics.size === 0) { // if everything returned send the response if (timeout) { - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout) + timeout = undefined } - callback(undefined, response); + callback(undefined, response) } - }); + }) } } @@ -1713,49 +1719,47 @@ export class Accessory extends EventEmitter { data: CharacteristicWrite, writeState: WriteRequestState, ): Promise { - const characteristic = this.findCharacteristic(data.aid, data.iid); + const characteristic = this.findCharacteristic(data.aid, data.iid) if (!characteristic) { - debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, data.aid, data.iid); - return { status: HAPStatus.INVALID_VALUE_IN_REQUEST }; + debug('[%s] Could not find a Characteristic with aid of %s and iid of %s', this.displayName, data.aid, data.iid) + return { status: HAPStatus.INVALID_VALUE_IN_REQUEST } } if (writeState === WriteRequestState.TIMED_WRITE_REJECTED) { - return { status: HAPStatus.INVALID_VALUE_IN_REQUEST }; + return { status: HAPStatus.INVALID_VALUE_IN_REQUEST } } if (data.ev == null && data.value == null) { - return { status: HAPStatus.INVALID_VALUE_IN_REQUEST }; + return { status: HAPStatus.INVALID_VALUE_IN_REQUEST } } if (data.ev != null) { // register/unregister event notifications if (!characteristic.props.perms.includes(Perms.NOTIFY)) { // check if notify is allowed for this characteristic - debug("[%s] Tried %s notifications for Characteristic which does not allow notify (aid of %s and iid of %s)", - this.displayName, data.ev? "enabling": "disabling", data.aid, data.iid); - return { status: HAPStatus.NOTIFICATION_NOT_SUPPORTED }; + debug('[%s] Tried %s notifications for Characteristic which does not allow notify (aid of %s and iid of %s)', this.displayName, data.ev ? 'enabling' : 'disabling', data.aid, data.iid) + return { status: HAPStatus.NOTIFICATION_NOT_SUPPORTED } } if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.NOTIFY)) { - const verifiable = connection.username && this._accessoryInfo; + const verifiable = connection.username && this._accessoryInfo if (!verifiable) { - debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for notify (aid of %s and iid of %s)", - this.displayName, data.aid, data.iid); + debug('[%s] Could not verify admin permissions for Characteristic which requires admin permissions for notify (aid of %s and iid of %s)', this.displayName, data.aid, data.iid) } if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) { - return { status: HAPStatus.INSUFFICIENT_PRIVILEGES }; + return { status: HAPStatus.INSUFFICIENT_PRIVILEGES } } } - const notificationsEnabled = connection.hasEventNotifications(data.aid, data.iid); + const notificationsEnabled = connection.hasEventNotifications(data.aid, data.iid) if (data.ev && !notificationsEnabled) { - connection.enableEventNotifications(data.aid, data.iid); - characteristic.subscribe(); - debug("[%s] Registered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName); + connection.enableEventNotifications(data.aid, data.iid) + characteristic.subscribe() + debug('[%s] Registered Characteristic "%s" on "%s" for events', connection.remoteAddress, characteristic.displayName, this.displayName) } else if (!data.ev && notificationsEnabled) { - characteristic.unsubscribe(); - connection.disableEventNotifications(data.aid, data.iid); - debug("[%s] Unregistered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName); + characteristic.unsubscribe() + connection.disableEventNotifications(data.aid, data.iid) + debug('[%s] Unregistered Characteristic "%s" on "%s" for events', connection.remoteAddress, characteristic.displayName, this.displayName) } // response is returned below in the else block @@ -1763,19 +1767,18 @@ export class Accessory extends EventEmitter { if (data.value != null) { if (!characteristic.props.perms.includes(Perms.PAIRED_WRITE)) { // check if write is allowed for this characteristic - debug("[%s] Tried writing to Characteristic which does not allow writing (aid of %s and iid of %s)", this.displayName, data.aid, data.iid); - return { status: HAPStatus.READ_ONLY_CHARACTERISTIC }; + debug('[%s] Tried writing to Characteristic which does not allow writing (aid of %s and iid of %s)', this.displayName, data.aid, data.iid) + return { status: HAPStatus.READ_ONLY_CHARACTERISTIC } } if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.WRITE)) { - const verifiable = connection.username && this._accessoryInfo; + const verifiable = connection.username && this._accessoryInfo if (!verifiable) { - debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for write (aid of %s and iid of %s)", - this.displayName, data.aid, data.iid); + debug('[%s] Could not verify admin permissions for Characteristic which requires admin permissions for write (aid of %s and iid of %s)', this.displayName, data.aid, data.iid) } if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) { - return { status: HAPStatus.INSUFFICIENT_PRIVILEGES }; + return { status: HAPStatus.INSUFFICIENT_PRIVILEGES } } } @@ -1783,131 +1786,128 @@ export class Accessory extends EventEmitter { // if the characteristic "supports additional authorization" but doesn't define a handler for the check // we conclude that the characteristic doesn't want to check the authData (currently) and just allows access for everybody - let allowWrite; + let allowWrite try { - allowWrite = characteristic.additionalAuthorizationHandler(data.authData); + allowWrite = characteristic.additionalAuthorizationHandler(data.authData) } catch (error) { - console.warn("[" + this.displayName + "] Additional authorization handler has thrown an error when checking authData: " + error.stack); - allowWrite = false; + console.warn(`[${this.displayName}] Additional authorization handler has thrown an error when checking authData: ${error.stack}`) + allowWrite = false } if (!allowWrite) { - return { status: HAPStatus.INSUFFICIENT_AUTHORIZATION }; + return { status: HAPStatus.INSUFFICIENT_AUTHORIZATION } } } if (characteristic.props.perms.includes(Perms.TIMED_WRITE) && writeState !== WriteRequestState.TIMED_WRITE_AUTHENTICATED) { - debug("[%s] Tried writing to a timed write only Characteristic without properly preparing (iid of %s and aid of %s)", - this.displayName, data.aid, data.iid); - return { status: HAPStatus.INVALID_VALUE_IN_REQUEST }; + debug('[%s] Tried writing to a timed write only Characteristic without properly preparing (iid of %s and aid of %s)', this.displayName, data.aid, data.iid) + return { status: HAPStatus.INVALID_VALUE_IN_REQUEST } } - return characteristic.handleSetRequest(data.value, connection).then(value => { - debug("[%s] Setting Characteristic \"%s\" to value %s", this.displayName, characteristic.displayName, data.value); + return characteristic.handleSetRequest(data.value, connection).then((value) => { + debug('[%s] Setting Characteristic "%s" to value %s', this.displayName, characteristic.displayName, data.value) return { // if write response is requests and value is provided, return that - value: data.r && value? formatOutgoingCharacteristicValue(value, characteristic.props): undefined, + value: data.r && value ? formatOutgoingCharacteristicValue(value, characteristic.props) : undefined, status: HAPStatus.SUCCESS, - }; + } }, (status: HAPStatus) => { - // @ts-expect-error: forceConsistentCasingInFileNames compiler option - debug("[%s] Error setting Characteristic \"%s\" to value %s: ", this.displayName, characteristic.displayName, data.value, HAPStatus[status]); + debug('[%s] Error setting Characteristic "%s" to value %s: ', this.displayName, characteristic.displayName, data.value, HAPStatus[status]) - return { status: status }; - }); + return { status } + }) } - return { status: HAPStatus.SUCCESS }; + return { status: HAPStatus.SUCCESS } } private handleResource(data: ResourceRequest, callback: ResourceRequestCallback): void { - if (data["resource-type"] === ResourceRequestType.IMAGE) { - const aid = data.aid; // aid is optionally supplied by HomeKit (for example when camera is bridged, multiple cams, etc) + if (data['resource-type'] === ResourceRequestType.IMAGE) { + const aid = data.aid // aid is optionally supplied by HomeKit (for example when camera is bridged, multiple cams, etc.) - let accessory: Accessory | undefined = undefined; - let controller: CameraController | undefined = undefined; + let accessory: Accessory | undefined + let controller: CameraController | undefined if (aid) { - accessory = this.getAccessoryByAID(aid); + accessory = this.getAccessoryByAID(aid) if (accessory && accessory.activeCameraController) { - controller = accessory.activeCameraController; + controller = accessory.activeCameraController } } else if (this.activeCameraController) { // aid was not supplied, check if this accessory is a camera - // eslint-disable-next-line @typescript-eslint/no-this-alias - accessory = this; - controller = this.activeCameraController; + accessory = this // eslint-disable-line ts/no-this-alias + controller = this.activeCameraController } if (!controller) { - debug("[%s] received snapshot request though no camera controller was associated!"); - callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST }); - return; + debug('[%s] received snapshot request though no camera controller was associated!') + callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST }) + return } - controller.handleSnapshotRequest(data["image-height"], data["image-width"], accessory?.displayName, data.reason) - .then(buffer => { - callback(undefined, buffer); + controller.handleSnapshotRequest(data['image-height'], data['image-width'], accessory?.displayName, data.reason) + .then((buffer) => { + callback(undefined, buffer) }, (status: HAPStatus) => { - callback({ httpCode: HAPHTTPCode.MULTI_STATUS, status: status }); - }); + callback({ httpCode: HAPHTTPCode.MULTI_STATUS, status }) + }) - return; + return } - debug("[%s] received request for unsupported image type: " + data["resource-type"], this._accessoryInfo?.username); - callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST }); + debug(`[%s] received request for unsupported image type: ${data['resource-type']}`, this._accessoryInfo?.username) + callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST }) } private handleHAPConnectionClosed(connection: HAPConnection): void { for (const event of connection.getRegisteredEvents()) { - const ids = event.split("."); - const aid = parseInt(ids[0], 10); - const iid = parseInt(ids[1], 10); + const ids = event.split('.') + const aid = Number.parseInt(ids[0], 10) + const iid = Number.parseInt(ids[1], 10) - const characteristic = this.findCharacteristic(aid, iid); + const characteristic = this.findCharacteristic(aid, iid) if (characteristic) { - characteristic.unsubscribe(); + characteristic.unsubscribe() } } - connection.clearRegisteredEvents(); + connection.clearRegisteredEvents() } private handleServiceConfigurationChangeEvent(service: Service): void { if (!service.isPrimaryService && service === this.primaryService) { // service changed form primary to non-primary service - this.primaryService = undefined; + this.primaryService = undefined } else if (service.isPrimaryService && service !== this.primaryService) { // service changed from non-primary to primary service if (this.primaryService !== undefined) { - this.primaryService.isPrimaryService = false; + this.primaryService.isPrimaryService = false } - this.primaryService = service; + this.primaryService = service } if (this.bridged) { - this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service }); + this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service }) } else { - this.enqueueConfigurationUpdate(); + this.enqueueConfigurationUpdate() } } private handleCharacteristicChangeEvent(accessory: Accessory, service: Service, change: ServiceCharacteristicChange): void { if (this.bridged) { // forward this to our main accessory - this.emit(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, { ...change, service: service }); + this.emit(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, { ...change, service }) } else { if (!this._server) { - return; // we're not running a HAPServer, so there's no one to notify about this event + return // we're not running a HAPServer, so there's no one to notify about this event } if (accessory.aid == null || change.characteristic.iid == null) { - debug("[%s] Muting event notification for %s as ids aren't yet assigned!", accessory.displayName, change.characteristic.displayName); - return; + debug('[%s] Muting event notification for %s as ids aren\'t yet assigned!', accessory.displayName, change.characteristic.displayName) + return } - if (change.context != null && typeof change.context === "object" && (change.context as CharacteristicOperationContext).omitEventUpdate) { - debug("[%s] Omitting event updates for %s as specified in the context object!", accessory.displayName, change.characteristic.displayName); - return; + if (change.context != null && typeof change.context === 'object' && (change.context as CharacteristicOperationContext).omitEventUpdate) { + debug('[%s] Omitting event updates for %s as specified in the context object!', accessory.displayName, change.characteristic.displayName) + return } if (!(change.reason === ChangeReason.EVENT || change.oldValue !== change.newValue @@ -1917,57 +1917,57 @@ export class Accessory extends EventEmitter { // we only emit a change event if the reason was a call to sendEventNotification, if the value changed // as of a write request or a read request or if the change happened on dedicated event characteristics // otherwise we ignore this change event (with the return below) - return; + return } - const uuid = change.characteristic.UUID; + const uuid = change.characteristic.UUID const immediateDelivery = uuid === Characteristic.ButtonEvent.UUID || uuid === Characteristic.ProgrammableSwitchEvent.UUID - || uuid === Characteristic.MotionDetected.UUID || uuid === Characteristic.ContactSensorState.UUID; + || uuid === Characteristic.MotionDetected.UUID || uuid === Characteristic.ContactSensorState.UUID - const value = formatOutgoingCharacteristicValue(change.newValue, change.characteristic.props); - this._server.sendEventNotifications(accessory.aid, change.characteristic.iid, value, change.originator, immediateDelivery); + const value = formatOutgoingCharacteristicValue(change.newValue, change.characteristic.props) + this._server.sendEventNotifications(accessory.aid, change.characteristic.iid, value, change.originator, immediateDelivery) } } private sendCharacteristicWarning(characteristic: Characteristic, type: CharacteristicWarningType, message: string): void { this.handleCharacteristicWarning({ - characteristic: characteristic, - type: type, - message: message, + characteristic, + type, + message, originatorChain: [characteristic.displayName], // we are missing the service displayName, but that's okay - stack: new Error().stack, - }); + stack: new Error().stack, // eslint-disable-line unicorn/error-message + }) } private handleCharacteristicWarning(warning: CharacteristicWarning): void { - warning.originatorChain = [this.displayName, ...warning.originatorChain]; + warning.originatorChain = [this.displayName, ...warning.originatorChain] - const emitted = this.emit(AccessoryEventTypes.CHARACTERISTIC_WARNING, warning); + const emitted = this.emit(AccessoryEventTypes.CHARACTERISTIC_WARNING, warning) if (!emitted) { - const message = `[${warning.originatorChain.join("@")}] ${warning.message}`; + const message = `[${warning.originatorChain.join('@')}] ${warning.message}` if (warning.type === CharacteristicWarningType.ERROR_MESSAGE - || warning.type === CharacteristicWarningType.TIMEOUT_READ|| warning.type === CharacteristicWarningType.TIMEOUT_WRITE) { - console.error(message); + || warning.type === CharacteristicWarningType.TIMEOUT_READ || warning.type === CharacteristicWarningType.TIMEOUT_WRITE) { + console.error(message) } else { - console.warn(message); + console.warn(message) } - debug("[%s] Above characteristic warning was thrown at: %s", this.displayName, warning.stack ?? "unknown"); + debug('[%s] Above characteristic warning was thrown at: %s', this.displayName, warning.stack ?? 'unknown') } } private setupServiceEventHandlers(service: Service): void { - service.on(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE, this.handleServiceConfigurationChangeEvent.bind(this, service)); - service.on(ServiceEventTypes.CHARACTERISTIC_CHANGE, this.handleCharacteristicChangeEvent.bind(this, this, service)); - service.on(ServiceEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this)); + service.on(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE, this.handleServiceConfigurationChangeEvent.bind(this, service)) + service.on(ServiceEventTypes.CHARACTERISTIC_CHANGE, this.handleCharacteristicChangeEvent.bind(this, this, service)) + service.on(ServiceEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this)) } private _sideloadServices(targetServices: Service[]): void { for (const service of targetServices) { - this.setupServiceEventHandlers(service); + this.setupServiceEventHandlers(service) } - this.services = targetServices.slice(); + this.services = targetServices.slice() // Fix Identify this @@ -1975,23 +1975,23 @@ export class Accessory extends EventEmitter { .getCharacteristic(Characteristic.Identify)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { if (value) { - const paired = true; - this.identificationRequest(paired, callback); + const paired = true + this.identificationRequest(paired, callback) } - }); + }) } private static _generateSetupID(): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const max = chars.length; - let setupID = ""; + const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + const max = chars.length + let setupID = '' for (let i = 0; i < 4; i++) { - const index = Math.floor(Math.random() * max); - setupID += chars.charAt(index); + const index = Math.floor(Math.random() * max) + setupID += chars.charAt(index) } - return setupID; + return setupID } // serialization and deserialization functions, mainly designed for homebridge to create a json copy to store on disk @@ -1999,203 +1999,202 @@ export class Accessory extends EventEmitter { const json: SerializedAccessory = { displayName: accessory.displayName, UUID: accessory.UUID, - lastKnownUsername: accessory._accessoryInfo? accessory._accessoryInfo.username: undefined, + lastKnownUsername: accessory._accessoryInfo ? accessory._accessoryInfo.username : undefined, category: accessory.category, services: [], - }; + } - const linkedServices: Record = {}; - let hasLinkedServices = false; + const linkedServices: Record = {} + let hasLinkedServices = false - accessory.services.forEach(service => { - json.services.push(Service.serialize(service)); + accessory.services.forEach((service) => { + json.services.push(Service.serialize(service)) - const linkedServicesPresentation: ServiceId[] = []; - service.linkedServices.forEach(linkedService => { - linkedServicesPresentation.push(linkedService.getServiceId()); - }); + const linkedServicesPresentation: ServiceId[] = [] + service.linkedServices.forEach((linkedService) => { + linkedServicesPresentation.push(linkedService.getServiceId()) + }) if (linkedServicesPresentation.length > 0) { - linkedServices[service.getServiceId()] = linkedServicesPresentation; - hasLinkedServices = true; + linkedServices[service.getServiceId()] = linkedServicesPresentation + hasLinkedServices = true } - }); + }) if (hasLinkedServices) { - json.linkedServices = linkedServices; + json.linkedServices = linkedServices } - const controllers: SerializedControllerContext[] = []; + const controllers: SerializedControllerContext[] = [] // save controllers - Object.values(accessory.controllers).forEach((context: ControllerContext) => { + Object.values(accessory.controllers).forEach((context: ControllerContext) => { controllers.push({ type: context.controller.controllerId(), services: Accessory.serializeServiceMap(context.serviceMap), - }); - }); + }) + }) // also save controller which didn't get initialized (could lead to service duplication if we throw that data away) Object.entries(accessory.serializedControllers || {}).forEach(([id, serviceMap]) => { controllers.push({ type: id, services: Accessory.serializeServiceMap(serviceMap), - }); - }); + }) + }) if (controllers.length > 0) { - json.controllers = controllers; + json.controllers = controllers } - return json; + return json } public static deserialize(json: SerializedAccessory): Accessory { - const accessory = new Accessory(json.displayName, json.UUID); + const accessory = new Accessory(json.displayName, json.UUID) - accessory.lastKnownUsername = json.lastKnownUsername; - accessory.category = json.category; + accessory.lastKnownUsername = json.lastKnownUsername + accessory.category = json.category - const services: Service[] = []; - const servicesMap: Record = {}; + const services: Service[] = [] + const servicesMap: Record = {} - json.services.forEach(serialized => { - const service = Service.deserialize(serialized); + json.services.forEach((serialized) => { + const service = Service.deserialize(serialized) - services.push(service); - servicesMap[service.getServiceId()] = service; - }); + services.push(service) + servicesMap[service.getServiceId()] = service + }) if (json.linkedServices) { - for (const [ serviceId, linkedServicesKeys ] of Object.entries(json.linkedServices)) { - const primaryService = servicesMap[serviceId]; + for (const [serviceId, linkedServicesKeys] of Object.entries(json.linkedServices)) { + const primaryService = servicesMap[serviceId] if (!primaryService) { - continue; + continue } - linkedServicesKeys.forEach(linkedServiceKey => { - const linkedService = servicesMap[linkedServiceKey]; + linkedServicesKeys.forEach((linkedServiceKey) => { + const linkedService = servicesMap[linkedServiceKey] if (linkedService) { - primaryService.addLinkedService(linkedService); + primaryService.addLinkedService(linkedService) } - }); + }) } } if (json.controllers) { // just save it for later if it exists {@see configureController} - accessory.serializedControllers = {}; + accessory.serializedControllers = {} - json.controllers.forEach(serializedController => { - accessory.serializedControllers![serializedController.type] = Accessory.deserializeServiceMap(serializedController.services, servicesMap); - }); + json.controllers.forEach((serializedController) => { + accessory.serializedControllers![serializedController.type] = Accessory.deserializeServiceMap(serializedController.services, servicesMap) + }) } - accessory._sideloadServices(services); + accessory._sideloadServices(services) - return accessory; + return accessory } public static cleanupAccessoryData(username: MacAddress): void { - IdentifierCache.remove(username); - AccessoryInfo.remove(username); - ControllerStorage.remove(username); + IdentifierCache.remove(username) + AccessoryInfo.remove(username) + ControllerStorage.remove(username) } private static serializeServiceMap(serviceMap: ControllerServiceMap): SerializedServiceMap { - const serialized: SerializedServiceMap = {}; + const serialized: SerializedServiceMap = {} Object.entries(serviceMap).forEach(([name, service]) => { if (!service) { - return; + return } - serialized[name] = service.getServiceId(); - }); + serialized[name] = service.getServiceId() + }) - return serialized; + return serialized } private static deserializeServiceMap(serializedServiceMap: SerializedServiceMap, servicesMap: Record): ControllerServiceMap { - const controllerServiceMap: ControllerServiceMap = {}; + const controllerServiceMap: ControllerServiceMap = {} Object.entries(serializedServiceMap).forEach(([name, serviceId]) => { - const service = servicesMap[serviceId]; + const service = servicesMap[serviceId] if (service) { - controllerServiceMap[name] = service; + controllerServiceMap[name] = service } - }); + }) - return controllerServiceMap; + return controllerServiceMap } private static parseBindOption(info: PublishInfo): { - advertiserAddress?: string[], - serviceRestrictedAddress?: string[], - serviceDisableIpv6?: boolean, - serverAddress?: string, + advertiserAddress?: string[] + serviceRestrictedAddress?: string[] + serviceDisableIpv6?: boolean + serverAddress?: string } { - let advertiserAddress: string[] | undefined = undefined; - let disableIpv6: boolean | undefined = undefined; - let serverAddress: string | undefined = undefined; + let advertiserAddress: string[] | undefined + let disableIpv6: boolean | undefined + let serverAddress: string | undefined if (info.bind) { - const entries: Set = new Set(Array.isArray(info.bind)? info.bind: [info.bind]); + const entries: Set = new Set(Array.isArray(info.bind) ? info.bind : [info.bind]) - if (entries.has("::")) { - serverAddress = "::"; + if (entries.has('::')) { + serverAddress = '::' - entries.delete("::"); + entries.delete('::') if (entries.size) { - advertiserAddress = Array.from(entries); + advertiserAddress = Array.from(entries) } - } else if (entries.has("0.0.0.0")) { - disableIpv6 = true; - serverAddress = "0.0.0.0"; + } else if (entries.has('0.0.0.0')) { + disableIpv6 = true + serverAddress = '0.0.0.0' - entries.delete("0.0.0.0"); + entries.delete('0.0.0.0') if (entries.size) { - advertiserAddress = Array.from(entries); + advertiserAddress = Array.from(entries) } } else if (entries.size === 1) { - advertiserAddress = Array.from(entries); + advertiserAddress = Array.from(entries) - const entry = entries.values().next().value; // grab the first one + const entry = entries.values().next().value // grab the first one - const version = net.isIP(entry); // check if ip address was specified or an interface name + const version = isIP(entry) // check if ip address was specified or an interface name if (version) { - serverAddress = version === 4? "0.0.0.0": "::"; // we currently bind to unspecified addresses so config-ui always has a connection via loopback + serverAddress = version === 4 ? '0.0.0.0' : '::' // we currently bind to unspecified addresses so config-ui always has a connection via loopback } else { - serverAddress = "::"; // the interface could have both ipv4 and ipv6 addresses + serverAddress = '::' // the interface could have both ipv4 and ipv6 addresses } } else if (entries.size > 1) { - advertiserAddress = Array.from(entries); + advertiserAddress = Array.from(entries) - let bindUnspecifiedIpv6 = false; // we bind on "::" if there are interface names, or we detect ipv6 addresses + let bindUnspecifiedIpv6 = false // we bind on "::" if there are interface names, or we detect ipv6 addresses for (const entry of entries) { - const version = net.isIP(entry); + const version = isIP(entry) if (version === 0 || version === 6) { - bindUnspecifiedIpv6 = true; - break; + bindUnspecifiedIpv6 = true + break } } if (bindUnspecifiedIpv6) { - serverAddress = "::"; + serverAddress = '::' } else { - serverAddress = "0.0.0.0"; + serverAddress = '0.0.0.0' } } } return { - advertiserAddress: advertiserAddress, + advertiserAddress, serviceRestrictedAddress: advertiserAddress, serviceDisableIpv6: disableIpv6, - serverAddress: serverAddress, - }; + serverAddress, + } } - } diff --git a/src/lib/Advertiser.spec.ts b/src/lib/Advertiser.spec.ts index 08c4546e0..c9b35ec7e 100644 --- a/src/lib/Advertiser.spec.ts +++ b/src/lib/Advertiser.spec.ts @@ -1,29 +1,31 @@ -import { CiaoAdvertiser, PairingFeatureFlag, StatusFlag } from "./Advertiser"; +import { describe, expect, it } from 'vitest' + +import { CiaoAdvertiser, PairingFeatureFlag, StatusFlag } from './Advertiser.js' describe(CiaoAdvertiser, () => { - describe("ff and sf", () => { - it("should correctly format pairing feature flags", () => { - expect(CiaoAdvertiser.ff()).toEqual(0); - expect(CiaoAdvertiser.ff(PairingFeatureFlag.SUPPORTS_HARDWARE_AUTHENTICATION)).toEqual(1); - expect(CiaoAdvertiser.ff(PairingFeatureFlag.SUPPORTS_SOFTWARE_AUTHENTICATION)).toEqual(2); + describe('ff and sf', () => { + it('should correctly format pairing feature flags', () => { + expect(CiaoAdvertiser.ff()).toEqual(0) + expect(CiaoAdvertiser.ff(PairingFeatureFlag.SUPPORTS_HARDWARE_AUTHENTICATION)).toEqual(1) + expect(CiaoAdvertiser.ff(PairingFeatureFlag.SUPPORTS_SOFTWARE_AUTHENTICATION)).toEqual(2) expect(CiaoAdvertiser.ff( PairingFeatureFlag.SUPPORTS_HARDWARE_AUTHENTICATION, PairingFeatureFlag.SUPPORTS_SOFTWARE_AUTHENTICATION, - )).toEqual(3); - }); + )).toEqual(3) + }) - it("should correctly format status flags", () => { - expect(CiaoAdvertiser.sf()).toEqual(0); + it('should correctly format status flags', () => { + expect(CiaoAdvertiser.sf()).toEqual(0) - expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED)).toEqual(1); - expect(CiaoAdvertiser.sf(StatusFlag.NOT_JOINED_WIFI)).toEqual(2); - expect(CiaoAdvertiser.sf(StatusFlag.PROBLEM_DETECTED)).toEqual(4); + expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED)).toEqual(1) + expect(CiaoAdvertiser.sf(StatusFlag.NOT_JOINED_WIFI)).toEqual(2) + expect(CiaoAdvertiser.sf(StatusFlag.PROBLEM_DETECTED)).toEqual(4) - expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.NOT_JOINED_WIFI)).toEqual(3); - expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.PROBLEM_DETECTED)).toEqual(5); - expect(CiaoAdvertiser.sf(StatusFlag.NOT_JOINED_WIFI, StatusFlag.PROBLEM_DETECTED)).toEqual(6); + expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.NOT_JOINED_WIFI)).toEqual(3) + expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.PROBLEM_DETECTED)).toEqual(5) + expect(CiaoAdvertiser.sf(StatusFlag.NOT_JOINED_WIFI, StatusFlag.PROBLEM_DETECTED)).toEqual(6) - expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.NOT_JOINED_WIFI, StatusFlag.PROBLEM_DETECTED)).toEqual(7); - }); - }); -}); + expect(CiaoAdvertiser.sf(StatusFlag.NOT_PAIRED, StatusFlag.NOT_JOINED_WIFI, StatusFlag.PROBLEM_DETECTED)).toEqual(7) + }) + }) +}) diff --git a/src/lib/Advertiser.ts b/src/lib/Advertiser.ts index 301e075c8..95801ca0a 100644 --- a/src/lib/Advertiser.ts +++ b/src/lib/Advertiser.ts @@ -1,17 +1,24 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import ciao, { CiaoService, MDNSServerOptions, Responder, ServiceEvent, ServiceTxt, ServiceType } from "@homebridge/ciao"; -import { InterfaceName, IPAddress } from "@homebridge/ciao/lib/NetworkManager"; -import dbus, { DBusInterface, MessageBus } from "@homebridge/dbus-native"; -import assert from "assert"; -import bonjour, { BonjourHAP, BonjourHAPService } from "bonjour-hap"; -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { AccessoryInfo } from "./model/AccessoryInfo"; -import { PromiseTimeout } from "./util/promise-utils"; - -const debug = createDebug("HAP-NodeJS:Advertiser"); +import type { CiaoService, MDNSServerOptions, Responder, ServiceTxt } from '@homebridge/ciao' +import type { InterfaceName, IPAddress } from '@homebridge/ciao/lib/NetworkManager.js' +import type { DBusInterface, MessageBus } from '@homebridge/dbus-native' +import type { BonjourHAP, BonjourHAPService } from 'bonjour-hap' + +import type { AccessoryInfo } from './model/AccessoryInfo' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' +import { EventEmitter } from 'node:events' + +import { getResponder, ServiceEvent, ServiceType } from '@homebridge/ciao' +import bonjour from 'bonjour-hap' +import createDebug from 'debug' + +import { systemBus } from './dbus/index.js' +import { PromiseTimeout } from './util/promise-utils.js' + +const debug = createDebug('HAP-NodeJS:Advertiser') /** * This enum lists all bitmasks for all known status flags. @@ -19,6 +26,7 @@ const debug = createDebug("HAP-NodeJS:Advertiser"); * * @group Advertiser */ +// eslint-disable-next-line no-restricted-syntax export const enum StatusFlag { NOT_PAIRED = 0x01, NOT_JOINED_WIFI = 0x02, @@ -31,6 +39,7 @@ export const enum StatusFlag { * * @group Advertiser */ +// eslint-disable-next-line no-restricted-syntax export const enum PairingFeatureFlag { SUPPORTS_HARDWARE_AUTHENTICATION = 0x01, SUPPORTS_SOFTWARE_AUTHENTICATION = 0x02, @@ -39,21 +48,23 @@ export const enum PairingFeatureFlag { /** * @group Advertiser */ +// eslint-disable-next-line no-restricted-syntax export const enum AdvertiserEvent { /** * Emitted if the underlying mDNS advertisers signals, that the service name * was automatically changed due to some naming conflicts on the network. */ - UPDATED_NAME = "updated-name", + UPDATED_NAME = 'updated-name', } /** * @group Advertiser */ export declare interface Advertiser { - on(event: "updated-name", listener: (name: string) => void): this; - - emit(event: "updated-name", name: string): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'updated-name', listener: (name: string) => void): this + emit(event: 'updated-name', name: string): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -74,30 +85,30 @@ export interface ServiceNetworkOptions { * If the service is set to advertise on a given interface, though the MDNSServer is * configured to ignore this interface, the service won't be advertised on the interface. */ - restrictedAddresses?: (InterfaceName | IPAddress)[]; + restrictedAddresses?: (InterfaceName | IPAddress)[] /** * The service won't advertise ipv6 address records. * This can be used to simulate binding on 0.0.0.0. * May be combined with {@link restrictedAddresses}. */ - disabledIpv6?: boolean; + disabledIpv6?: boolean } /** * A generic Advertiser interface required for any MDNS Advertiser backend implementations. * - * All implementations have to extend NodeJS' {@link EventEmitter} and emit the events defined in {@link AdvertiserEvent}. + * All implementations have to extend Node.js {@link EventEmitter} and emit the events defined in {@link AdvertiserEvent}. * * @group Advertiser */ export interface Advertiser { - initPort(port: number): void; + initPort: (port: number) => void - startAdvertising(): Promise; + startAdvertising: () => Promise - updateAdvertisement(silent?: boolean): void; + updateAdvertisement: (silent?: boolean) => void - destroy(): void; + destroy: () => void } /** @@ -111,92 +122,92 @@ export interface Advertiser { * @group Advertiser */ export class CiaoAdvertiser extends EventEmitter implements Advertiser { - static protocolVersion = "1.1"; - static protocolVersionService = "1.1.0"; + static protocolVersion = '1.1' + static protocolVersionService = '1.1.0' - private readonly accessoryInfo: AccessoryInfo; - private readonly setupHash: string; + private readonly accessoryInfo: AccessoryInfo + private readonly setupHash: string - private readonly responder: Responder; - private readonly advertisedService: CiaoService; + private readonly responder: Responder + private readonly advertisedService: CiaoService constructor(accessoryInfo: AccessoryInfo, responderOptions?: MDNSServerOptions, serviceOptions?: ServiceNetworkOptions) { - super(); - this.accessoryInfo = accessoryInfo; - this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); + super() + this.accessoryInfo = accessoryInfo + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo) - this.responder = ciao.getResponder({ + this.responder = getResponder({ ...responderOptions, - }); + }) this.advertisedService = this.responder.createService({ name: this.accessoryInfo.displayName, type: ServiceType.HAP, txt: CiaoAdvertiser.createTxt(accessoryInfo, this.setupHash), // host will default now to .local, spaces replaced with dashes ...serviceOptions, - }); - this.advertisedService.on(ServiceEvent.NAME_CHANGED, this.emit.bind(this, AdvertiserEvent.UPDATED_NAME)); + }) + this.advertisedService.on(ServiceEvent.NAME_CHANGED, this.emit.bind(this, AdvertiserEvent.UPDATED_NAME)) - debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using ciao backend!`); + debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using ciao backend!`) } public initPort(port: number): void { - this.advertisedService.updatePort(port); + this.advertisedService.updatePort(port) } public startAdvertising(): Promise { - debug(`Starting to advertise '${this.accessoryInfo.displayName}' using ciao backend!`); - return this.advertisedService!.advertise(); + debug(`Starting to advertise '${this.accessoryInfo.displayName}' using ciao backend!`) + return this.advertisedService!.advertise() } public updateAdvertisement(silent?: boolean): void { - const txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash); - debug("Updating txt record (txt: %o, silent: %d)", txt, silent); - this.advertisedService!.updateTxt(txt, silent); + const txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash) + debug('Updating txt record (txt: %o, silent: %d)', txt, silent) + this.advertisedService!.updateTxt(txt, silent) } public async destroy(): Promise { // advertisedService.destroy(); is called implicitly via the shutdown call - await this.responder.shutdown(); - this.removeAllListeners(); + await this.responder.shutdown() + this.removeAllListeners() } static createTxt(accessoryInfo: AccessoryInfo, setupHash: string): ServiceTxt { - const statusFlags: StatusFlag[] = []; + const statusFlags: StatusFlag[] = [] if (!accessoryInfo.paired()) { - statusFlags.push(StatusFlag.NOT_PAIRED); + statusFlags.push(StatusFlag.NOT_PAIRED) } return { - "c#": accessoryInfo.getConfigVersion(), // current configuration number - ff: CiaoAdvertiser.ff(), // pairing feature flags - id: accessoryInfo.username, // device id - md: accessoryInfo.model, // model name - pv: CiaoAdvertiser.protocolVersion, // protocol version - "s#": 1, // current state number (must be 1) - sf: CiaoAdvertiser.sf(...statusFlags), // status flags - ci: accessoryInfo.category, - sh: setupHash, - }; + 'c#': accessoryInfo.getConfigVersion(), // current configuration number + 'ff': CiaoAdvertiser.ff(), // pairing feature flags + 'id': accessoryInfo.username, // device id + 'md': accessoryInfo.model, // model name + 'pv': CiaoAdvertiser.protocolVersion, // protocol version + 's#': 1, // current state number (must be 1) + 'sf': CiaoAdvertiser.sf(...statusFlags), // status flags + 'ci': accessoryInfo.category, + 'sh': setupHash, + } } static computeSetupHash(accessoryInfo: AccessoryInfo): string { - const hash = crypto.createHash("sha512"); - hash.update(accessoryInfo.setupID + accessoryInfo.username.toUpperCase()); - return hash.digest().slice(0, 4).toString("base64"); + const hash = createHash('sha512') + hash.update(accessoryInfo.setupID + accessoryInfo.username.toUpperCase()) + return hash.digest().toString('base64').substring(0, 4) } public static ff(...flags: PairingFeatureFlag[]): number { - let value = 0; - flags.forEach(flag => value |= flag); - return value; + let value = 0 + flags.forEach(flag => value |= flag) + return value } public static sf(...flags: StatusFlag[]): number { - let value = 0; - flags.forEach(flag => value |= flag); - return value; + let value = 0 + flags.forEach(flag => value |= flag) + return value } } @@ -206,123 +217,120 @@ export class CiaoAdvertiser extends EventEmitter implements Advertiser { * @group Advertiser */ export class BonjourHAPAdvertiser extends EventEmitter implements Advertiser { - private readonly accessoryInfo: AccessoryInfo; - private readonly setupHash: string; - private readonly serviceOptions?: ServiceNetworkOptions; + private readonly accessoryInfo: AccessoryInfo + private readonly setupHash: string + private readonly serviceOptions?: ServiceNetworkOptions - private bonjour: BonjourHAP; - private advertisement?: BonjourHAPService; + private bonjour: BonjourHAP + private advertisement?: BonjourHAPService - private port?: number; - private destroyed = false; + private port?: number + private destroyed = false constructor(accessoryInfo: AccessoryInfo, serviceOptions?: ServiceNetworkOptions) { - super(); - this.accessoryInfo = accessoryInfo; - this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); - this.serviceOptions = serviceOptions; + super() + this.accessoryInfo = accessoryInfo + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo) + this.serviceOptions = serviceOptions - this.bonjour = bonjour(); - debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using bonjour-hap backend!`); + this.bonjour = bonjour() + debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using bonjour-hap backend!`) } public initPort(port: number): void { - this.port = port; + this.port = port } public startAdvertising(): Promise { - assert(!this.destroyed, "Can't advertise on a destroyed bonjour instance!"); + assert(!this.destroyed, 'Can\'t advertise on a destroyed bonjour instance!') if (this.port == null) { - throw new Error("Tried starting bonjour-hap advertisement without initializing port!"); + throw new Error('Tried starting bonjour-hap advertisement without initializing port!') } - debug(`Starting to advertise '${this.accessoryInfo.displayName}' using bonjour-hap backend!`); + debug(`Starting to advertise '${this.accessoryInfo.displayName}' using bonjour-hap backend!`) if (this.advertisement) { - this.destroy(); + this.destroy() } - const hostname = this.accessoryInfo.username.replace(/:/ig, "_") + ".local"; + const hostname = `${this.accessoryInfo.username.replace(/:/g, '_')}.local` this.advertisement = this.bonjour.publish({ name: this.accessoryInfo.displayName, - type: "hap", + type: 'hap', port: this.port, txt: CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), host: hostname, addUnsafeServiceEnumerationRecord: true, ...this.serviceOptions, - }); + }) - return PromiseTimeout(1); + return PromiseTimeout(1) } public updateAdvertisement(silent?: boolean): void { - const txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash); - debug("Updating txt record (txt: %o, silent: %d)", txt, silent); + const txt = CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash) + debug('Updating txt record (txt: %o, silent: %d)', txt, silent) if (this.advertisement) { - this.advertisement.updateTxt(txt, silent); + this.advertisement.updateTxt(txt, silent) } } public destroy(): void { if (this.advertisement) { this.advertisement.stop(() => { - this.advertisement!.destroy(); - this.advertisement = undefined; - this.bonjour.destroy(); - }); + this.advertisement!.destroy() + this.advertisement = undefined + this.bonjour.destroy() + }) } else { - this.bonjour.destroy(); + this.bonjour.destroy() } } - } function messageBusConnectionResult(bus: MessageBus): Promise { return new Promise((resolve, reject) => { const errorHandler = (error: Error) => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - bus.connection.removeListener("connect", connectHandler); - reject(error); - }; + // eslint-disable-next-line ts/no-use-before-define + bus.connection.removeListener('connect', connectHandler) + reject(error) + } const connectHandler = () => { - bus.connection.removeListener("error", errorHandler); - resolve(); - }; + bus.connection.removeListener('error', errorHandler) + resolve() + } - bus.connection.once("connect", connectHandler); - bus.connection.once("error", errorHandler); - }); + bus.connection.once('connect', connectHandler) + bus.connection.once('error', errorHandler) + }) } /** * @group Advertiser */ export class DBusInvokeError extends Error { - readonly errorName: string; + readonly errorName: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(errorObject: { name: string, message: any }) { - super(); + super() - Object.setPrototypeOf(this, DBusInvokeError.prototype); + Object.setPrototypeOf(this, DBusInvokeError.prototype) - this.name = "DBusInvokeError"; + this.name = 'DBusInvokeError' - this.errorName = errorObject.name; + this.errorName = errorObject.name if (Array.isArray(errorObject.message) && errorObject.message.length === 1) { - this.message = errorObject.message[0]; + this.message = errorObject.message[0] } else { - this.message = errorObject.message.toString(); + this.message = errorObject.message.toString() } } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInterface: string, member: string, others?: any): Promise { +function dbusInvoke(bus: MessageBus, destination: string, path: string, dbusInterface: string, member: string, others?: any): Promise { return new Promise((resolve, reject) => { const command = { destination, @@ -330,20 +338,18 @@ function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInt interface: dbusInterface, member, ...(others || {}), - }; + } bus.invoke(command, (err, result) => { if (err) { - reject(new DBusInvokeError(err)); + reject(new DBusInvokeError(err)) } else { - resolve(result); + resolve(result) } - }); - - }); + }) + }) } - /** * AvahiServerState. * @@ -351,13 +357,13 @@ function dbusInvoke( bus: MessageBus, destination: string, path: string, dbusInt * * @group Advertiser */ +// eslint-disable-next-line no-restricted-syntax const enum AvahiServerState { - // noinspection JSUnusedGlobalSymbols INVALID = 0, REGISTERING, RUNNING, COLLISION, - FAILURE + FAILURE, } /** @@ -370,77 +376,77 @@ const enum AvahiServerState { * @group Advertiser */ export class AvahiAdvertiser extends EventEmitter implements Advertiser { - private readonly accessoryInfo: AccessoryInfo; - private readonly setupHash: string; + private readonly accessoryInfo: AccessoryInfo + private readonly setupHash: string - private port?: number; + private port?: number - private bus?: MessageBus; - private avahiServerInterface?: DBusInterface; - private path?: string; + private bus?: MessageBus + private avahiServerInterface?: DBusInterface + private path?: string - private readonly stateChangeHandler: (state: AvahiServerState) => void; + private readonly stateChangeHandler: (state: AvahiServerState) => void constructor(accessoryInfo: AccessoryInfo) { - super(); - this.accessoryInfo = accessoryInfo; - this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); + super() + this.accessoryInfo = accessoryInfo + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo) - debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using Avahi backend!`); + debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using Avahi backend!`) - this.bus = dbus.systemBus(); + this.bus = systemBus() - this.stateChangeHandler = this.handleStateChangedEvent.bind(this); + this.stateChangeHandler = this.handleStateChangedEvent.bind(this) } private createTxt(): Array { return Object .entries(CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash)) - .map((el: Array) => Buffer.from(el[0] + "=" + el[1])); + .map((el: Array) => Buffer.from(`${el[0]}=${el[1]}`)) } public initPort(port: number): void { - this.port = port; + this.port = port } public async startAdvertising(): Promise { if (this.port == null) { - throw new Error("Tried starting Avahi advertisement without initializing port!"); + throw new Error('Tried starting Avahi advertisement without initializing port!') } if (!this.bus) { - throw new Error("Tried to start Avahi advertisement on a destroyed advertiser!"); + throw new Error('Tried to start Avahi advertisement on a destroyed advertiser!') } - debug(`Starting to advertise '${this.accessoryInfo.displayName}' using Avahi backend!`); + debug(`Starting to advertise '${this.accessoryInfo.displayName}' using Avahi backend!`) - this.path = await AvahiAdvertiser.avahiInvoke(this.bus, "/", "Server", "EntryGroupNew") as string; - await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "AddService", { + this.path = await AvahiAdvertiser.avahiInvoke(this.bus, '/', 'Server', 'EntryGroupNew') as string + await AvahiAdvertiser.avahiInvoke(this.bus, this.path, 'EntryGroup', 'AddService', { body: [ -1, // interface -1, // protocol 0, // flags this.accessoryInfo.displayName, // name - "_hap._tcp", // type - "", // domain - "", // host + '_hap._tcp', // type + '', // domain + '', // host this.port, // port this.createTxt(), // txt ], - signature: "iiussssqaay", - }); - await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "Commit"); + signature: 'iiussssqaay', + }) + await AvahiAdvertiser.avahiInvoke(this.bus, this.path, 'EntryGroup', 'Commit') try { if (!this.avahiServerInterface) { - this.avahiServerInterface = await AvahiAdvertiser.avahiInterface(this.bus, "Server"); - this.avahiServerInterface.on("StateChanged", this.stateChangeHandler); + this.avahiServerInterface = await AvahiAdvertiser.avahiInterface(this.bus, 'Server') + this.avahiServerInterface.on('StateChanged', this.stateChangeHandler) } } catch (error) { // We have some problem on Synology https://github.com/homebridge/HAP-NodeJS/issues/993 - console.warn("Failed to create listener for avahi-daemon server state. The system will not be notified about restarts of avahi-daemon " + - "and will therefore stay undiscoverable in those instances. Error message: " + error); + console.warn(`Failed to create listener for avahi-daemon server state. The system will not be notified about restarts of avahi-daemon ` + + `and will therefore stay undiscoverable in those instances. Error message: ${error}`) if (error.stack) { - debug("Detailed error: " + error.stack); + debug(`Detailed error: ${error.stack}`) } } } @@ -453,117 +459,113 @@ export class AvahiAdvertiser extends EventEmitter implements Advertiser { */ private handleStateChangedEvent(state: AvahiServerState): void { if (state === AvahiServerState.RUNNING && this.path) { - debug("Found Avahi daemon to have restarted!"); + debug('Found Avahi daemon to have restarted!') this.startAdvertising() - .catch(reason => console.error("Could not (re-)create mDNS advertisement. The HAP-Server won't be discoverable: " + reason)); + .catch(reason => console.error(`Could not (re-)create mDNS advertisement. The HAP-Server won't be discoverable: ${reason}`)) } } public async updateAdvertisement(silent?: boolean): Promise { if (!this.bus) { - throw new Error("Tried to update Avahi advertisement on a destroyed advertiser!"); + throw new Error('Tried to update Avahi advertisement on a destroyed advertiser!') } if (!this.path) { - debug("Tried to update advertisement without a valid `path`!"); - return; + debug('Tried to update advertisement without a valid `path`!') + return } - debug("Updating txt record (txt: %o, silent: %d)", CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), silent); + debug('Updating txt record (txt: %o, silent: %d)', CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), silent) try { - await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "UpdateServiceTxt", { - body: [-1, -1, 0, this.accessoryInfo.displayName, "_hap._tcp", "", this.createTxt()], - signature: "iiusssaay", - }); + await AvahiAdvertiser.avahiInvoke(this.bus, this.path, 'EntryGroup', 'UpdateServiceTxt', { + body: [-1, -1, 0, this.accessoryInfo.displayName, '_hap._tcp', '', this.createTxt()], + signature: 'iiusssaay', + }) } catch (error) { - console.error("Failed to update avahi advertisement: " + error); + console.error(`Failed to update avahi advertisement: ${error}`) } } public async destroy(): Promise { if (!this.bus) { - throw new Error("Tried to destroy Avahi advertisement on a destroyed advertiser!"); + throw new Error('Tried to destroy Avahi advertisement on a destroyed advertiser!') } if (this.path) { try { - await AvahiAdvertiser.avahiInvoke(this.bus, this.path, "EntryGroup", "Free"); + await AvahiAdvertiser.avahiInvoke(this.bus, this.path, 'EntryGroup', 'Free') } catch (error) { // Typically, this fails if e.g. avahi service was stopped in the meantime. - debug("Destroying Avahi advertisement failed: " + error); + debug(`Destroying Avahi advertisement failed: ${error}`) } - this.path = undefined; + this.path = undefined } if (this.avahiServerInterface) { - this.avahiServerInterface.removeListener("StateChanged", this.stateChangeHandler); - this.avahiServerInterface = undefined; + this.avahiServerInterface.removeListener('StateChanged', this.stateChangeHandler) + this.avahiServerInterface = undefined } - this.bus.connection.stream.destroy(); - this.bus = undefined; + this.bus.connection.stream.destroy() + this.bus = undefined } public static async isAvailable(): Promise { - const bus = dbus.systemBus(); - + const bus = systemBus() try { try { - await messageBusConnectionResult(bus); + await messageBusConnectionResult(bus) } catch (error) { - debug("Avahi/DBus classified unavailable due to missing dbus interface!"); - return false; + return false } try { - const version = await this.avahiInvoke(bus, "/", "Server", "GetVersionString"); - debug("Detected Avahi over DBus interface running version '%s'.", version); + const version = await this.avahiInvoke(bus, '/', 'Server', 'GetVersionString') + debug('Detected Avahi over DBus interface running version \'%s\'.', version) } catch (error) { - debug("Avahi/DBus classified unavailable due to missing avahi interface!"); - return false; + debug('Avahi/DBus classified unavailable due to missing avahi interface!') + return false } - return true; + return true } finally { - bus.connection.stream.destroy(); + bus.connection.stream.destroy() } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private static avahiInvoke(bus: MessageBus, path: string, dbusInterface: string, member: string, others?: any): Promise { return dbusInvoke( bus, - "org.freedesktop.Avahi", + 'org.freedesktop.Avahi', path, `org.freedesktop.Avahi.${dbusInterface}`, member, others, - ); + ) } private static avahiInterface(bus: MessageBus, dbusInterface: string): Promise { return new Promise((resolve, reject) => { bus - .getService("org.freedesktop.Avahi") - .getInterface("/", "org.freedesktop.Avahi." + dbusInterface, (error, iface) => { + .getService('org.freedesktop.Avahi') + .getInterface('/', `org.freedesktop.Avahi.${dbusInterface}`, (error, iface) => { if (error || !iface) { - reject(error ?? new Error("Interface not present!")); + reject(error ?? new Error('Interface not present!')) } else { - resolve(iface); + resolve(iface) } - }); - }); + }) + }) } } -type ResolvedServiceTxt = Array>; +type ResolvedServiceTxt = Array> const RESOLVED_PERMISSIONS_ERRORS = [ - "org.freedesktop.DBus.Error.AccessDenied", - "org.freedesktop.DBus.Error.AuthFailed", - "org.freedesktop.DBus.Error.InteractiveAuthorizationRequired", -]; - + 'org.freedesktop.DBus.Error.AccessDenied', + 'org.freedesktop.DBus.Error.AuthFailed', + 'org.freedesktop.DBus.Error.InteractiveAuthorizationRequired', +] /** * Advertiser based on the systemd-resolved D-Bus library. @@ -572,176 +574,174 @@ const RESOLVED_PERMISSIONS_ERRORS = [ * @group Advertiser */ export class ResolvedAdvertiser extends EventEmitter implements Advertiser { - private readonly accessoryInfo: AccessoryInfo; - private readonly setupHash: string; + private readonly accessoryInfo: AccessoryInfo + private readonly setupHash: string - private port?: number; + private port?: number - private bus?: MessageBus; - private path?: string; + private bus?: MessageBus + private path?: string constructor(accessoryInfo: AccessoryInfo) { - super(); - this.accessoryInfo = accessoryInfo; - this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo); + super() + this.accessoryInfo = accessoryInfo + this.setupHash = CiaoAdvertiser.computeSetupHash(accessoryInfo) - this.bus = dbus.systemBus(); + this.bus = systemBus() - debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using systemd-resolved backend!`); + debug(`Preparing Advertiser for '${this.accessoryInfo.displayName}' using systemd-resolved backend!`) } private createTxt(): ResolvedServiceTxt { return Object .entries(CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash)) - .map((el: Array) => [el[0].toString(), Buffer.from(el[1].toString())]); + .map((el: Array) => [el[0].toString(), Buffer.from(el[1].toString())]) } public initPort(port: number): void { - this.port = port; + this.port = port } public async startAdvertising(): Promise { if (this.port == null) { - throw new Error("Tried starting systemd-resolved advertisement without initializing port!"); + throw new Error('Tried starting systemd-resolved advertisement without initializing port!') } if (!this.bus) { - throw new Error("Tried to start systemd-resolved advertisement on a destroyed advertiser!"); + throw new Error('Tried to start systemd-resolved advertisement on a destroyed advertiser!') } - debug(`Starting to advertise '${this.accessoryInfo.displayName}' using systemd-resolved backend!`); + debug(`Starting to advertise '${this.accessoryInfo.displayName}' using systemd-resolved backend!`) try { - this.path = await ResolvedAdvertiser.managerInvoke(this.bus, "RegisterService", { + this.path = await ResolvedAdvertiser.managerInvoke(this.bus, 'RegisterService', { body: [ this.accessoryInfo.displayName, // name this.accessoryInfo.displayName, // name_template - "_hap._tcp", // type + '_hap._tcp', // type this.port, // service_port 0, // service_priority 0, // service_weight [this.createTxt()], // txt_datas ], - signature: "sssqqqaa{say}", - }); + signature: 'sssqqqaa{say}', + }) } catch (error) { if (error instanceof DBusInvokeError) { if (RESOLVED_PERMISSIONS_ERRORS.includes(error.errorName)) { - error.message = `Permissions issue. See https://homebridge.io/w/mDNS-Options for more info. ${error.message}`; + error.message = `Permissions issue. See https://homebridge.io/w/mDNS-Options for more info. ${error.message}` } } - throw error; + throw error } } public async updateAdvertisement(silent?: boolean): Promise { if (!this.bus) { - throw new Error("Tried to update systemd-resolved advertisement on a destroyed advertiser!"); + throw new Error('Tried to update systemd-resolved advertisement on a destroyed advertiser!') } - debug("Updating txt record (txt: %o, silent: %d)", CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), silent); + debug('Updating txt record (txt: %o, silent: %d)', CiaoAdvertiser.createTxt(this.accessoryInfo, this.setupHash), silent) // Currently, systemd-resolved has no way to update an existing record. - await this.stopAdvertising(); - await this.startAdvertising(); + await this.stopAdvertising() + await this.startAdvertising() } private async stopAdvertising(): Promise { if (!this.bus) { - throw new Error("Tried to destroy systemd-resolved advertisement on a destroyed advertiser!"); + throw new Error('Tried to destroy systemd-resolved advertisement on a destroyed advertiser!') } if (this.path) { try { - await ResolvedAdvertiser.managerInvoke(this.bus, "UnregisterService", { + await ResolvedAdvertiser.managerInvoke(this.bus, 'UnregisterService', { body: [this.path], - signature: "o", - }); + signature: 'o', + }) } catch (error) { // Typically, this fails if e.g. systemd-resolved service was stopped in the meantime. - debug("Destroying systemd-resolved advertisement failed: " + error); + debug(`Destroying systemd-resolved advertisement failed: ${error}`) } - this.path = undefined; + this.path = undefined } } public async destroy(): Promise { if (!this.bus) { - throw new Error("Tried to destroy systemd-resolved advertisement on a destroyed advertiser!"); + throw new Error('Tried to destroy systemd-resolved advertisement on a destroyed advertiser!') } - await this.stopAdvertising(); + await this.stopAdvertising() - this.bus.connection.stream.destroy(); - this.bus = undefined; + this.bus.connection.stream.destroy() + this.bus = undefined } public static async isAvailable(): Promise { - const bus = dbus.systemBus(); + const bus = systemBus() try { try { - await messageBusConnectionResult(bus); + await messageBusConnectionResult(bus) } catch (error) { - debug("systemd-resolved/DBus classified unavailable due to missing dbus interface!"); - return false; + debug('systemd-resolved/DBus classified unavailable due to missing dbus interface!') + return false } try { // Ensure that systemd-resolved is accessible. - await this.managerInvoke(bus, "ResolveHostname", { - body: [0, "127.0.0.1", 0, 0], - signature: "isit", - }); - debug("Detected systemd-resolved over DBus interface running version."); + await this.managerInvoke(bus, 'ResolveHostname', { + body: [0, '127.0.0.1', 0, 0], + signature: 'isit', + }) + debug('Detected systemd-resolved over DBus interface running version.') } catch (error) { - debug("systemd-resolved/DBus classified unavailable due to missing systemd-resolved interface!"); - return false; + debug('systemd-resolved/DBus classified unavailable due to missing systemd-resolved interface!') + return false } try { const mdnsStatus = await this.resolvedInvoke( bus, - "org.freedesktop.DBus.Properties", - "Get", + 'org.freedesktop.DBus.Properties', + 'Get', { - body: ["org.freedesktop.resolve1.Manager", "MulticastDNS"], - signature: "ss", + body: ['org.freedesktop.resolve1.Manager', 'MulticastDNS'], + signature: 'ss', }, - ); + ) - if (mdnsStatus[0][0].type !== "s") { - throw new Error("Invalid type for MulticastDNS"); + if (mdnsStatus[0][0].type !== 's') { + throw new Error('Invalid type for MulticastDNS') } - if (mdnsStatus[1][0] !== "yes" ) { - debug("systemd-resolved/DBus classified unavailable because MulticastDNS is not enabled!"); - return false; + if (mdnsStatus[1][0] !== 'yes') { + debug('systemd-resolved/DBus classified unavailable because MulticastDNS is not enabled!') + return false } } catch (error) { - debug("systemd-resolved/DBus classified unavailable due to failure checking system status: " + error); - return false; + debug(`systemd-resolved/DBus classified unavailable due to failure checking system status: ${error}`) + return false } - return true; + return true } finally { - bus.connection.stream.destroy(); + bus.connection.stream.destroy() } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private static resolvedInvoke(bus: MessageBus, dbusInterface: string, member: string, others?: any): Promise { return dbusInvoke( bus, - "org.freedesktop.resolve1", - "/org/freedesktop/resolve1", + 'org.freedesktop.resolve1', + '/org/freedesktop/resolve1', dbusInterface, member, others, - ); + ) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private static managerInvoke(bus: MessageBus, member: string, others?: any): Promise { - return this.resolvedInvoke(bus, "org.freedesktop.resolve1.Manager", member, others); + return this.resolvedInvoke(bus, 'org.freedesktop.resolve1.Manager', member, others) } } diff --git a/src/lib/Bridge.ts b/src/lib/Bridge.ts index 5e3390006..001ca4f40 100644 --- a/src/lib/Bridge.ts +++ b/src/lib/Bridge.ts @@ -1,4 +1,4 @@ -import { Accessory } from "./Accessory"; +import { Accessory } from './Accessory.js' /** * Bridge is a special type of HomeKit Accessory that hosts other Accessories "behind" it. This way you @@ -9,7 +9,7 @@ import { Accessory } from "./Accessory"; */ export class Bridge extends Accessory { constructor(displayName: string, UUID: string) { - super(displayName, UUID); - this._isBridge = true; + super(displayName, UUID) + this._isBridge = true } } diff --git a/src/lib/Characteristic.spec.ts b/src/lib/Characteristic.spec.ts index 5bf0e5994..a39e2cc4c 100644 --- a/src/lib/Characteristic.spec.ts +++ b/src/lib/Characteristic.spec.ts @@ -1,811 +1,824 @@ -import { CharacteristicWarningType } from "./Accessory"; +import type { CharacteristicChange, CharacteristicProps, SerializedCharacteristic } from './Characteristic' + +import { Buffer } from 'node:buffer' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { CharacteristicWarningType } from './Accessory.js' import { Access, Characteristic, - CharacteristicChange, CharacteristicEventTypes, - CharacteristicProps, Formats, Perms, - SerializedCharacteristic, Units, -} from "./Characteristic"; -import { SelectedRTPStreamConfiguration } from "./definitions"; -import { HAPStatus } from "./HAPServer"; -import { HapStatusError } from "./util/hapStatusError"; -import * as uuid from "./util/uuid"; +} from './Characteristic.js' +import { SelectedRTPStreamConfiguration } from './definitions/index.js' +import { HAPStatus } from './HAPServer.js' +import { HapStatusError } from './util/hapStatusError.js' +import { generate } from './util/uuid.js' function createCharacteristic(type: Formats, customUUID?: string): Characteristic { - return new Characteristic("Test", customUUID || uuid.generate("Foo"), { format: type, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE] }); + return new Characteristic('Test', customUUID || generate('Foo'), { format: type, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE] }) } function createCharacteristicWithProps(props: CharacteristicProps, customUUID?: string): Characteristic { - return new Characteristic("Test", customUUID || uuid.generate("Foo"), props); + return new Characteristic('Test', customUUID || generate('Foo'), props) } -describe("Characteristic", () => { +describe('characteristic', () => { beforeEach(() => { - jest.resetAllMocks(); - }); + vi.resetAllMocks() + }) - describe("#setProps()", () => { - it("should overwrite existing properties", () => { - const characteristic = createCharacteristic(Formats.BOOL); + describe('#setProps()', () => { + it('should overwrite existing properties', () => { + const characteristic = createCharacteristic(Formats.BOOL) - const NEW_PROPS = { format: Formats.STRING, perms: [Perms.NOTIFY] }; - characteristic.setProps(NEW_PROPS); + const NEW_PROPS = { format: Formats.STRING, perms: [Perms.NOTIFY] } + characteristic.setProps(NEW_PROPS) - expect(characteristic.props).toEqual(NEW_PROPS); - }); + expect(characteristic.props).toEqual(NEW_PROPS) + }) - it("should fail when setting invalid value range", () => { - const characteristic = createCharacteristic(Formats.INT); + it('should fail when setting invalid value range', () => { + const characteristic = createCharacteristic(Formats.INT) const setProps = (min: number, max: number) => characteristic.setProps({ minValue: min, maxValue: max, - }); + }) - expect(() => setProps(-256, -512)).toThrow(Error); - expect(() => setProps(0, -3)).toThrow(Error); - expect(() => setProps(6, 0)).toThrow(Error); - expect(() => setProps(678, 234)).toThrow(Error); + expect(() => setProps(-256, -512)).toThrow(Error) + expect(() => setProps(0, -3)).toThrow(Error) + expect(() => setProps(6, 0)).toThrow(Error) + expect(() => setProps(678, 234)).toThrow(Error) // should allow setting equal values - setProps(0, 0); - setProps(3, 3); - }); + setProps(0, 0) + setProps(3, 3) + }) - it("should reject update to minValue and maxValue when they are out of range for format type", () => { + it('should reject update to minValue and maxValue when they are out of range for format type', () => { const characteristic = createCharacteristicWithProps({ format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 255, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: 700, maxValue: 1000, - }); + }) - expect(characteristic.props.minValue).toEqual(0); // min for UINT8 - expect(characteristic.props.maxValue).toEqual(255); // max for UINT8 - expect(mock).toBeCalledTimes(2); + expect(characteristic.props.minValue).toEqual(0) // min for UINT8 + expect(characteristic.props.maxValue).toEqual(255) // max for UINT8 + expect(mock).toBeCalledTimes(2) - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: -1000, maxValue: -500, - }); + }) - expect(characteristic.props.minValue).toEqual(0); // min for UINT8 - expect(characteristic.props.maxValue).toEqual(255); // max for UINT8 - expect(mock).toBeCalledTimes(2); + expect(characteristic.props.minValue).toEqual(0) // min for UINT8 + expect(characteristic.props.maxValue).toEqual(255) // max for UINT8 + expect(mock).toBeCalledTimes(2) - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: 10, maxValue: 1000, - }); + }) - expect(characteristic.props.minValue).toEqual(10); - expect(characteristic.props.maxValue).toEqual(255); // max for UINT8 - expect(mock).toBeCalledTimes(1); - }); + expect(characteristic.props.minValue).toEqual(10) + expect(characteristic.props.maxValue).toEqual(255) // max for UINT8 + expect(mock).toBeCalledTimes(1) + }) - it("should reject update to minValue and maxValue when minValue is greater than maxValue", () => { + it('should reject update to minValue and maxValue when minValue is greater than maxValue', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); + }) expect(() => { characteristic.setProps({ minValue: 1000, maxValue: 500, - }); - }).toThrowError(); + }) + }).toThrowError() - expect(characteristic.props.minValue).toBeUndefined(); - expect(characteristic.props.maxValue).toBeUndefined(); - }); + expect(characteristic.props.minValue).toBeUndefined() + expect(characteristic.props.maxValue).toBeUndefined() + }) - it("should accept update to minValue and maxValue when they are in range for format type", () => { + it('should accept update to minValue and maxValue when they are in range for format type', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 255, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: 10, maxValue: 240, - }); + }) - expect(characteristic.props.minValue).toEqual(10); - expect(characteristic.props.maxValue).toEqual(240); - expect(mock).toBeCalledTimes(0); + expect(characteristic.props.minValue).toEqual(10) + expect(characteristic.props.maxValue).toEqual(240) + expect(mock).toBeCalledTimes(0) - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: -2147483648, maxValue: 2147483647, - }); + }) - expect(characteristic.props.minValue).toEqual(-2147483648); - expect(characteristic.props.maxValue).toEqual(2147483647); - expect(mock).toBeCalledTimes(0); - }); + expect(characteristic.props.minValue).toEqual(-2147483648) + expect(characteristic.props.maxValue).toEqual(2147483647) + expect(mock).toBeCalledTimes(0) + }) - it("should reject non-finite numbers for minValue and maxValue for numeric characteristics", () => { + it('should reject non-finite numbers for minValue and maxValue for numeric characteristics', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); + mock.mockReset() characteristic.setProps({ minValue: Number.NEGATIVE_INFINITY, - }); + }) - expect(characteristic.props.minValue).toEqual(undefined); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith(expect.stringContaining("Property 'minValue' must be a finite number"), expect.anything()); + expect(characteristic.props.minValue).toEqual(undefined) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith(expect.stringContaining('Property \'minValue\' must be a finite number'), expect.anything()) - mock.mockReset(); + mock.mockReset() characteristic.setProps({ maxValue: Number.POSITIVE_INFINITY, - }); + }) - expect(characteristic.props.maxValue).toEqual(undefined); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith(expect.stringContaining("Property 'maxValue' must be a finite number"), expect.anything()); - }); + expect(characteristic.props.maxValue).toEqual(undefined) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith(expect.stringContaining('Property \'maxValue\' must be a finite number'), expect.anything()) + }) - it("should reject NaN numbers for minValue and maxValue for numeric characteristics", () => { + it('should reject NaN numbers for minValue and maxValue for numeric characteristics', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); + mock.mockReset() characteristic.setProps({ - minValue: NaN, - }); + minValue: Number.NaN, + }) - expect(characteristic.props.minValue).toEqual(undefined); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith(expect.stringContaining("Property 'minValue' must be a finite number"), expect.anything()); + expect(characteristic.props.minValue).toEqual(undefined) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith(expect.stringContaining('Property \'minValue\' must be a finite number'), expect.anything()) - mock.mockReset(); + mock.mockReset() characteristic.setProps({ - maxValue: NaN, - }); + maxValue: Number.NaN, + }) - expect(characteristic.props.maxValue).toEqual(undefined); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith(expect.stringContaining("Property 'maxValue' must be a finite number"), expect.anything()); - }); + expect(characteristic.props.maxValue).toEqual(undefined) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith(expect.stringContaining('Property \'maxValue\' must be a finite number'), expect.anything()) + }) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "numeric values of format %p should be corrected on setProps call with min/max restrictions", format => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'numeric values of format %p should be corrected on setProps call with min/max restrictions', + (format) => { const characteristic = createCharacteristicWithProps({ - format: format, + format, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) - const changedMock = jest.fn(); - characteristic.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + characteristic.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(characteristic, "characteristicWarning"); + const warningMock = vi.spyOn(characteristic, 'characteristicWarning') const expectNoChange = () => { - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - }; + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + } const expectChange = () => { - expect(changedMock).toBeCalledTimes(1); - expect(warningMock).toBeCalledTimes(1); - expect(warningMock).toBeCalledWith(expect.anything(), CharacteristicWarningType.DEBUG_MESSAGE); - }; + expect(changedMock).toBeCalledTimes(1) + expect(warningMock).toBeCalledTimes(1) + expect(warningMock).toBeCalledWith(expect.anything(), CharacteristicWarningType.DEBUG_MESSAGE) + } const reset = () => { - changedMock.mockReset(); - warningMock.mockReset(); - }; + changedMock.mockReset() + warningMock.mockReset() + } - reset(); + reset() characteristic.setProps({ minValue: 0, maxValue: 100, - }); + }) // a value of null must not be corrected! - expectNoChange(); + expectNoChange() - characteristic.setValue(0); + characteristic.setValue(0) - reset(); + reset() characteristic.setProps({ minValue: 10, maxValue: 100, - }); + }) // value should be corrected to 10. If changing the min/max value bounds, users should change the value first! - expectChange(); - expect(characteristic.value).toEqual(10); + expectChange() + expect(characteristic.value).toEqual(10) - reset(); + reset() characteristic.setProps({ minValue: null, maxValue: null, - }); + }) // unsetting min/max value restriction must not make a difference - expectNoChange(); + expectNoChange() - reset(); + reset() characteristic.setProps({ validValueRanges: [20, 100], - }); + }) // value should be corrected to 20 - expectChange(); - expect(characteristic.value).toEqual(20); + expectChange() + expect(characteristic.value).toEqual(20) - reset(); + reset() characteristic.setProps({ validValueRanges: null, - }); + }) // unsetting min/max value restriction must not make a difference - expectNoChange(); + expectNoChange() - reset(); + reset() characteristic.setProps({ validValues: [1, 2, 3], - }); + }) // value should be corrected to the first valid value - expectChange(); - expect(characteristic.value).toBe(1); - }); + expectChange() + expect(characteristic.value).toBe(1) + }, + ) - test("string values should be corrected on setProps call with length restrictions", () => { + it('string values should be corrected on setProps call with length restrictions', () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) - const changedMock = jest.fn(); - characteristic.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + characteristic.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(characteristic, "characteristicWarning"); + const warningMock = vi.spyOn(characteristic, 'characteristicWarning') const expectNoChange = () => { - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - }; + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + } const expectChange = () => { - expect(changedMock).toBeCalledTimes(1); - expect(warningMock).toBeCalledTimes(1); - expect(warningMock).toBeCalledWith(expect.anything(), CharacteristicWarningType.DEBUG_MESSAGE); - }; + expect(changedMock).toBeCalledTimes(1) + expect(warningMock).toBeCalledTimes(1) + expect(warningMock).toBeCalledWith(expect.anything(), CharacteristicWarningType.DEBUG_MESSAGE) + } const reset = () => { - changedMock.mockReset(); - warningMock.mockReset(); - }; + changedMock.mockReset() + warningMock.mockReset() + } - reset(); + reset() characteristic.setProps({ maxLen: 256, - }); + }) - expectNoChange(); - expect(characteristic.value).toBeNull(); + expectNoChange() + expect(characteristic.value).toBeNull() - characteristic.setValue("Hello World"); + characteristic.setValue('Hello World') - reset(); + reset() characteristic.setProps({ maxLen: 5, - }); + }) - expectChange(); - expect(characteristic.value).toBe("Hello"); // TODO string length cutting should happen on read only? + expectChange() + expect(characteristic.value).toBe('Hello') // TODO string length cutting should happen on read only? - reset(); + reset() characteristic.setProps({ maxLen: null, - }); + }) - expectNoChange(); - }); + expectNoChange() + }) - test("setProps call should not remove error state", () => { + it('setProps call should not remove error state', () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) - const changedMock = jest.fn(); - characteristic.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + characteristic.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(characteristic, "characteristicWarning"); + const warningMock = vi.spyOn(characteristic, 'characteristicWarning') - characteristic.setValue("Hello World"); - characteristic.updateValue(new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE)); + characteristic.setValue('Hello World') + characteristic.updateValue(new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE)) - changedMock.mockReset(); - warningMock.mockReset(); + changedMock.mockReset() + warningMock.mockReset() characteristic.setProps({ maxLen: 5, - }); + }) - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - }); + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + }) - test("setProps should not try to correct value if impossible", () => { + it('setProps should not try to correct value if impossible', () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) - const changedMock = jest.fn(); - characteristic.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + characteristic.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(characteristic, "characteristicWarning"); + const warningMock = vi.spyOn(characteristic, 'characteristicWarning') - characteristic.setValue("Hello World"); + characteristic.setValue('Hello World') - changedMock.mockReset(); - warningMock.mockReset(); + changedMock.mockReset() + warningMock.mockReset() characteristic.setProps({ format: Formats.INT, - }); + }) - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - expect(characteristic.value).toEqual("Hello World"); + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + expect(characteristic.value).toEqual('Hello World') // when changing the format, users must change the value after changing setProps! - }); + }) - test("setProps must not emit a change event for event-type characteristics: ProgrammableSwitchEvent", () => { - const switchEvent = new Characteristic.ProgrammableSwitchEvent(); + it('setProps must not emit a change event for event-type characteristics: ProgrammableSwitchEvent', () => { + const switchEvent = new Characteristic.ProgrammableSwitchEvent() - const changedMock = jest.fn(); - switchEvent.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + switchEvent.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(switchEvent, "characteristicWarning"); + const warningMock = vi.spyOn(switchEvent, 'characteristicWarning') - switchEvent.updateValue(Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS); - changedMock.mockReset(); - warningMock.mockReset(); + switchEvent.updateValue(Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS) + changedMock.mockReset() + warningMock.mockReset() switchEvent.setProps({ validValues: [0, 2], - }); + }) - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - expect(switchEvent.value).toEqual(1); - }); + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + expect(switchEvent.value).toEqual(1) + }) - test("setProps must not emit a change event for event-type characteristics: ButtonEvent", () => { - const buttonEvent = new Characteristic.ButtonEvent(); + it('setProps must not emit a change event for event-type characteristics: ButtonEvent', () => { + const buttonEvent = new Characteristic.ButtonEvent() - const changedMock = jest.fn(); - buttonEvent.on(CharacteristicEventTypes.CHANGE, changedMock); + const changedMock = vi.fn() + buttonEvent.on(CharacteristicEventTypes.CHANGE, changedMock) // @ts-expect-error: spying on private property - const warningMock = jest.spyOn(buttonEvent, "characteristicWarning"); + const warningMock = vi.spyOn(buttonEvent, 'characteristicWarning') - buttonEvent.updateValue("0000"); // some empty tlv - changedMock.mockReset(); - warningMock.mockReset(); + buttonEvent.updateValue('0000') // some empty tlv + changedMock.mockReset() + warningMock.mockReset() // we actually don't modify anything buttonEvent.setProps({ - }); + }) - expect(changedMock).toBeCalledTimes(0); - expect(warningMock).toBeCalledTimes(0); - expect(buttonEvent.value).toEqual("0000"); - }); - }); + expect(changedMock).toBeCalledTimes(0) + expect(warningMock).toBeCalledTimes(0) + expect(buttonEvent.value).toEqual('0000') + }) + }) - describe("validValuesIterator", () => { - it ("should iterate over min/max value definition", () => { + describe('validValuesIterator', () => { + it ('should iterate over min/max value definition', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.PAIRED_READ], minValue: 2, maxValue: 5, - }); + }) - const result = Array.from(characteristic.validValuesIterator()); - expect(result).toEqual([2, 3, 4, 5]); - }); + const result = Array.from(characteristic.validValuesIterator()) + expect(result).toEqual([2, 3, 4, 5]) + }) - it ("should iterate over min/max value definition with minStep defined", () => { + it ('should iterate over min/max value definition with minStep defined', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.PAIRED_READ], minValue: 2, maxValue: 10, minStep: 2, // can't really test with .x precision as of floating point precision - }); + }) - const result = Array.from(characteristic.validValuesIterator()); - expect(result).toEqual([2, 4, 6, 8, 10]); - }); + const result = Array.from(characteristic.validValuesIterator()) + expect(result).toEqual([2, 4, 6, 8, 10]) + }) - it ("should iterate over validValues array definition", () => { - const validValues = [1, 3, 4, 5, 8]; + it ('should iterate over validValues array definition', () => { + const validValues = [1, 3, 4, 5, 8] const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.PAIRED_READ], - validValues: validValues, - }); + validValues, + }) - const result = Array.from(characteristic.validValuesIterator()); - expect(result).toEqual(validValues); - }); + const result = Array.from(characteristic.validValuesIterator()) + expect(result).toEqual(validValues) + }) - it ("should iterate over validValueRanges definition", () => { + it ('should iterate over validValueRanges definition', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.PAIRED_READ], validValueRanges: [2, 5], - }); + }) - const result = Array.from(characteristic.validValuesIterator()); - expect(result).toEqual([2, 3, 4, 5]); - }); + const result = Array.from(characteristic.validValuesIterator()) + expect(result).toEqual([2, 3, 4, 5]) + }) - it("should iterate over UINT8 definition", () => { - const characteristic = createCharacteristic(Formats.UINT8); + it('should iterate over UINT8 definition', () => { + const characteristic = createCharacteristic(Formats.UINT8) - const result = Array.from(characteristic.validValuesIterator()); - expect(result).toEqual(Array.from(new Uint8Array(256).map((value, i) => i))); - }); + const result = Array.from(characteristic.validValuesIterator()) + expect(result).toEqual(Array.from(new Uint8Array(256).map((value, i) => i))) + }) - // we could do the same for UINT16, UINT32 and UINT64 but i think thats kind of pointless and takes to long - }); + // We could do the same for UINT16, UINT32 and UINT64, but I think that's kind of pointless and takes to long + }) - describe("#subscribe()", () => { - it("correctly adds a single subscription", () => { - const characteristic = createCharacteristic(Formats.BOOL); - const subscribeSpy = jest.fn(); - characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy); - characteristic.subscribe(); + describe('#subscribe()', () => { + it('correctly adds a single subscription', () => { + const characteristic = createCharacteristic(Formats.BOOL) + const subscribeSpy = vi.fn() + characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy) + characteristic.subscribe() - expect(subscribeSpy).toHaveBeenCalledTimes(1); + expect(subscribeSpy).toHaveBeenCalledTimes(1) // @ts-expect-error: private access - expect(characteristic.subscriptions).toEqual(1); - }); - - it("correctly adds multiple subscriptions", () => { - const characteristic = createCharacteristic(Formats.BOOL); - const subscribeSpy = jest.fn(); - characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy); - characteristic.subscribe(); - characteristic.subscribe(); - characteristic.subscribe(); - - expect(subscribeSpy).toHaveBeenCalledTimes(1); + expect(characteristic.subscriptions).toEqual(1) + }) + + it('correctly adds multiple subscriptions', () => { + const characteristic = createCharacteristic(Formats.BOOL) + const subscribeSpy = vi.fn() + characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy) + characteristic.subscribe() + characteristic.subscribe() + characteristic.subscribe() + + expect(subscribeSpy).toHaveBeenCalledTimes(1) // @ts-expect-error: private access - expect(characteristic.subscriptions).toEqual(3); - }); - }); - - describe("#unsubscribe()", () => { - it("correctly removes a single subscription", () => { - const characteristic = createCharacteristic(Formats.BOOL); - const subscribeSpy = jest.fn(); - const unsubscribeSpy = jest.fn(); - characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy); - characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, unsubscribeSpy); - characteristic.subscribe(); - characteristic.unsubscribe(); - - expect(subscribeSpy).toHaveBeenCalledTimes(1); - expect(unsubscribeSpy).toHaveBeenCalledTimes(1); + expect(characteristic.subscriptions).toEqual(3) + }) + }) + + describe('#unsubscribe()', () => { + it('correctly removes a single subscription', () => { + const characteristic = createCharacteristic(Formats.BOOL) + const subscribeSpy = vi.fn() + const unsubscribeSpy = vi.fn() + characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy) + characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, unsubscribeSpy) + characteristic.subscribe() + characteristic.unsubscribe() + + expect(subscribeSpy).toHaveBeenCalledTimes(1) + expect(unsubscribeSpy).toHaveBeenCalledTimes(1) // @ts-expect-error: private access - expect(characteristic.subscriptions).toEqual(0); - }); - - it("correctly removes multiple subscriptions", () => { - const characteristic = createCharacteristic(Formats.BOOL); - const subscribeSpy = jest.fn(); - const unsubscribeSpy = jest.fn(); - characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy); - characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, unsubscribeSpy); - characteristic.subscribe(); - characteristic.subscribe(); - characteristic.subscribe(); - characteristic.unsubscribe(); - characteristic.unsubscribe(); - characteristic.unsubscribe(); - - expect(subscribeSpy).toHaveBeenCalledTimes(1); - expect(unsubscribeSpy).toHaveBeenCalledTimes(1); + expect(characteristic.subscriptions).toEqual(0) + }) + + it('correctly removes multiple subscriptions', () => { + const characteristic = createCharacteristic(Formats.BOOL) + const subscribeSpy = vi.fn() + const unsubscribeSpy = vi.fn() + characteristic.on(CharacteristicEventTypes.SUBSCRIBE, subscribeSpy) + characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, unsubscribeSpy) + characteristic.subscribe() + characteristic.subscribe() + characteristic.subscribe() + characteristic.unsubscribe() + characteristic.unsubscribe() + characteristic.unsubscribe() + + expect(subscribeSpy).toHaveBeenCalledTimes(1) + expect(unsubscribeSpy).toHaveBeenCalledTimes(1) // @ts-expect-error: private access - expect(characteristic.subscriptions).toEqual(0); - }); - }); - - describe("#handleGetRequest()", () => { - it("should handle special event only characteristics", (callback) => { - const characteristic = createCharacteristic(Formats.BOOL, Characteristic.ProgrammableSwitchEvent.UUID); - - characteristic.handleGetRequest().then(() => { - expect(characteristic.statusCode).toEqual(HAPStatus.SUCCESS); - expect(characteristic.value).toEqual(null); - callback(); - }); - }); - - it("should return cached values if no listeners are registered", (callback) => { - const characteristic = createCharacteristic(Formats.BOOL); - - characteristic.handleGetRequest().then(() => { - expect(characteristic.statusCode).toEqual(HAPStatus.SUCCESS); - expect(characteristic.value).toEqual(null); - callback(); - }); - }); - }); - - describe("#validateClientSuppliedValue()", () => { - it("rejects undefined values from client", async () => { + expect(characteristic.subscriptions).toEqual(0) + }) + }) + + describe('#handleGetRequest()', () => { + it('should handle special event only characteristics', async () => { + const characteristic = createCharacteristic(Formats.BOOL, Characteristic.ProgrammableSwitchEvent.UUID) + + await characteristic.handleGetRequest() + expect(characteristic.statusCode).toEqual(HAPStatus.SUCCESS) + expect(characteristic.value).toEqual(null) + }) + + it('should return cached values if no listeners are registered', async () => { + const characteristic = createCharacteristic(Formats.BOOL) + + await characteristic.handleGetRequest() + expect(characteristic.statusCode).toEqual(HAPStatus.SUCCESS) + expect(characteristic.value).toEqual(null) + }) + }) + + describe('#validateClientSuppliedValue()', () => { + it('rejects undefined values from client', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.UINT8, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error await expect(characteristic.handleSetRequest(undefined as unknown as boolean, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalled(); - }); + expect(validateClientSuppliedValueMock).toBeCalled() + }) - it("rejects invalid values for the boolean format type", async () => { + it('rejects invalid values for the boolean format type', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.BOOL, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(true); + characteristic.setValue(true) // numbers other than 1 or 0 should throw an error await expect(characteristic.handleSetRequest(20, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // strings should throw an error - await expect(characteristic.handleSetRequest("true", null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + await expect(characteristic.handleSetRequest('true', null as unknown as undefined)) + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual(true); + expect(characteristic.value).toEqual(true) // 0 should set the value to false await expect(characteristic.handleSetRequest(0, null as unknown as undefined)) - .resolves.toEqual(undefined); - expect(characteristic.value).toEqual(false); + .resolves.toEqual(undefined) + expect(characteristic.value).toEqual(false) // 1 should set the value to true await expect(characteristic.handleSetRequest(1, null as unknown as undefined)) - .resolves.toEqual(undefined); - expect(characteristic.value).toEqual(true); + .resolves.toEqual(undefined) + expect(characteristic.value).toEqual(true) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(4); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(4) + }) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "boolean types sent for %p types should be transformed from false to 0", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'boolean types sent for %p types should be transformed from false to 0', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') - await characteristic.handleSetRequest(false, null as unknown as undefined); - expect(characteristic.value).toEqual(0); + await characteristic.handleSetRequest(false, null as unknown as undefined) + expect(characteristic.value).toEqual(0) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalled(); - }); - + expect(validateClientSuppliedValueMock).toBeCalled() + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "boolean types sent for %p types should be transformed from true to 1", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'boolean types sent for %p types should be transformed from true to 1', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') - await characteristic.handleSetRequest(true, null as unknown as undefined); - expect(characteristic.value).toEqual(1); + await characteristic.handleSetRequest(true, null as unknown as undefined) + expect(characteristic.value).toEqual(1) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalled(); - }); + expect(validateClientSuppliedValueMock).toBeCalled() + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "rejects string values sent for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'rejects string values sent for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error - await expect(characteristic.handleSetRequest("what is this!", null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + await expect(characteristic.handleSetRequest('what is this!', null as unknown as undefined)) + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalled(); - }); + expect(validateClientSuppliedValueMock).toBeCalled() + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "ensure maxValue is not exceeded for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'ensure maxValue is not exceeded for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error await expect(characteristic.handleSetRequest(100, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // this should throw an error await expect(characteristic.handleSetRequest(-100, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // value should revert to - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // this should pass await expect(characteristic.handleSetRequest(0, null as unknown as undefined)) - .resolves.toEqual(undefined); + .resolves.toEqual(undefined) // value should now be 3 - expect(characteristic.value).toEqual(0); + expect(characteristic.value).toEqual(0) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(3); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(3) + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "ensure NaN is rejected for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'ensure NaN is rejected for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1, minValue: 0, minStep: 1, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error - await expect(characteristic.handleSetRequest(NaN, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + await expect(characteristic.handleSetRequest(Number.NaN, null as unknown as undefined)) + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // value should revert to - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(1); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(1) + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "ensure non-finite values are rejected for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'ensure non-finite values are rejected for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error await expect(characteristic.handleSetRequest(Infinity, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // value should revert to - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(1); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(1) + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "ensure value is rejected if outside valid values for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'ensure value is rejected if outside valid values for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 10, @@ -813,34 +826,36 @@ describe("Characteristic", () => { minStep: 1, validValues: [1, 3, 5, 10], perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(1); + characteristic.setValue(1) // this should throw an error await expect(characteristic.handleSetRequest(6, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // value should revert to - expect(characteristic.value).toEqual(1); + expect(characteristic.value).toEqual(1) // this should pass await expect(characteristic.handleSetRequest(3, null as unknown as undefined)) - .resolves.toEqual(undefined); + .resolves.toEqual(undefined) // value should now be 3 - expect(characteristic.value).toEqual(3); + expect(characteristic.value).toEqual(3) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(2); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(2) + }, + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "ensure value is rejected if outside valid value ranges for %p types sent from client", async (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'ensure value is rejected if outside valid value ranges for %p types sent from client', + async (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, maxValue: 1000, @@ -848,1207 +863,1198 @@ describe("Characteristic", () => { minStep: 1, validValueRanges: [50, 55], perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(50); + characteristic.setValue(50) // this should throw an error await expect(characteristic.handleSetRequest(100, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // this should throw an error await expect(characteristic.handleSetRequest(20, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // value should still be 50 - expect(characteristic.value).toEqual(50); + expect(characteristic.value).toEqual(50) // this should pass await expect(characteristic.handleSetRequest(52, null as unknown as undefined)) - .resolves.toEqual(undefined); + .resolves.toEqual(undefined) // value should now be 52 - expect(characteristic.value).toEqual(52); + expect(characteristic.value).toEqual(52) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(3); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(3) + }, + ) - test.each([Formats.STRING, Formats.TLV8, Formats.DATA])( - "rejects non-string values for the %p format type from the client", async (stringType) => { + it.each([Formats.STRING, Formats.TLV8, Formats.DATA])( + 'rejects non-string values for the %p format type from the client', + async (stringType) => { const characteristic = createCharacteristicWithProps({ format: stringType, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue("some string"); + characteristic.setValue('some string') // numbers should throw an error await expect(characteristic.handleSetRequest(1234, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // booleans should throw an error await expect(characteristic.handleSetRequest(false, null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual("some string"); + expect(characteristic.value).toEqual('some string') // strings should pass - await expect(characteristic.handleSetRequest("some other test string", null as unknown as undefined)) - .resolves.toEqual(undefined); + await expect(characteristic.handleSetRequest('some other test string', null as unknown as undefined)) + .resolves.toEqual(undefined) // value should now be updated - expect(characteristic.value).toEqual("some other test string"); + expect(characteristic.value).toEqual('some other test string') // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(3); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(3) + }, + ) - it("should accept Formats.FLOAT with precision provided by client", async () => { + it('should accept Formats.FLOAT with precision provided by client', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue(0.0005); + characteristic.setValue(0.0005) // the existing valid value should remain - expect(characteristic.value).toEqual(0.0005); + expect(characteristic.value).toEqual(0.0005) // should allow float await expect(characteristic.handleSetRequest(0.0001005, null as unknown as undefined)) - .resolves.toEqual(undefined); + .resolves.toEqual(undefined) // value should now be updated - expect(characteristic.value).toEqual(0.0001005); + expect(characteristic.value).toEqual(0.0001005) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(1); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(1) + }) - it("should accept negative floats in range for Formats.FLOAT provided by the client", async () => { + it('should accept negative floats in range for Formats.FLOAT provided by the client', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: -1000, maxValue: 1000, - }); + }) // @ts-expect-error - spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // should allow negative float await expect(characteristic.handleSetRequest(-0.013, null as unknown as undefined)) - .resolves.toEqual(undefined); + .resolves.toEqual(undefined) // value should now be updated - expect(characteristic.value).toEqual(-0.013); + expect(characteristic.value).toEqual(-0.013) // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(1); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(1) + }) - it("rejects string values exceeding the max length from the client", async () => { + it('rejects string values exceeding the max length from the client', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, maxLen: 5, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue("abcde"); + characteristic.setValue('abcde') - // should reject strings that are to long - await expect(characteristic.handleSetRequest("this is to long", null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + // should reject strings that are too long + await expect(characteristic.handleSetRequest('this is to long', null as unknown as undefined)) + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual("abcde"); + expect(characteristic.value).toEqual('abcde') // strings should pass - await expect(characteristic.handleSetRequest("abc", null as unknown as undefined)) - .resolves.toEqual(undefined); + await expect(characteristic.handleSetRequest('abc', null as unknown as undefined)) + .resolves.toEqual(undefined) // value should now be updated - expect(characteristic.value).toEqual("abc"); + expect(characteristic.value).toEqual('abc') // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(2); - }); + expect(validateClientSuppliedValueMock).toBeCalledTimes(2) + }) - it("rejects data values exceeding the max length from the client", async () => { + it('rejects data values exceeding the max length from the client', async () => { const characteristic = createCharacteristicWithProps({ format: Formats.DATA, maxDataLen: 5, perms: [Perms.EVENTS, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const validateClientSuppliedValueMock = jest.spyOn(characteristic, "validateClientSuppliedValue"); + const validateClientSuppliedValueMock = vi.spyOn(characteristic, 'validateClientSuppliedValue') // set initial known good value - characteristic.setValue("abcde"); + characteristic.setValue('abcde') - // should reject strings that are to long - await expect(characteristic.handleSetRequest("this is to long", null as unknown as undefined)) - .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST); + // should reject strings that are too long + await expect(characteristic.handleSetRequest('this is to long', null as unknown as undefined)) + .rejects.toEqual(HAPStatus.INVALID_VALUE_IN_REQUEST) // the existing valid value should remain - expect(characteristic.value).toEqual("abcde"); + expect(characteristic.value).toEqual('abcde') // strings should pass - await expect(characteristic.handleSetRequest("abc", null as unknown as undefined)) - .resolves.toEqual(undefined); + await expect(characteristic.handleSetRequest('abc', null as unknown as undefined)) + .resolves.toEqual(undefined) // value should now be updated - expect(characteristic.value).toEqual("abc"); + expect(characteristic.value).toEqual('abc') // ensure validator was actually called - expect(validateClientSuppliedValueMock).toBeCalledTimes(2); - }); - - }); - - describe("#validateUserInput()", () => { - - it("should validate an integer property", () => { - const VALUE = 1024; - const characteristic = createCharacteristic(Formats.INT); + expect(validateClientSuppliedValueMock).toBeCalledTimes(2) + }) + }) + + describe('#validateUserInput()', () => { + it('should validate an integer property', () => { + const VALUE = 1024 + const characteristic = createCharacteristic(Formats.INT) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a float property", () => { - const VALUE = 1.024; + it('should validate a float property', () => { + const VALUE = 1.024 const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, minStep: 0.001, minValue: 0, perms: [Perms.NOTIFY], - }); + }) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a UINT8 property", () => { - const VALUE = 10; - const characteristic = createCharacteristic(Formats.UINT8); + it('should validate a UINT8 property', () => { + const VALUE = 10 + const characteristic = createCharacteristic(Formats.UINT8) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a UINT16 property", () => { - const VALUE = 10; - const characteristic = createCharacteristic(Formats.UINT16); + it('should validate a UINT16 property', () => { + const VALUE = 10 + const characteristic = createCharacteristic(Formats.UINT16) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a UINT32 property", () => { - const VALUE = 10; - const characteristic = createCharacteristic(Formats.UINT32); + it('should validate a UINT32 property', () => { + const VALUE = 10 + const characteristic = createCharacteristic(Formats.UINT32) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a UINT64 property", () => { - const VALUE = 10; - const characteristic = createCharacteristic(Formats.UINT64); + it('should validate a UINT64 property', () => { + const VALUE = 10 + const characteristic = createCharacteristic(Formats.UINT64) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a boolean property", () => { - const VALUE = true; - const characteristic = createCharacteristic(Formats.BOOL); + it('should validate a boolean property', () => { + const VALUE = true + const characteristic = createCharacteristic(Formats.BOOL) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a string property", () => { - const VALUE = "Test"; - const characteristic = createCharacteristic(Formats.STRING); + it('should validate a string property', () => { + const VALUE = 'Test' + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a data property", () => { - const VALUE = Buffer.from("Hello my good friend. Have a nice day!", "ascii").toString("base64"); - const characteristic = createCharacteristic(Formats.DATA); + it('should validate a data property', () => { + const VALUE = Buffer.from('Hello my good friend. Have a nice day!', 'ascii').toString('base64') + const characteristic = createCharacteristic(Formats.DATA) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate a TLV8 property", () => { - const VALUE = ""; - const characteristic = createCharacteristic(Formats.TLV8); + it('should validate a TLV8 property', () => { + const VALUE = '' + const characteristic = createCharacteristic(Formats.TLV8) // @ts-expect-error: private access - expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE); - }); + expect(characteristic.validateUserInput(VALUE)).toEqual(VALUE) + }) - it("should validate boolean inputs", () => { + it('should validate boolean inputs', () => { const characteristic = createCharacteristicWithProps({ format: Formats.BOOL, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - characteristic.setValue(true); - expect(characteristic.value).toEqual(true); + characteristic.setValue(true) + expect(characteristic.value).toEqual(true) - characteristic.setValue(false); - expect(characteristic.value).toEqual(false); + characteristic.setValue(false) + expect(characteristic.value).toEqual(false) - characteristic.setValue(1); - expect(characteristic.value).toEqual(true); + characteristic.setValue(1) + expect(characteristic.value).toEqual(true) - characteristic.setValue(0); - expect(characteristic.value).toEqual(false); + characteristic.setValue(0) + expect(characteristic.value).toEqual(false) - characteristic.setValue("1"); - expect(characteristic.value).toEqual(true); + characteristic.setValue('1') + expect(characteristic.value).toEqual(true) - characteristic.setValue("true"); - expect(characteristic.value).toEqual(true); + characteristic.setValue('true') + expect(characteristic.value).toEqual(true) - characteristic.setValue("0"); - expect(characteristic.value).toEqual(false); + characteristic.setValue('0') + expect(characteristic.value).toEqual(false) - characteristic.setValue("false"); - expect(characteristic.value).toEqual(false); + characteristic.setValue('false') + expect(characteristic.value).toEqual(false) - characteristic.setValue({ some: "object" }); - expect(characteristic.value).toEqual(false); - expect(mock).toBeCalledTimes(1); - }); + characteristic.setValue({ some: 'object' }) + expect(characteristic.value).toEqual(false) + expect(mock).toBeCalledTimes(1) + }) - it("should validate boolean inputs when value is undefined", () => { + it('should validate boolean inputs when value is undefined', () => { const characteristic = createCharacteristicWithProps({ format: Formats.BOOL, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - characteristic.setValue(undefined as unknown as boolean); - expect(characteristic.value).toEqual(false); - expect(mock).toBeCalledTimes(1); - }); + characteristic.setValue(undefined as unknown as boolean) + expect(characteristic.value).toEqual(false) + expect(mock).toBeCalledTimes(1) + }) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "should validate %p inputs", (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'should validate %p inputs', + (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 100, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - characteristic.setValue(1); - expect(characteristic.value).toEqual(1); + characteristic.setValue(1) + expect(characteristic.value).toEqual(1) // round to nearest valid value, trigger warning - mock.mockReset(); - characteristic.setValue(-100); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(-100) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(1) // round to nearest valid value, trigger warning - mock.mockReset(); - characteristic.setValue(200); - expect(characteristic.value).toEqual(100); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(200) + expect(characteristic.value).toEqual(100) + expect(mock).toBeCalledTimes(1) // parse string - mock.mockReset(); - characteristic.setValue("50"); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue('50') + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(0) // handle NaN from non-numeric string, restore last known value, trigger warning - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue("SOME STRING"); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith("characteristic value expected valid finite number and received \"NaN\" (number)", "warn-message"); + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue('SOME STRING') + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith('characteristic value expected valid finite number and received "NaN" (number)', 'warn-message') // handle NaN: number from number value - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue(NaN); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(1); - expect(mock).toBeCalledWith("characteristic value expected valid finite number and received \"NaN\" (number)", "warn-message"); + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue(Number.NaN) + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(1) + expect(mock).toBeCalledWith('characteristic value expected valid finite number and received "NaN" (number)', 'warn-message') // handle object, restore last known value, trigger warning - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue({ some: "object" }); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue({ some: 'object' }) + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(1) // handle boolean - true -> 1 - mock.mockReset(); - characteristic.setValue(true); - expect(characteristic.value).toEqual(1); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue(true) + expect(characteristic.value).toEqual(1) + expect(mock).toBeCalledTimes(0) // handle boolean - false -> 0 - mock.mockReset(); - characteristic.setValue(false); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue(false) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(0) }, - ); + ) - test.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "should validate %p inputs when value is undefined", (intType) => { + it.each([Formats.INT, Formats.FLOAT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'should validate %p inputs when value is undefined', + (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 100, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // undefined values should be set to the minValue if not yet set - mock.mockReset(); - characteristic.setValue(undefined as unknown as boolean); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(undefined as unknown as boolean) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(1) // undefined values should be set to the existing value if set - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue(undefined as unknown as boolean); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue(undefined as unknown as boolean) + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(1) }, - ); + ) - test.each([Formats.INT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( - "should round when a float is provided for %p inputs", (intType) => { + it.each([Formats.INT, Formats.UINT8, Formats.UINT16, Formats.UINT32, Formats.UINT64])( + 'should round when a float is provided for %p inputs', + (intType) => { const characteristic = createCharacteristicWithProps({ format: intType, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 100, - }); + }) - characteristic.setValue(99.5); - expect(characteristic.value).toEqual(100); + characteristic.setValue(99.5) + expect(characteristic.value).toEqual(100) - characteristic.setValue(0.1); - expect(characteristic.value).toEqual(0); + characteristic.setValue(0.1) + expect(characteristic.value).toEqual(0) }, - ); + ) - it("should not round floats for Formats.FLOAT", () => { + it('should not round floats for Formats.FLOAT', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 100, - }); + }) - characteristic.setValue(99.5); - expect(characteristic.value).toEqual(99.5); + characteristic.setValue(99.5) + expect(characteristic.value).toEqual(99.5) - characteristic.setValue(0.1); - expect(characteristic.value).toEqual(0.1); - }); + characteristic.setValue(0.1) + expect(characteristic.value).toEqual(0.1) + }) - it("should accept Formats.FLOAT with non-defined min/max value", () => { + it('should accept Formats.FLOAT with non-defined min/max value', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, minStep: 0.01, perms: [Perms.PAIRED_READ, Perms.NOTIFY], - }, uuid.generate("051")); + }, generate('051')) // @ts-expect-error - spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); - characteristic.updateValue(0.09); - expect(characteristic.value).toEqual(0.09); - expect(mock).toBeCalledTimes(0); - }); + mock.mockReset() + characteristic.updateValue(0.09) + expect(characteristic.value).toEqual(0.09) + expect(mock).toBeCalledTimes(0) + }) - it("should validate Formats.FLOAT with precision", () => { - const characteristic = new Characteristic.CurrentAmbientLightLevel(); + it('should validate Formats.FLOAT with precision', () => { + const characteristic = new Characteristic.CurrentAmbientLightLevel() // @ts-expect-error - spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); - - mock.mockReset(); - characteristic.setValue(0); - expect(characteristic.value).toEqual(0.0001); - expect(mock).toBeCalledTimes(1); - - mock.mockReset(); - characteristic.setValue(0.0001); - expect(characteristic.value).toEqual(0.0001); - expect(mock).toBeCalledTimes(0); - - mock.mockReset(); - characteristic.setValue("0.0001"); - expect(characteristic.value).toEqual(0.0001); - expect(mock).toBeCalledTimes(0); - - mock.mockReset(); - characteristic.setValue(100000.00000001); - expect(characteristic.value).toEqual(100000); - expect(mock).toBeCalledTimes(1); - - mock.mockReset(); - characteristic.setValue(100000); - expect(characteristic.value).toEqual(100000); - expect(mock).toBeCalledTimes(0); - }); - - it("should validate Formats.FLOAT with precision with minimum steps", () => { - const characteristic = createCharacteristic(Formats.FLOAT); - let minStep; - - minStep = 100 / 6; + const mock = vi.spyOn(characteristic, 'characteristicWarning') + + mock.mockReset() + characteristic.setValue(0) + expect(characteristic.value).toEqual(0.0001) + expect(mock).toBeCalledTimes(1) + + mock.mockReset() + characteristic.setValue(0.0001) + expect(characteristic.value).toEqual(0.0001) + expect(mock).toBeCalledTimes(0) + + mock.mockReset() + characteristic.setValue('0.0001') + expect(characteristic.value).toEqual(0.0001) + expect(mock).toBeCalledTimes(0) + + mock.mockReset() + characteristic.setValue(100000.00000001) + expect(characteristic.value).toEqual(100000) + expect(mock).toBeCalledTimes(1) + + mock.mockReset() + characteristic.setValue(100000) + expect(characteristic.value).toEqual(100000) + expect(mock).toBeCalledTimes(0) + }) + + it('should validate Formats.FLOAT with precision with minimum steps', () => { + const characteristic = createCharacteristic(Formats.FLOAT) + let minStep + + minStep = 100 / 6 characteristic.setProps({ minValue: 0, maxValue: 100, - minStep: minStep, - }); - for(let i = 1; i <= 7; i++) { - const desiredValue = Math.min(Math.max(i * minStep, 0), 100); - characteristic.setValue(i * minStep); - expect(characteristic.value).toEqual(desiredValue); + minStep, + }) + for (let i = 1; i <= 7; i++) { + const desiredValue = Math.min(Math.max(i * minStep, 0), 100) + characteristic.setValue(i * minStep) + expect(characteristic.value).toEqual(desiredValue) } - minStep = 1; + minStep = 1 characteristic.setProps({ minValue: 0.5, maxValue: 2.5, - minStep: minStep, - }); - for(let i = 1; i <= 4; i++) { - const desiredValue = Math.min(Math.max(i * minStep + 0.5, 0.5), 2.5); - characteristic.setValue(i * minStep + 0.5); - expect(characteristic.value).toEqual(desiredValue); + minStep, + }) + for (let i = 1; i <= 4; i++) { + const desiredValue = Math.min(Math.max(i * minStep + 0.5, 0.5), 2.5) + characteristic.setValue(i * minStep + 0.5) + expect(characteristic.value).toEqual(desiredValue) } - minStep = 100 / 3; + minStep = 100 / 3 characteristic.setProps({ minValue: 0, maxValue: 100, - minStep: minStep, - }); - for(let i = 1; i <= 4; i++) { - const desiredValue = Math.min(Math.max(i * minStep, 0), 100); - characteristic.setValue(i * minStep); - expect(characteristic.value).toEqual(desiredValue); + minStep, + }) + for (let i = 1; i <= 4; i++) { + const desiredValue = Math.min(Math.max(i * minStep, 0), 100) + characteristic.setValue(i * minStep) + expect(characteristic.value).toEqual(desiredValue) } - }); + }) - it("should allow negative floats in range for Formats.FLOAT", () => { + it('should allow negative floats in range for Formats.FLOAT', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: -1000, maxValue: 1000, - }); + }) // @ts-expect-error - spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); - characteristic.setValue(-0.013); - expect(characteristic.value).toEqual(-0.013); - expect(mock).toBeCalledTimes(0); - }); + mock.mockReset() + characteristic.setValue(-0.013) + expect(characteristic.value).toEqual(-0.013) + expect(mock).toBeCalledTimes(0) + }) - it("should not allow non-finite floats in range for Formats.FLOAT", () => { + it('should not allow non-finite floats in range for Formats.FLOAT', () => { const characteristic = createCharacteristicWithProps({ format: Formats.FLOAT, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error - spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') - mock.mockReset(); - characteristic.setValue(Infinity); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(Infinity) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(1) - mock.mockReset(); - characteristic.setValue(Number.POSITIVE_INFINITY); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(Number.POSITIVE_INFINITY) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(1) - mock.mockReset(); - characteristic.setValue(Number.NEGATIVE_INFINITY); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(1); - }); + mock.mockReset() + characteristic.setValue(Number.NEGATIVE_INFINITY) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(1) + }) - it("should validate string inputs", () => { + it('should validate string inputs', () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], maxLen: 15, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // valid string - mock.mockReset(); - characteristic.setValue("ok string"); - expect(characteristic.value).toEqual("ok string"); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue('ok string') + expect(characteristic.value).toEqual('ok string') + expect(mock).toBeCalledTimes(0) // number - convert to string - trigger warning - mock.mockReset(); - characteristic.setValue(12345); - expect(characteristic.value).toEqual("12345"); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(12345) + expect(characteristic.value).toEqual('12345') + expect(mock).toBeCalledTimes(1) // not a string or number, use last known good value and trigger warning - mock.mockReset(); - characteristic.setValue("ok string"); - characteristic.setValue({ ok: "an object" }); - expect(characteristic.value).toEqual("ok string"); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue('ok string') + characteristic.setValue({ ok: 'an object' }) + expect(characteristic.value).toEqual('ok string') + expect(mock).toBeCalledTimes(1) // max length exceeded - mock.mockReset(); - characteristic.setValue("this string exceeds the max length allowed"); - expect(characteristic.value).toEqual("this string exc"); - expect(mock).toBeCalledTimes(1); - }); + mock.mockReset() + characteristic.setValue('this string exceeds the max length allowed') + expect(characteristic.value).toEqual('this string exc') + expect(mock).toBeCalledTimes(1) + }) - it("should validate string inputs when undefined", () => { + it('should validate string inputs when undefined', () => { const characteristic = createCharacteristicWithProps({ format: Formats.STRING, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], maxLen: 15, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // undefined values should be set to "undefined" of no valid value is set yet - mock.mockReset(); - characteristic.setValue(undefined as unknown as boolean); - expect(characteristic.value).toEqual("undefined"); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue(undefined as unknown as boolean) + expect(characteristic.value).toEqual('undefined') + expect(mock).toBeCalledTimes(1) // undefined values should revert back to last known good value if set - mock.mockReset(); - characteristic.setValue("ok string"); - characteristic.setValue(undefined as unknown as boolean); - expect(characteristic.value).toEqual("ok string"); - expect(mock).toBeCalledTimes(1); - }); - - it("should validate data type intputs", () => { + mock.mockReset() + characteristic.setValue('ok string') + characteristic.setValue(undefined as unknown as boolean) + expect(characteristic.value).toEqual('ok string') + expect(mock).toBeCalledTimes(1) + }) + + it('should validate data type inputs', () => { const characteristic = createCharacteristicWithProps({ format: Formats.DATA, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], maxDataLen: 15, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // valid data - mock.mockReset(); - characteristic.setValue("some data"); - expect(characteristic.value).toEqual("some data"); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue('some data') + expect(characteristic.value).toEqual('some data') + expect(mock).toBeCalledTimes(0) // not valid data - mock.mockReset(); - characteristic.setValue({ some: "data" }); - expect(mock).toBeCalledTimes(1); + mock.mockReset() + characteristic.setValue({ some: 'data' }) + expect(mock).toBeCalledTimes(1) // max length exceeded - mock.mockReset(); - characteristic.setValue("this string exceeds the max length allowed"); - expect(mock).toBeCalledTimes(1); - }); + mock.mockReset() + characteristic.setValue('this string exceeds the max length allowed') + expect(mock).toBeCalledTimes(1) + }) - it("should handle null inputs correctly for scalar Apple characteristics", () => { - const characteristic = new Characteristic("CurrentTemperature", Characteristic.CurrentTemperature.UUID, { + it('should handle null inputs correctly for scalar Apple characteristics', () => { + const characteristic = new Characteristic('CurrentTemperature', Characteristic.CurrentTemperature.UUID, { perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], format: Formats.FLOAT, minValue: 0, maxValue: 100, - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // if the initial value is null, validation should set a valid default - mock.mockReset(); - characteristic.setValue(null as unknown as boolean); - expect(characteristic.value).toEqual(0); - expect(mock).toBeCalledTimes(2); + mock.mockReset() + characteristic.setValue(null as unknown as boolean) + expect(characteristic.value).toEqual(0) + expect(mock).toBeCalledTimes(2) // if the value has been previously set, and null is received, the previous value should be returned, - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue(null as unknown as boolean); - expect(characteristic.value).toEqual(50); - expect(mock).toBeCalledTimes(1); - }); - - it("should handle null inputs correctly for scalar non-scalar Apple characteristics", () => { - const characteristicTLV = new SelectedRTPStreamConfiguration(); - const characteristicData = new Characteristic("Data characteristic", Characteristic.SupportedRTPConfiguration.UUID, { + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue(null as unknown as boolean) + expect(characteristic.value).toEqual(50) + expect(mock).toBeCalledTimes(1) + }) + + it('should handle null inputs correctly for scalar non-scalar Apple characteristics', () => { + const characteristicTLV = new SelectedRTPStreamConfiguration() + const characteristicData = new Characteristic('Data characteristic', Characteristic.SupportedRTPConfiguration.UUID, { perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], format: Formats.DATA, - }); + }) - const exampleString = "Example String"; // data and tlv8 are both string based + const exampleString = 'Example String' // data and tlv8 are both string based // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristicTLV, "characteristicWarning"); + const mock = vi.spyOn(characteristicTLV, 'characteristicWarning') // null is a valid value for tlv8 format - mock.mockReset(); - characteristicTLV.setValue(exampleString); - expect(characteristicTLV.value).toEqual(exampleString); - characteristicTLV.setValue(null as unknown as string); - expect(characteristicTLV.value).toEqual(null); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristicTLV.setValue(exampleString) + expect(characteristicTLV.value).toEqual(exampleString) + characteristicTLV.setValue(null as unknown as string) + expect(characteristicTLV.value).toEqual(null) + expect(mock).toBeCalledTimes(0) // null is a valid value for data format - mock.mockReset(); - characteristicData.setValue(exampleString); - expect(characteristicData.value).toEqual(exampleString); - characteristicData.setValue(null as unknown as string); - expect(characteristicData.value).toEqual(null); - expect(mock).toBeCalledTimes(0); - }); - - it("should handle null inputs correctly for non-Apple characteristics", () => { + mock.mockReset() + characteristicData.setValue(exampleString) + expect(characteristicData.value).toEqual(exampleString) + characteristicData.setValue(null as unknown as string) + expect(characteristicData.value).toEqual(null) + expect(mock).toBeCalledTimes(0) + }) + + it('should handle null inputs correctly for non-Apple characteristics', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); + }) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // if the initial value is null, still allow null for non-Apple characteristics - mock.mockReset(); - characteristic.setValue(null as unknown as boolean); - expect(characteristic.value).toEqual(null); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.setValue(null as unknown as boolean) + expect(characteristic.value).toEqual(null) + expect(mock).toBeCalledTimes(0) // if the value has been previously set, and null is received, still allow null for non-Apple characteristics - mock.mockReset(); - characteristic.setValue(50); - characteristic.setValue(null as unknown as boolean); - expect(characteristic.value).toEqual(null); - expect(mock).toBeCalledTimes(0); - }); - }); - - describe("#getDefaultValue()", () => { - - it("should get the correct default value for a boolean property", () => { - const characteristic = createCharacteristic(Formats.BOOL); + mock.mockReset() + characteristic.setValue(50) + characteristic.setValue(null as unknown as boolean) + expect(characteristic.value).toEqual(null) + expect(mock).toBeCalledTimes(0) + }) + }) + + describe('#getDefaultValue()', () => { + it('should get the correct default value for a boolean property', () => { + const characteristic = createCharacteristic(Formats.BOOL) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(false); - }); + expect(characteristic.getDefaultValue()).toEqual(false) + }) - it("should get the correct default value for a string property", () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should get the correct default value for a string property', () => { + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(""); - }); + expect(characteristic.getDefaultValue()).toEqual('') + }) - it("should get the correct default value for a data property", () => { - const characteristic = createCharacteristic(Formats.DATA); + it('should get the correct default value for a data property', () => { + const characteristic = createCharacteristic(Formats.DATA) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(""); - }); + expect(characteristic.getDefaultValue()).toEqual('') + }) - it("should get the correct default value for a TLV8 property", () => { - const characteristic = createCharacteristic(Formats.TLV8); + it('should get the correct default value for a TLV8 property', () => { + const characteristic = createCharacteristic(Formats.TLV8) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(""); - }); + expect(characteristic.getDefaultValue()).toEqual('') + }) - it("should get the correct default value a UINT8 property without minValue", () => { + it('should get the correct default value a UINT8 property without minValue', () => { const characteristic = createCharacteristicWithProps({ format: Formats.UINT8, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(0); - expect(characteristic.value).toEqual(null); // null if never set - }); + expect(characteristic.getDefaultValue()).toEqual(0) + expect(characteristic.value).toEqual(null) // null if never set + }) - it("should get the correct default value a UINT8 property with minValue", () => { + it('should get the correct default value a UINT8 property with minValue', () => { const characteristic = createCharacteristicWithProps({ format: Formats.UINT8, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], minValue: 50, - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(50); - expect(characteristic.value).toEqual(null); // null if never set - }); + expect(characteristic.getDefaultValue()).toEqual(50) + expect(characteristic.value).toEqual(null) // null if never set + }) - it("should get the correct default value a INT property without minValue", () => { + it('should get the correct default value a INT property without minValue', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(0); - expect(characteristic.value).toEqual(null); // null if never set - }); + expect(characteristic.getDefaultValue()).toEqual(0) + expect(characteristic.value).toEqual(null) // null if never set + }) - it("should get the correct default value a INT property with minValue", () => { + it('should get the correct default value a INT property with minValue', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], minValue: 50, - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(50); - expect(characteristic.value).toEqual(null); // null if never set - }); + expect(characteristic.getDefaultValue()).toEqual(50) + expect(characteristic.value).toEqual(null) // null if never set + }) - it("should get the correct default value for the current temperature characteristic", () => { - const characteristic = new Characteristic.CurrentTemperature(); + it('should get the correct default value for the current temperature characteristic', () => { + const characteristic = new Characteristic.CurrentTemperature() // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(0); - expect(characteristic.value).toEqual(0); - }); + expect(characteristic.getDefaultValue()).toEqual(0) + expect(characteristic.value).toEqual(0) + }) - it("should get the default value from the first item in the validValues prop", () => { + it('should get the default value from the first item in the validValues prop', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], validValues: [5, 4, 3, 2], - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(5); - expect(characteristic.value).toEqual(null); // null if never set - }); + expect(characteristic.getDefaultValue()).toEqual(5) + expect(characteristic.value).toEqual(null) // null if never set + }) - it("should get the default value from minValue prop if set", () => { + it('should get the default value from minValue prop if set', () => { const characteristic = createCharacteristicWithProps({ format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], minValue: 100, maxValue: 255, - }); + }) // @ts-expect-error: private access - expect(characteristic.getDefaultValue()).toEqual(100); - expect(characteristic.value).toEqual(null); // null if never set - }); - - }); + expect(characteristic.getDefaultValue()).toEqual(100) + expect(characteristic.value).toEqual(null) // null if never set + }) + }) describe(`@${CharacteristicEventTypes.GET}`, () => { - it("should call any listeners for the event", (callback) => { - const characteristic = createCharacteristic(Formats.STRING); + it('should call any listeners for the event', async () => { + const characteristic = createCharacteristic(Formats.STRING) - const listenerCallback = jest.fn(); + const listenerCallback = vi.fn() - characteristic.handleGetRequest().then(() => { - characteristic.on(CharacteristicEventTypes.GET, listenerCallback); - characteristic.handleGetRequest(); - expect(listenerCallback).toHaveBeenCalledTimes(1); - callback(); - }); - }); + await characteristic.handleGetRequest() + characteristic.on(CharacteristicEventTypes.GET, listenerCallback) + characteristic.handleGetRequest() + expect(listenerCallback).toHaveBeenCalledTimes(1) + }) - it("should handle GET event errors gracefully when using on('get')", async () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should handle GET event errors gracefully when using on(\'get\')', async () => { + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // throw HapStatusError - should not trigger characteristic warning - mock.mockReset(); - characteristic.removeAllListeners("get"); - characteristic.on("get", (callback) => { - callback(new HapStatusError(HAPStatus.RESOURCE_BUSY)); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('get') + characteristic.on('get', (callback) => { + callback(new HapStatusError(HAPStatus.RESOURCE_BUSY)) + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw number - should not trigger characteristic warning - mock.mockReset(); - characteristic.removeAllListeners("get"); - characteristic.on("get", (callback) => { - callback(HAPStatus.RESOURCE_BUSY); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('get') + characteristic.on('get', (callback) => { + callback(HAPStatus.RESOURCE_BUSY) + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw out of range number - should convert status code to SERVICE_COMMUNICATION_FAILURE - mock.mockReset(); - characteristic.removeAllListeners("get"); - characteristic.on("get", (callback) => { - callback(234234234234 as HAPStatus); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('get') + characteristic.on('get', (callback) => { + callback(234234234234 as HAPStatus) + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw other error - callback style getters should still not trigger warning when error is passed in - mock.mockReset(); - characteristic.removeAllListeners("get"); - characteristic.on("get", (callback) => { - callback(new Error("Something else")); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); - }); - }); - - describe("onGet handler", () => { - it("should ignore GET event handler when onGet was specified", async () => { - const characteristic = createCharacteristic(Formats.STRING); - - const listenerCallback = jest.fn().mockImplementation((callback) => { - callback(undefined, "OddValue"); - }); - const handlerMock = jest.fn(); + mock.mockReset() + characteristic.removeAllListeners('get') + characteristic.on('get', (callback) => { + callback(new Error('Something else')) + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) + }) + }) + + describe('onGet handler', () => { + it('should ignore GET event handler when onGet was specified', async () => { + const characteristic = createCharacteristic(Formats.STRING) + + const listenerCallback = vi.fn().mockImplementation((callback) => { + callback(undefined, 'OddValue') + }) + const handlerMock = vi.fn() characteristic.onGet(() => { - handlerMock(); - return "CurrentValue"; - }); - characteristic.on(CharacteristicEventTypes.GET, listenerCallback); - const value = await characteristic.handleGetRequest(); + handlerMock() + return 'CurrentValue' + }) + characteristic.on(CharacteristicEventTypes.GET, listenerCallback) + const value = await characteristic.handleGetRequest() - expect(value).toEqual("CurrentValue"); - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(listenerCallback).toHaveBeenCalledTimes(0); - }); + expect(value).toEqual('CurrentValue') + expect(handlerMock).toHaveBeenCalledTimes(1) + expect(listenerCallback).toHaveBeenCalledTimes(0) + }) - it("should handle GET event errors gracefully when using the onGet handler", async () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should handle GET event errors gracefully when using the onGet handler', async () => { + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // throw HapStatusError - should not trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onGet(() => { - throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw number - should not trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onGet(() => { - throw HAPStatus.SERVICE_COMMUNICATION_FAILURE; - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + throw HAPStatus.SERVICE_COMMUNICATION_FAILURE + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw out of range number - should convert status code to SERVICE_COMMUNICATION_FAILURE - mock.mockReset(); + mock.mockReset() characteristic.onGet(() => { - throw 234234234234; - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + throw 234234234234 // eslint-disable-line no-throw-literal + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw other error - should trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onGet(() => { - throw new Error("A Random Error"); - }); - await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(1); - }); - }); + throw new Error('A Random Error') + }) + await expect(characteristic.handleGetRequest()).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(1) + }) + }) describe(`@${CharacteristicEventTypes.SET}`, () => { - it("should call any listeners for the event", () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should call any listeners for the event', () => { + const characteristic = createCharacteristic(Formats.STRING) - const VALUE = "NewValue"; - const listenerCallback = jest.fn(); + const VALUE = 'NewValue' + const listenerCallback = vi.fn() - characteristic.handleSetRequest(VALUE); - characteristic.on(CharacteristicEventTypes.SET, listenerCallback); - characteristic.handleSetRequest(VALUE); + characteristic.handleSetRequest(VALUE) + characteristic.on(CharacteristicEventTypes.SET, listenerCallback) + characteristic.handleSetRequest(VALUE) - expect(listenerCallback).toHaveBeenCalledTimes(1); - }); + expect(listenerCallback).toHaveBeenCalledTimes(1) + }) - it("should handle SET event errors gracefully when using on('set')", async () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should handle SET event errors gracefully when using on(\'set\')', async () => { + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // throw HapStatusError - should not trigger characteristic warning - mock.mockReset(); - characteristic.removeAllListeners("set"); - characteristic.on("set", (value, callback) => { - callback(new HapStatusError(HAPStatus.RESOURCE_BUSY)); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('set') + characteristic.on('set', (value, callback) => { + callback(new HapStatusError(HAPStatus.RESOURCE_BUSY)) + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw number - should not trigger characteristic warning - mock.mockReset(); - characteristic.removeAllListeners("set"); - characteristic.on("set", (value, callback) => { - callback(HAPStatus.RESOURCE_BUSY); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('set') + characteristic.on('set', (value, callback) => { + callback(HAPStatus.RESOURCE_BUSY) + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw out of range number - should convert status code to SERVICE_COMMUNICATION_FAILURE - mock.mockReset(); - characteristic.removeAllListeners("set"); - characteristic.on("set", (value, callback) => { - callback(234234234234 as HAPStatus); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + mock.mockReset() + characteristic.removeAllListeners('set') + characteristic.on('set', (value, callback) => { + callback(234234234234 as HAPStatus) + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw other error - callback style setters should still not trigger warning when error is passed in - mock.mockReset(); - characteristic.removeAllListeners("set"); - characteristic.on("set", (value, callback) => { - callback(new Error("Something else")); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); - }); - }); - - describe("onSet handler", () => { - it("should ignore SET event handler when onSet was specified", () => { - const characteristic = createCharacteristic(Formats.STRING); - - const listenerCallback = jest.fn(); - const handlerMock = jest.fn(); - - characteristic.onSet(value => { - handlerMock(value); - expect(value).toEqual("NewValue"); - return; - }); - characteristic.on(CharacteristicEventTypes.SET, listenerCallback); - characteristic.handleSetRequest("NewValue"); - - expect(handlerMock).toHaveBeenCalledTimes(1); - expect(listenerCallback).toHaveBeenCalledTimes(0); - }); - - it("should handle SET event errors gracefully when using onSet handler", async () => { - const characteristic = createCharacteristic(Formats.STRING); + mock.mockReset() + characteristic.removeAllListeners('set') + characteristic.on('set', (value, callback) => { + callback(new Error('Something else')) + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) + }) + }) + + describe('onSet handler', () => { + it('should ignore SET event handler when onSet was specified', () => { + const characteristic = createCharacteristic(Formats.STRING) + + const listenerCallback = vi.fn() + const handlerMock = vi.fn() + + characteristic.onSet((value) => { + handlerMock(value) + expect(value).toEqual('NewValue') + }) + characteristic.on(CharacteristicEventTypes.SET, listenerCallback) + characteristic.handleSetRequest('NewValue') + + expect(handlerMock).toHaveBeenCalledTimes(1) + expect(listenerCallback).toHaveBeenCalledTimes(0) + }) + + it('should handle SET event errors gracefully when using onSet handler', async () => { + const characteristic = createCharacteristic(Formats.STRING) // @ts-expect-error: spying on private property - const mock = jest.spyOn(characteristic, "characteristicWarning"); + const mock = vi.spyOn(characteristic, 'characteristicWarning') // throw HapStatusError - should not trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onSet(() => { - throw new HapStatusError(HAPStatus.RESOURCE_BUSY); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + throw new HapStatusError(HAPStatus.RESOURCE_BUSY) + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw number - should not trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onSet(() => { - throw HAPStatus.RESOURCE_BUSY; - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.RESOURCE_BUSY); - expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY); - expect(mock).toBeCalledTimes(0); + throw HAPStatus.RESOURCE_BUSY + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.RESOURCE_BUSY) + expect(characteristic.statusCode).toEqual(HAPStatus.RESOURCE_BUSY) + expect(mock).toBeCalledTimes(0) // throw out of range number - should convert status code to SERVICE_COMMUNICATION_FAILURE - mock.mockReset(); + mock.mockReset() characteristic.onSet(() => { - throw 234234234234; - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(0); + throw 234234234234 // eslint-disable-line no-throw-literal + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(0) // throw other error - should trigger characteristic warning - mock.mockReset(); + mock.mockReset() characteristic.onSet(() => { - throw new Error("A Random Error"); - }); - await expect(characteristic.handleSetRequest("hello")).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - expect(mock).toBeCalledTimes(1); - }); - }); + throw new Error('A Random Error') + }) + await expect(characteristic.handleSetRequest('hello')).rejects.toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(characteristic.statusCode).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + expect(mock).toBeCalledTimes(1) + }) + }) describe(`@${CharacteristicEventTypes.CHANGE}`, () => { + it('should call listeners for the event when the characteristic is event-only, and the value is set', async () => { + const characteristic = createCharacteristic(Formats.STRING, Characteristic.ProgrammableSwitchEvent.UUID) - it("should call listeners for the event when the characteristic is event-only, and the value is set", (callback) => { - const characteristic = createCharacteristic(Formats.STRING, Characteristic.ProgrammableSwitchEvent.UUID); + const VALUE = 'NewValue' + const listenerCallback = vi.fn() + const setValueCallback = vi.fn() - const VALUE = "NewValue"; - const listenerCallback = jest.fn(); - const setValueCallback = jest.fn(); + await characteristic.setValue(VALUE) + setValueCallback() - characteristic.setValue(VALUE, () => { - setValueCallback(); + characteristic.on(CharacteristicEventTypes.CHANGE, listenerCallback) + await characteristic.setValue(VALUE) + setValueCallback() - characteristic.on(CharacteristicEventTypes.CHANGE, listenerCallback); - characteristic.setValue(VALUE, () => { - setValueCallback(); + expect(listenerCallback).toHaveBeenCalledTimes(1) + expect(setValueCallback).toHaveBeenCalledTimes(2) + }) - expect(listenerCallback).toHaveBeenCalledTimes(1); - expect(setValueCallback).toHaveBeenCalledTimes(2); - callback(); - }); - }); - }); - - it("should call any listeners for the event when the characteristic is event-only, and the value is updated", () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should call any listeners for the event when the characteristic is event-only, and the value is updated', () => { + const characteristic = createCharacteristic(Formats.STRING) // characteristic.eventOnlyCharacteristic = true; - const VALUE = "NewValue"; - const listenerCallback = jest.fn(); - const updateValueCallback = jest.fn(); - - characteristic.on(CharacteristicEventTypes.CHANGE, listenerCallback); - // noinspection JSDeprecatedSymbols - characteristic.updateValue(VALUE, updateValueCallback); + const VALUE = 'NewValue' + const listenerCallback = vi.fn() + const updateValueCallback = vi.fn() - expect(listenerCallback).toHaveBeenCalledTimes(1); - expect(updateValueCallback).toHaveBeenCalledTimes(1); - }); + characteristic.on(CharacteristicEventTypes.CHANGE, listenerCallback) + characteristic.updateValue(VALUE, updateValueCallback) - it("should call the change listener with proper context when supplied as second argument to updateValue", () => { - const characteristic = createCharacteristic(Formats.STRING); + expect(listenerCallback).toHaveBeenCalledTimes(1) + expect(updateValueCallback).toHaveBeenCalledTimes(1) + }) - const VALUE = "NewValue"; - const CONTEXT = "Context"; + it('should call the change listener with proper context when supplied as second argument to updateValue', () => { + const characteristic = createCharacteristic(Formats.STRING) - const listener = jest.fn().mockImplementation((change: CharacteristicChange) => { - expect(change.newValue).toEqual(VALUE); - expect(change.context).toEqual(CONTEXT); - }); + const VALUE = 'NewValue' + const CONTEXT = 'Context' - characteristic.on(CharacteristicEventTypes.CHANGE, listener); - characteristic.updateValue(VALUE, CONTEXT); + const listener = vi.fn().mockImplementation((change: CharacteristicChange) => { + expect(change.newValue).toEqual(VALUE) + expect(change.context).toEqual(CONTEXT) + }) - expect(listener).toHaveBeenCalledTimes(1); - }); + characteristic.on(CharacteristicEventTypes.CHANGE, listener) + characteristic.updateValue(VALUE, CONTEXT) - it("should call the change listener with proper context when supplied as second argument to setValue", () => { - const characteristic = createCharacteristic(Formats.STRING); + expect(listener).toHaveBeenCalledTimes(1) + }) - const VALUE = "NewValue"; - const CONTEXT = "Context"; + it('should call the change listener with proper context when supplied as second argument to setValue', () => { + const characteristic = createCharacteristic(Formats.STRING) - const listener = jest.fn().mockImplementation((change: CharacteristicChange) => { - expect(change.newValue).toEqual(VALUE); - expect(change.context).toEqual(CONTEXT); - }); + const VALUE = 'NewValue' + const CONTEXT = 'Context' - characteristic.on(CharacteristicEventTypes.CHANGE, listener); - characteristic.setValue(VALUE, CONTEXT); + const listener = vi.fn().mockImplementation((change: CharacteristicChange) => { + expect(change.newValue).toEqual(VALUE) + expect(change.context).toEqual(CONTEXT) + }) + characteristic.on(CharacteristicEventTypes.CHANGE, listener) + characteristic.setValue(VALUE, CONTEXT) - expect(listener).toHaveBeenCalledTimes(1); - }); - }); + expect(listener).toHaveBeenCalledTimes(1) + }) + }) describe(`@${CharacteristicEventTypes.SUBSCRIBE}`, () => { + it('should call any listeners for the event', () => { + const characteristic = createCharacteristic(Formats.STRING) - it("should call any listeners for the event", () => { - const characteristic = createCharacteristic(Formats.STRING); + const cb = vi.fn() - const cb = jest.fn(); + characteristic.on(CharacteristicEventTypes.SUBSCRIBE, cb) + characteristic.subscribe() - characteristic.on(CharacteristicEventTypes.SUBSCRIBE, cb); - characteristic.subscribe(); - - expect(cb).toHaveBeenCalledTimes(1); - }); - }); + expect(cb).toHaveBeenCalledTimes(1) + }) + }) describe(`@${CharacteristicEventTypes.UNSUBSCRIBE}`, () => { + it('should call any listeners for the event', () => { + const characteristic = createCharacteristic(Formats.STRING) - it("should call any listeners for the event", () => { - const characteristic = createCharacteristic(Formats.STRING); - - const cb = jest.fn(); + const cb = vi.fn() - characteristic.subscribe(); - characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, cb); - characteristic.unsubscribe(); + characteristic.subscribe() + characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, cb) + characteristic.unsubscribe() - expect(cb).toHaveBeenCalledTimes(1); - }); + expect(cb).toHaveBeenCalledTimes(1) + }) - it("should not call any listeners for the event if none are registered", () => { - const characteristic = createCharacteristic(Formats.STRING); + it('should not call any listeners for the event if none are registered', () => { + const characteristic = createCharacteristic(Formats.STRING) - const cb = jest.fn(); + const cb = vi.fn() - characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, cb); - characteristic.unsubscribe(); + characteristic.on(CharacteristicEventTypes.UNSUBSCRIBE, cb) + characteristic.unsubscribe() - expect(cb).not.toHaveBeenCalled(); - }); - }); + expect(cb).not.toHaveBeenCalled() + }) + }) - describe("#serialize", () => { - it("should serialize characteristic", () => { + describe('#serialize', () => { + it('should serialize characteristic', () => { const props: CharacteristicProps = { format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], @@ -2057,54 +2063,54 @@ describe("Characteristic", () => { minValue: 123, validValueRanges: [123, 1234], adminOnlyAccess: [Access.WRITE], - }; + } - const characteristic = createCharacteristicWithProps(props, Characteristic.ProgrammableSwitchEvent.UUID); - characteristic.value = "TestValue"; + const characteristic = createCharacteristicWithProps(props, Characteristic.ProgrammableSwitchEvent.UUID) + characteristic.value = 'TestValue' - const json = Characteristic.serialize(characteristic); + const json = Characteristic.serialize(characteristic) expect(json).toEqual({ displayName: characteristic.displayName, UUID: characteristic.UUID, - props: props, - value: "TestValue", + props, + value: 'TestValue', eventOnlyCharacteristic: true, - }); - }); + }) + }) - it("should serialize characteristic with proper constructor name", () => { - const characteristic = new Characteristic.Name(); - characteristic.updateValue("New Name!"); + it('should serialize characteristic with proper constructor name', () => { + const characteristic = new Characteristic.Name() + characteristic.updateValue('New Name!') - const json = Characteristic.serialize(characteristic); + const json = Characteristic.serialize(characteristic) expect(json).toEqual({ - displayName: "Name", - UUID: "00000023-0000-1000-8000-0026BB765291", + displayName: 'Name', + UUID: '00000023-0000-1000-8000-0026BB765291', eventOnlyCharacteristic: false, - constructorName: "Name", - value: "New Name!", - props: { format: "string", perms: [ "pr" ], maxLen: 64 }, - }); - }); - }); - - describe("#deserialize", () => { - it("should deserialize legacy json from homebridge", () => { - const json = JSON.parse("{\"displayName\": \"On\", \"UUID\": \"00000025-0000-1000-8000-0026BB765291\", " + - "\"props\": {\"format\": \"int\", \"unit\": \"seconds\", \"minValue\": 4, \"maxValue\": 6, \"minStep\": 0.1, \"perms\": [\"pr\", \"pw\", \"ev\"]}, " + - "\"value\": false, \"eventOnlyCharacteristic\": false}"); - const characteristic = Characteristic.deserialize(json); - - expect(characteristic.displayName).toEqual(json.displayName); - expect(characteristic.UUID).toEqual(json.UUID); - expect(characteristic.props).toEqual(json.props); - expect(characteristic.value).toEqual(json.value); - }); - - it("should deserialize complete json", () => { + constructorName: 'Name', + value: 'New Name!', + props: { format: 'string', perms: ['pr'], maxLen: 64 }, + }) + }) + }) + + describe('#deserialize', () => { + it('should deserialize legacy json from homebridge', () => { + const json = JSON.parse('{"displayName": "On", "UUID": "00000025-0000-1000-8000-0026BB765291", ' + + '"props": {"format": "int", "unit": "seconds", "minValue": 4, "maxValue": 6, "minStep": 0.1, "perms": ["pr", "pw", "ev"]}, ' + + '"value": false, "eventOnlyCharacteristic": false}') + const characteristic = Characteristic.deserialize(json) + + expect(characteristic.displayName).toEqual(json.displayName) + expect(characteristic.UUID).toEqual(json.UUID) + expect(characteristic.props).toEqual(json.props) + expect(characteristic.value).toEqual(json.value) + }) + + it('should deserialize complete json', () => { const json: SerializedCharacteristic = { - displayName: "MyName", - UUID: "00000001-0000-1000-8000-0026BB765291", + displayName: 'MyName', + UUID: '00000001-0000-1000-8000-0026BB765291', props: { format: Formats.INT, perms: [Perms.TIMED_WRITE, Perms.PAIRED_READ], @@ -2114,33 +2120,31 @@ describe("Characteristic", () => { validValueRanges: [123, 1234], adminOnlyAccess: [Access.NOTIFY, Access.READ], }, - value: "testValue", + value: 'testValue', eventOnlyCharacteristic: false, - }; + } - const characteristic = Characteristic.deserialize(json); + const characteristic = Characteristic.deserialize(json) - expect(characteristic.displayName).toEqual(json.displayName); - expect(characteristic.UUID).toEqual(json.UUID); - expect(characteristic.props).toEqual(json.props); - expect(characteristic.value).toEqual(json.value); - }); + expect(characteristic.displayName).toEqual(json.displayName) + expect(characteristic.UUID).toEqual(json.UUID) + expect(characteristic.props).toEqual(json.props) + expect(characteristic.value).toEqual(json.value) + }) - it("should deserialize from json with constructor name", () => { + it('should deserialize from json with constructor name', () => { const json: SerializedCharacteristic = { - displayName: "Name", - UUID: "00000023-0000-1000-8000-0026BB765291", + displayName: 'Name', + UUID: '00000023-0000-1000-8000-0026BB765291', eventOnlyCharacteristic: false, - constructorName: "Name", - value: "New Name!", - props: { format: "string", perms: [ Perms.PAIRED_READ ], maxLen: 64 }, - }; - - const characteristic = Characteristic.deserialize(json); - - expect(characteristic instanceof Characteristic.Name).toBeTruthy(); - }); + constructorName: 'Name', + value: 'New Name!', + props: { format: 'string', perms: [Perms.PAIRED_READ], maxLen: 64 }, + } - }); + const characteristic = Characteristic.deserialize(json) -}); + expect(characteristic instanceof Characteristic.Name).toBeTruthy() + }) + }) +}) diff --git a/src/lib/Characteristic.ts b/src/lib/Characteristic.ts index c99ea81de..b8e919d8e 100644 --- a/src/lib/Characteristic.ts +++ b/src/lib/Characteristic.ts @@ -1,8 +1,4 @@ -import assert from "assert"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { CharacteristicJsonObject, CharacteristicValue, Nullable, PartialAllowingNull, VoidCallback } from "../types"; -import { CharacteristicWarningType } from "./Accessory"; +import type { CharacteristicJsonObject, CharacteristicValue, Nullable, PartialAllowingNull, VoidCallback } from '../types' import type { AccessCodeControlPoint, AccessCodeSupportedConfiguration, @@ -243,13 +239,21 @@ import type { WiFiCapabilities, WiFiConfigurationControl, WiFiSatelliteStatus, -} from "./definitions"; -import { HAPStatus, IsKnownHAPStatusError } from "./HAPServer"; -import { IdentifierCache } from "./model/IdentifierCache"; -import { clone } from "./util/clone"; -import { HAPConnection } from "./util/eventedhttp"; -import { HapStatusError } from "./util/hapStatusError"; -import { once } from "./util/once"; +} from './definitions' +import type { IdentifierCache } from './model/IdentifierCache' +import type { HAPConnection } from './util/eventedhttp' + +import assert from 'node:assert' +import { EventEmitter } from 'node:events' + +import createDebug from 'debug' + +import { CharacteristicWarningType } from './Accessory.js' +import { HAPStatus, isKnownHAPStatusError } from './HAPServer.js' +import { checkName } from './util/checkName.js' +import { clone } from './util/clone.js' +import { HapStatusError } from './util/hapStatusError.js' +import { once } from './util/once.js' import { formatOutgoingCharacteristicValue, isIntegerNumericFormat, @@ -257,125 +261,126 @@ import { isUnsignedNumericFormat, numericLowerBound, numericUpperBound, -} from "./util/request-util"; -import { BASE_UUID, toShortForm } from "./util/uuid"; -import { checkName } from "./util/checkName"; +} from './util/request-util.js' +import { BASE_UUID, toShortForm } from './util/uuid.js' -const debug = createDebug("HAP-NodeJS:Characteristic"); +const debug = createDebug('HAP-NodeJS:Characteristic') /** * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum Formats { - BOOL = "bool", + BOOL = 'bool', /** * Signed 32-bit integer */ - INT = "int", // signed 32-bit int + INT = 'int', // signed 32-bit int /** * Signed 64-bit floating point */ - FLOAT = "float", + FLOAT = 'float', /** * String encoded in utf8 */ - STRING = "string", + STRING = 'string', /** * Unsigned 8-bit integer. */ - UINT8 = "uint8", + UINT8 = 'uint8', /** * Unsigned 16-bit integer. */ - UINT16 = "uint16", + UINT16 = 'uint16', /** * Unsigned 32-bit integer. */ - UINT32 = "uint32", + UINT32 = 'uint32', /** * Unsigned 64-bit integer. */ - UINT64 = "uint64", + UINT64 = 'uint64', /** * Data is base64 encoded string. */ - DATA = "data", + DATA = 'data', /** * Base64 encoded tlv8 string. */ - TLV8 = "tlv8" + TLV8 = 'tlv8', } /** * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum Units { /** * Celsius is the only temperature unit in the HomeKit Accessory Protocol. * Unit conversion is always done on the client side e.g. on the iPhone in the Home App depending on * the configured unit on the device itself. */ - CELSIUS = "celsius", - PERCENTAGE = "percentage", - ARC_DEGREE = "arcdegrees", - LUX = "lux", - SECONDS = "seconds", + CELSIUS = 'celsius', + PERCENTAGE = 'percentage', + ARC_DEGREE = 'arcdegrees', + LUX = 'lux', + SECONDS = 'seconds', } /** * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum Perms { - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - PAIRED_READ = "pr", - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - PAIRED_WRITE = "pw", - NOTIFY = "ev", - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - EVENTS = "ev", - ADDITIONAL_AUTHORIZATION = "aa", - TIMED_WRITE = "tw", - HIDDEN = "hd", - WRITE_RESPONSE = "wr", + PAIRED_READ = 'pr', + PAIRED_WRITE = 'pw', + NOTIFY = 'ev', + + // eslint-disable-next-line ts/no-duplicate-enum-values + EVENTS = 'ev', + ADDITIONAL_AUTHORIZATION = 'aa', + TIMED_WRITE = 'tw', + HIDDEN = 'hd', + WRITE_RESPONSE = 'wr', } /** * @group Characteristic */ export interface CharacteristicProps { - format: Formats | string; - perms: Perms[]; - unit?: Units | string; - description?: string; + format: Formats | string + perms: Perms[] + unit?: Units | string + description?: string /** * Defines the minimum value for a numeric characteristic */ - minValue?: number; + minValue?: number /** * Defines the maximum value for a numeric characteristic */ - maxValue?: number; - minStep?: number; + maxValue?: number + minStep?: number /** * Maximum number of characters when format is {@link Formats.STRING}. * Default is 64 characters. Maximum allowed is 256 characters. */ - maxLen?: number; + maxLen?: number /** * Maximum number of characters when format is {@link Formats.DATA}. * Default is 2097152 characters. */ - maxDataLen?: number; + maxDataLen?: number /** * Defines an array of valid values to be used for the characteristic. */ - validValues?: number[]; + validValues?: number[] /** * Two element array where the first value specifies the lowest valid value and * the second element specifies the highest valid value. */ - validValueRanges?: [min: number, max: number]; - adminOnlyAccess?: Access[]; + validValueRanges?: [min: number, max: number] + adminOnlyAccess?: Access[] } /** @@ -386,43 +391,45 @@ export interface CharacteristicProps { * * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum Access { READ = 0x00, WRITE = 0x01, - NOTIFY = 0x02 + NOTIFY = 0x02, } /** * @group Characteristic */ -export type CharacteristicChange = { - originator?: HAPConnection, - newValue: Nullable; - oldValue: Nullable; - reason: ChangeReason, - context?: CharacteristicContext; -}; +export interface CharacteristicChange { + originator?: HAPConnection + newValue: Nullable + oldValue: Nullable + reason: ChangeReason + context?: CharacteristicContext +} /** * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum ChangeReason { /** * Reason used when HomeKit writes a value or the API user calls {@link Characteristic.setValue}. */ - WRITE = "write", + WRITE = 'write', /** * Reason used when the API user calls the method {@link Characteristic.updateValue}. */ - UPDATE = "update", + UPDATE = 'update', /** * Used when HomeKit reads a value or the API user calls the deprecated method `Characteristic.getValue`. */ - READ = "read", + READ = 'read', /** * Used when call to {@link Characteristic.sendEventNotification} was made. */ - EVENT = "event", + EVENT = 'event', } /** @@ -438,25 +445,26 @@ export interface CharacteristicOperationContext { * the Accessory won't send any event notifications to HomeKit controllers * for that particular change. */ - omitEventUpdate?: boolean; + omitEventUpdate?: boolean } /** * @group Characteristic */ export interface SerializedCharacteristic { - displayName: string, - UUID: string, - eventOnlyCharacteristic: boolean, - constructorName?: string, + displayName: string + UUID: string + eventOnlyCharacteristic: boolean + constructorName?: string - value: Nullable, - props: CharacteristicProps, + value: Nullable + props: CharacteristicProps } /** * @group Characteristic */ +// eslint-disable-next-line no-restricted-syntax export const enum CharacteristicEventTypes { /** * This event is thrown when a HomeKit controller wants to read the current value of the characteristic. @@ -464,191 +472,188 @@ export const enum CharacteristicEventTypes { * * HAP-NodeJS will complain about slow running get handlers after 3 seconds and terminate the request after 10 seconds. */ - GET = "get", + GET = 'get', /** * This event is thrown when a HomeKit controller wants to write a new value to the characteristic. * The event handler should call the supplied callback as fast as possible. * * HAP-NodeJS will complain about slow running set handlers after 3 seconds and terminate the request after 10 seconds. */ - SET = "set", + SET = 'set', /** * Emitted after a new value is set for the characteristic. * The new value can be set via a request by a HomeKit controller or via an API call. */ - CHANGE = "change", + CHANGE = 'change', /** * @private */ - SUBSCRIBE = "subscribe", + SUBSCRIBE = 'subscribe', /** * @private */ - UNSUBSCRIBE = "unsubscribe", + UNSUBSCRIBE = 'unsubscribe', /** * @private */ - CHARACTERISTIC_WARNING = "characteristic-warning", + CHARACTERISTIC_WARNING = 'characteristic-warning', } /** * @group Characteristic */ -export type CharacteristicContext = any; // eslint-disable-line @typescript-eslint/no-explicit-any +export type CharacteristicContext = any /** * @group Characteristic */ -export type CharacteristicGetCallback = (status?: HAPStatus | null | Error, value?: Nullable) => void; +export type CharacteristicGetCallback = (status?: HAPStatus | null | Error, value?: Nullable) => void /** * @group Characteristic */ -export type CharacteristicSetCallback = (error?: HAPStatus | null | Error, writeResponse?: Nullable) => void; +export type CharacteristicSetCallback = (error?: HAPStatus | null | Error, writeResponse?: Nullable) => void /** * @group Characteristic */ export type CharacteristicGetHandler = (context: CharacteristicContext, connection?: HAPConnection) - => Promise> | Nullable; +=> Promise> | Nullable /** * @group Characteristic */ export type CharacteristicSetHandler = (value: CharacteristicValue, context: CharacteristicContext, connection?: HAPConnection) - => Promise | void> | Nullable | void; +=> Promise | void> | Nullable | void /** * @group Characteristic */ -export type AdditionalAuthorizationHandler = (additionalAuthorizationData: string | undefined) => boolean; +export type AdditionalAuthorizationHandler = (additionalAuthorizationData: string | undefined) => boolean /** * @group Characteristic */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface Characteristic { - - on(event: "get", listener: (callback: CharacteristicGetCallback, context: CharacteristicContext, connection?: HAPConnection) => void): this; + /* eslint-disable ts/method-signature-style */ + on(event: 'get', listener: (callback: CharacteristicGetCallback, context: CharacteristicContext, connection?: HAPConnection) => void): this on( - event: "set", + event: 'set', listener: (value: CharacteristicValue, callback: CharacteristicSetCallback, context: CharacteristicContext, connection?: HAPConnection) => void ): this - on(event: "change", listener: (change: CharacteristicChange) => void): this; + on(event: 'change', listener: (change: CharacteristicChange) => void): this /** * @private */ - on(event: "subscribe", listener: VoidCallback): this; + on(event: 'subscribe', listener: VoidCallback): this /** * @private */ - on(event: "unsubscribe", listener: VoidCallback): this; + on(event: 'unsubscribe', listener: VoidCallback): this /** * @private */ - on(event: "characteristic-warning", listener: (type: CharacteristicWarningType, message: string, stack?: string) => void): this; - + on(event: 'characteristic-warning', listener: (type: CharacteristicWarningType, message: string, stack?: string) => void): this /** * @private */ - emit(event: "get", callback: CharacteristicGetCallback, context: CharacteristicContext, connection?: HAPConnection): boolean; + emit(event: 'get', callback: CharacteristicGetCallback, context: CharacteristicContext, connection?: HAPConnection): boolean /** * @private */ - emit(event: "set", value: CharacteristicValue, callback: CharacteristicSetCallback, context: CharacteristicContext, connection?: HAPConnection): boolean; + emit(event: 'set', value: CharacteristicValue, callback: CharacteristicSetCallback, context: CharacteristicContext, connection?: HAPConnection): boolean /** * @private */ - emit(event: "change", change: CharacteristicChange): boolean; + emit(event: 'change', change: CharacteristicChange): boolean /** * @private */ - emit(event: "subscribe"): boolean; + emit(event: 'subscribe'): boolean /** * @private */ - emit(event: "unsubscribe"): boolean; + emit(event: 'unsubscribe'): boolean /** * @private */ - emit(event: "characteristic-warning", type: CharacteristicWarningType, message: string, stack?: string): boolean; - + emit(event: 'characteristic-warning', type: CharacteristicWarningType, message: string, stack?: string): boolean + /* eslint-enable ts/method-signature-style */ } /** * @group Characteristic */ class ValidValuesIterable implements Iterable { - - private readonly props: CharacteristicProps; + private readonly props: CharacteristicProps constructor(props: CharacteristicProps) { - assert(isNumericFormat(props.format), "Cannot instantiate valid values iterable when format is not numeric. Found " + props.format); - this.props = props; + assert(isNumericFormat(props.format), `Cannot instantiate valid values iterable when format is not numeric. Found ${props.format}`) + this.props = props } *[Symbol.iterator](): Iterator { if (this.props.validValues) { for (const value of this.props.validValues) { - yield value; + yield value } } else { - let min = 0; // default is zero for all the uint types - let max: number; - let stepValue = 1; + let min = 0 // default is zero for all the uint types + let max: number + let stepValue = 1 if (this.props.validValueRanges) { - min = this.props.validValueRanges[0]; - max = this.props.validValueRanges[1]; + min = this.props.validValueRanges[0] + max = this.props.validValueRanges[1] } else if (this.props.minValue != null && this.props.maxValue != null) { - min = this.props.minValue; - max = this.props.maxValue; + min = this.props.minValue + max = this.props.maxValue if (this.props.minStep != null) { - stepValue = this.props.minStep; + stepValue = this.props.minStep } } else if (isUnsignedNumericFormat(this.props.format)) { - max = numericUpperBound(this.props.format); + max = numericUpperBound(this.props.format) } else { - throw new Error("Could not find valid iterator strategy for props: " + JSON.stringify(this.props)); + throw new Error(`Could not find valid iterator strategy for props: ${JSON.stringify(this.props)}`) } for (let i = min; i <= max; i += stepValue) { - yield i; + yield i } } } - } -const numberPattern = /^-?\d+$/; +const numberPattern = /^-?\d+$/ function extractHAPStatusFromError(error: Error) { - let errorValue = HAPStatus.SERVICE_COMMUNICATION_FAILURE; + let errorValue = HAPStatus.SERVICE_COMMUNICATION_FAILURE if (numberPattern.test(error.message)) { - const value = parseInt(error.message, 10); + const value = Number.parseInt(error.message, 10) - if (IsKnownHAPStatusError(value)) { - errorValue = value; + if (isKnownHAPStatusError(value)) { + errorValue = value } } - return errorValue; + return errorValue } function maxWithUndefined(a?: number, b?: number): number | undefined { if (a == null) { - return b; + return b } else if (b == null) { - return a; + return a } else { - return Math.max(a, b); + return Math.max(a, b) } } function minWithUndefined(a?: number, b?: number): number | undefined { if (a == null) { - return b; + return b } else if (b == null) { - return a; + return a } else { - return Math.min(a, b); + return Math.min(a, b) } } @@ -664,1006 +669,1005 @@ function minWithUndefined(a?: number, b?: number): number | undefined { * * @group Characteristic */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class Characteristic extends EventEmitter { - // Pattern below is for automatic detection of the section of defined characteristics. Used by the generator // -=-=-=-=-=-=-=-=-=-=-=-=-=-=- /** * @group Characteristic Definitions */ - public static AccessCodeControlPoint: typeof AccessCodeControlPoint; + public static AccessCodeControlPoint: typeof AccessCodeControlPoint /** * @group Characteristic Definitions */ - public static AccessCodeSupportedConfiguration: typeof AccessCodeSupportedConfiguration; + public static AccessCodeSupportedConfiguration: typeof AccessCodeSupportedConfiguration /** * @group Characteristic Definitions */ - public static AccessControlLevel: typeof AccessControlLevel; + public static AccessControlLevel: typeof AccessControlLevel /** * @group Characteristic Definitions */ - public static AccessoryFlags: typeof AccessoryFlags; + public static AccessoryFlags: typeof AccessoryFlags /** * @group Characteristic Definitions */ - public static AccessoryIdentifier: typeof AccessoryIdentifier; + public static AccessoryIdentifier: typeof AccessoryIdentifier /** * @group Characteristic Definitions */ - public static Active: typeof Active; + public static Active: typeof Active /** * @group Characteristic Definitions */ - public static ActiveIdentifier: typeof ActiveIdentifier; + public static ActiveIdentifier: typeof ActiveIdentifier /** * @group Characteristic Definitions */ - public static ActivityInterval: typeof ActivityInterval; + public static ActivityInterval: typeof ActivityInterval /** * @group Characteristic Definitions */ - public static AdministratorOnlyAccess: typeof AdministratorOnlyAccess; + public static AdministratorOnlyAccess: typeof AdministratorOnlyAccess /** * @group Characteristic Definitions */ - public static AirParticulateDensity: typeof AirParticulateDensity; + public static AirParticulateDensity: typeof AirParticulateDensity /** * @group Characteristic Definitions */ - public static AirParticulateSize: typeof AirParticulateSize; + public static AirParticulateSize: typeof AirParticulateSize /** * @group Characteristic Definitions */ - public static AirPlayEnable: typeof AirPlayEnable; + public static AirPlayEnable: typeof AirPlayEnable /** * @group Characteristic Definitions */ - public static AirQuality: typeof AirQuality; + public static AirQuality: typeof AirQuality /** * @group Characteristic Definitions */ - public static AppMatchingIdentifier: typeof AppMatchingIdentifier; + public static AppMatchingIdentifier: typeof AppMatchingIdentifier /** * @group Characteristic Definitions */ - public static AssetUpdateReadiness: typeof AssetUpdateReadiness; + public static AssetUpdateReadiness: typeof AssetUpdateReadiness /** * @group Characteristic Definitions */ - public static AudioFeedback: typeof AudioFeedback; + public static AudioFeedback: typeof AudioFeedback /** * @group Characteristic Definitions */ - public static BatteryLevel: typeof BatteryLevel; + public static BatteryLevel: typeof BatteryLevel /** * @group Characteristic Definitions */ - public static Brightness: typeof Brightness; + public static Brightness: typeof Brightness /** * @group Characteristic Definitions */ - public static ButtonEvent: typeof ButtonEvent; + public static ButtonEvent: typeof ButtonEvent /** * @group Characteristic Definitions */ - public static CameraOperatingModeIndicator: typeof CameraOperatingModeIndicator; + public static CameraOperatingModeIndicator: typeof CameraOperatingModeIndicator /** * @group Characteristic Definitions */ - public static CarbonDioxideDetected: typeof CarbonDioxideDetected; + public static CarbonDioxideDetected: typeof CarbonDioxideDetected /** * @group Characteristic Definitions */ - public static CarbonDioxideLevel: typeof CarbonDioxideLevel; + public static CarbonDioxideLevel: typeof CarbonDioxideLevel /** * @group Characteristic Definitions */ - public static CarbonDioxidePeakLevel: typeof CarbonDioxidePeakLevel; + public static CarbonDioxidePeakLevel: typeof CarbonDioxidePeakLevel /** * @group Characteristic Definitions */ - public static CarbonMonoxideDetected: typeof CarbonMonoxideDetected; + public static CarbonMonoxideDetected: typeof CarbonMonoxideDetected /** * @group Characteristic Definitions */ - public static CarbonMonoxideLevel: typeof CarbonMonoxideLevel; + public static CarbonMonoxideLevel: typeof CarbonMonoxideLevel /** * @group Characteristic Definitions */ - public static CarbonMonoxidePeakLevel: typeof CarbonMonoxidePeakLevel; + public static CarbonMonoxidePeakLevel: typeof CarbonMonoxidePeakLevel /** * @group Characteristic Definitions */ - public static CCAEnergyDetectThreshold: typeof CCAEnergyDetectThreshold; + public static CCAEnergyDetectThreshold: typeof CCAEnergyDetectThreshold /** * @group Characteristic Definitions */ - public static CCASignalDetectThreshold: typeof CCASignalDetectThreshold; + public static CCASignalDetectThreshold: typeof CCASignalDetectThreshold /** * @group Characteristic Definitions */ - public static CharacteristicValueActiveTransitionCount: typeof CharacteristicValueActiveTransitionCount; + public static CharacteristicValueActiveTransitionCount: typeof CharacteristicValueActiveTransitionCount /** * @group Characteristic Definitions */ - public static CharacteristicValueTransitionControl: typeof CharacteristicValueTransitionControl; + public static CharacteristicValueTransitionControl: typeof CharacteristicValueTransitionControl /** * @group Characteristic Definitions */ - public static ChargingState: typeof ChargingState; + public static ChargingState: typeof ChargingState /** * @group Characteristic Definitions */ - public static ClosedCaptions: typeof ClosedCaptions; + public static ClosedCaptions: typeof ClosedCaptions /** * @group Characteristic Definitions */ - public static ColorTemperature: typeof ColorTemperature; + public static ColorTemperature: typeof ColorTemperature /** * @group Characteristic Definitions */ - public static ConfigurationState: typeof ConfigurationState; + public static ConfigurationState: typeof ConfigurationState /** * @group Characteristic Definitions */ - public static ConfiguredName: typeof ConfiguredName; + public static ConfiguredName: typeof ConfiguredName /** * @group Characteristic Definitions */ - public static ContactSensorState: typeof ContactSensorState; + public static ContactSensorState: typeof ContactSensorState /** * @group Characteristic Definitions */ - public static CoolingThresholdTemperature: typeof CoolingThresholdTemperature; + public static CoolingThresholdTemperature: typeof CoolingThresholdTemperature /** * @group Characteristic Definitions */ - public static CryptoHash: typeof CryptoHash; + public static CryptoHash: typeof CryptoHash /** * @group Characteristic Definitions */ - public static CurrentAirPurifierState: typeof CurrentAirPurifierState; + public static CurrentAirPurifierState: typeof CurrentAirPurifierState /** * @group Characteristic Definitions */ - public static CurrentAmbientLightLevel: typeof CurrentAmbientLightLevel; + public static CurrentAmbientLightLevel: typeof CurrentAmbientLightLevel /** * @group Characteristic Definitions */ - public static CurrentDoorState: typeof CurrentDoorState; + public static CurrentDoorState: typeof CurrentDoorState /** * @group Characteristic Definitions */ - public static CurrentFanState: typeof CurrentFanState; + public static CurrentFanState: typeof CurrentFanState /** * @group Characteristic Definitions */ - public static CurrentHeaterCoolerState: typeof CurrentHeaterCoolerState; + public static CurrentHeaterCoolerState: typeof CurrentHeaterCoolerState /** * @group Characteristic Definitions */ - public static CurrentHeatingCoolingState: typeof CurrentHeatingCoolingState; + public static CurrentHeatingCoolingState: typeof CurrentHeatingCoolingState /** * @group Characteristic Definitions */ - public static CurrentHorizontalTiltAngle: typeof CurrentHorizontalTiltAngle; + public static CurrentHorizontalTiltAngle: typeof CurrentHorizontalTiltAngle /** * @group Characteristic Definitions */ - public static CurrentHumidifierDehumidifierState: typeof CurrentHumidifierDehumidifierState; + public static CurrentHumidifierDehumidifierState: typeof CurrentHumidifierDehumidifierState /** * @group Characteristic Definitions */ - public static CurrentMediaState: typeof CurrentMediaState; + public static CurrentMediaState: typeof CurrentMediaState /** * @group Characteristic Definitions */ - public static CurrentPosition: typeof CurrentPosition; + public static CurrentPosition: typeof CurrentPosition /** * @group Characteristic Definitions */ - public static CurrentRelativeHumidity: typeof CurrentRelativeHumidity; + public static CurrentRelativeHumidity: typeof CurrentRelativeHumidity /** * @group Characteristic Definitions */ - public static CurrentSlatState: typeof CurrentSlatState; + public static CurrentSlatState: typeof CurrentSlatState /** * @group Characteristic Definitions */ - public static CurrentTemperature: typeof CurrentTemperature; + public static CurrentTemperature: typeof CurrentTemperature /** * @group Characteristic Definitions */ - public static CurrentTiltAngle: typeof CurrentTiltAngle; + public static CurrentTiltAngle: typeof CurrentTiltAngle /** * @group Characteristic Definitions */ - public static CurrentTransport: typeof CurrentTransport; + public static CurrentTransport: typeof CurrentTransport /** * @group Characteristic Definitions */ - public static CurrentVerticalTiltAngle: typeof CurrentVerticalTiltAngle; + public static CurrentVerticalTiltAngle: typeof CurrentVerticalTiltAngle /** * @group Characteristic Definitions */ - public static CurrentVisibilityState: typeof CurrentVisibilityState; + public static CurrentVisibilityState: typeof CurrentVisibilityState /** * @group Characteristic Definitions */ - public static DataStreamHAPTransport: typeof DataStreamHAPTransport; + public static DataStreamHAPTransport: typeof DataStreamHAPTransport /** * @group Characteristic Definitions */ - public static DataStreamHAPTransportInterrupt: typeof DataStreamHAPTransportInterrupt; + public static DataStreamHAPTransportInterrupt: typeof DataStreamHAPTransportInterrupt /** * @group Characteristic Definitions */ - public static DiagonalFieldOfView: typeof DiagonalFieldOfView; + public static DiagonalFieldOfView: typeof DiagonalFieldOfView /** * @group Characteristic Definitions */ - public static DigitalZoom: typeof DigitalZoom; + public static DigitalZoom: typeof DigitalZoom /** * @group Characteristic Definitions */ - public static DisplayOrder: typeof DisplayOrder; + public static DisplayOrder: typeof DisplayOrder /** * @group Characteristic Definitions */ - public static EventRetransmissionMaximum: typeof EventRetransmissionMaximum; + public static EventRetransmissionMaximum: typeof EventRetransmissionMaximum /** * @group Characteristic Definitions */ - public static EventSnapshotsActive: typeof EventSnapshotsActive; + public static EventSnapshotsActive: typeof EventSnapshotsActive /** * @group Characteristic Definitions */ - public static EventTransmissionCounters: typeof EventTransmissionCounters; + public static EventTransmissionCounters: typeof EventTransmissionCounters /** * @group Characteristic Definitions */ - public static FilterChangeIndication: typeof FilterChangeIndication; + public static FilterChangeIndication: typeof FilterChangeIndication /** * @group Characteristic Definitions */ - public static FilterLifeLevel: typeof FilterLifeLevel; + public static FilterLifeLevel: typeof FilterLifeLevel /** * @group Characteristic Definitions */ - public static FirmwareRevision: typeof FirmwareRevision; + public static FirmwareRevision: typeof FirmwareRevision /** * @group Characteristic Definitions */ - public static FirmwareUpdateReadiness: typeof FirmwareUpdateReadiness; + public static FirmwareUpdateReadiness: typeof FirmwareUpdateReadiness /** * @group Characteristic Definitions */ - public static FirmwareUpdateStatus: typeof FirmwareUpdateStatus; + public static FirmwareUpdateStatus: typeof FirmwareUpdateStatus /** * @group Characteristic Definitions */ - public static HardwareFinish: typeof HardwareFinish; + public static HardwareFinish: typeof HardwareFinish /** * @group Characteristic Definitions */ - public static HardwareRevision: typeof HardwareRevision; + public static HardwareRevision: typeof HardwareRevision /** * @group Characteristic Definitions */ - public static HeartBeat: typeof HeartBeat; + public static HeartBeat: typeof HeartBeat /** * @group Characteristic Definitions */ - public static HeatingThresholdTemperature: typeof HeatingThresholdTemperature; + public static HeatingThresholdTemperature: typeof HeatingThresholdTemperature /** * @group Characteristic Definitions */ - public static HoldPosition: typeof HoldPosition; + public static HoldPosition: typeof HoldPosition /** * @group Characteristic Definitions */ - public static HomeKitCameraActive: typeof HomeKitCameraActive; + public static HomeKitCameraActive: typeof HomeKitCameraActive /** * @group Characteristic Definitions */ - public static Hue: typeof Hue; + public static Hue: typeof Hue /** * @group Characteristic Definitions */ - public static Identifier: typeof Identifier; + public static Identifier: typeof Identifier /** * @group Characteristic Definitions */ - public static Identify: typeof Identify; + public static Identify: typeof Identify /** * @group Characteristic Definitions */ - public static ImageMirroring: typeof ImageMirroring; + public static ImageMirroring: typeof ImageMirroring /** * @group Characteristic Definitions */ - public static ImageRotation: typeof ImageRotation; + public static ImageRotation: typeof ImageRotation /** * @group Characteristic Definitions */ - public static InputDeviceType: typeof InputDeviceType; + public static InputDeviceType: typeof InputDeviceType /** * @group Characteristic Definitions */ - public static InputSourceType: typeof InputSourceType; + public static InputSourceType: typeof InputSourceType /** * @group Characteristic Definitions */ - public static InUse: typeof InUse; + public static InUse: typeof InUse /** * @group Characteristic Definitions */ - public static IsConfigured: typeof IsConfigured; + public static IsConfigured: typeof IsConfigured /** * @group Characteristic Definitions */ - public static LeakDetected: typeof LeakDetected; + public static LeakDetected: typeof LeakDetected /** * @group Characteristic Definitions */ - public static ListPairings: typeof ListPairings; + public static ListPairings: typeof ListPairings /** * @group Characteristic Definitions */ - public static LockControlPoint: typeof LockControlPoint; + public static LockControlPoint: typeof LockControlPoint /** * @group Characteristic Definitions */ - public static LockCurrentState: typeof LockCurrentState; + public static LockCurrentState: typeof LockCurrentState /** * @group Characteristic Definitions */ - public static LockLastKnownAction: typeof LockLastKnownAction; + public static LockLastKnownAction: typeof LockLastKnownAction /** * @group Characteristic Definitions */ - public static LockManagementAutoSecurityTimeout: typeof LockManagementAutoSecurityTimeout; + public static LockManagementAutoSecurityTimeout: typeof LockManagementAutoSecurityTimeout /** * @group Characteristic Definitions */ - public static LockPhysicalControls: typeof LockPhysicalControls; + public static LockPhysicalControls: typeof LockPhysicalControls /** * @group Characteristic Definitions */ - public static LockTargetState: typeof LockTargetState; + public static LockTargetState: typeof LockTargetState /** * @group Characteristic Definitions */ - public static Logs: typeof Logs; + public static Logs: typeof Logs /** * @group Characteristic Definitions */ - public static MACRetransmissionMaximum: typeof MACRetransmissionMaximum; + public static MACRetransmissionMaximum: typeof MACRetransmissionMaximum /** * @group Characteristic Definitions */ - public static MACTransmissionCounters: typeof MACTransmissionCounters; + public static MACTransmissionCounters: typeof MACTransmissionCounters /** * @group Characteristic Definitions */ - public static ManagedNetworkEnable: typeof ManagedNetworkEnable; + public static ManagedNetworkEnable: typeof ManagedNetworkEnable /** * @group Characteristic Definitions */ - public static ManuallyDisabled: typeof ManuallyDisabled; + public static ManuallyDisabled: typeof ManuallyDisabled /** * @group Characteristic Definitions */ - public static Manufacturer: typeof Manufacturer; + public static Manufacturer: typeof Manufacturer /** * @group Characteristic Definitions */ - public static MatterFirmwareRevisionNumber: typeof MatterFirmwareRevisionNumber; + public static MatterFirmwareRevisionNumber: typeof MatterFirmwareRevisionNumber /** * @group Characteristic Definitions */ - public static MatterFirmwareUpdateStatus: typeof MatterFirmwareUpdateStatus; + public static MatterFirmwareUpdateStatus: typeof MatterFirmwareUpdateStatus /** * @group Characteristic Definitions */ - public static MaximumTransmitPower: typeof MaximumTransmitPower; + public static MaximumTransmitPower: typeof MaximumTransmitPower /** * @group Characteristic Definitions */ - public static MetricsBufferFullState: typeof MetricsBufferFullState; + public static MetricsBufferFullState: typeof MetricsBufferFullState /** * @group Characteristic Definitions */ - public static Model: typeof Model; + public static Model: typeof Model /** * @group Characteristic Definitions */ - public static MotionDetected: typeof MotionDetected; + public static MotionDetected: typeof MotionDetected /** * @group Characteristic Definitions */ - public static MultifunctionButton: typeof MultifunctionButton; + public static MultifunctionButton: typeof MultifunctionButton /** * @group Characteristic Definitions */ - public static Mute: typeof Mute; + public static Mute: typeof Mute /** * @group Characteristic Definitions */ - public static Name: typeof Name; + public static Name: typeof Name /** * @group Characteristic Definitions */ - public static NetworkAccessViolationControl: typeof NetworkAccessViolationControl; + public static NetworkAccessViolationControl: typeof NetworkAccessViolationControl /** * @group Characteristic Definitions */ - public static NetworkClientProfileControl: typeof NetworkClientProfileControl; + public static NetworkClientProfileControl: typeof NetworkClientProfileControl /** * @group Characteristic Definitions */ - public static NetworkClientStatusControl: typeof NetworkClientStatusControl; + public static NetworkClientStatusControl: typeof NetworkClientStatusControl /** * @group Characteristic Definitions */ - public static NFCAccessControlPoint: typeof NFCAccessControlPoint; + public static NFCAccessControlPoint: typeof NFCAccessControlPoint /** * @group Characteristic Definitions */ - public static NFCAccessSupportedConfiguration: typeof NFCAccessSupportedConfiguration; + public static NFCAccessSupportedConfiguration: typeof NFCAccessSupportedConfiguration /** * @group Characteristic Definitions */ - public static NightVision: typeof NightVision; + public static NightVision: typeof NightVision /** * @group Characteristic Definitions */ - public static NitrogenDioxideDensity: typeof NitrogenDioxideDensity; + public static NitrogenDioxideDensity: typeof NitrogenDioxideDensity /** * @group Characteristic Definitions */ - public static ObstructionDetected: typeof ObstructionDetected; + public static ObstructionDetected: typeof ObstructionDetected /** * @group Characteristic Definitions */ - public static OccupancyDetected: typeof OccupancyDetected; + public static OccupancyDetected: typeof OccupancyDetected /** * @group Characteristic Definitions */ - public static On: typeof On; + public static On: typeof On /** * @group Characteristic Definitions */ - public static OperatingStateResponse: typeof OperatingStateResponse; + public static OperatingStateResponse: typeof OperatingStateResponse /** * @group Characteristic Definitions */ - public static OpticalZoom: typeof OpticalZoom; + public static OpticalZoom: typeof OpticalZoom /** * @group Characteristic Definitions */ - public static OutletInUse: typeof OutletInUse; + public static OutletInUse: typeof OutletInUse /** * @group Characteristic Definitions */ - public static OzoneDensity: typeof OzoneDensity; + public static OzoneDensity: typeof OzoneDensity /** * @group Characteristic Definitions */ - public static PairingFeatures: typeof PairingFeatures; + public static PairingFeatures: typeof PairingFeatures /** * @group Characteristic Definitions */ - public static PairSetup: typeof PairSetup; + public static PairSetup: typeof PairSetup /** * @group Characteristic Definitions */ - public static PairVerify: typeof PairVerify; + public static PairVerify: typeof PairVerify /** * @group Characteristic Definitions */ - public static PasswordSetting: typeof PasswordSetting; + public static PasswordSetting: typeof PasswordSetting /** * @group Characteristic Definitions */ - public static PeriodicSnapshotsActive: typeof PeriodicSnapshotsActive; + public static PeriodicSnapshotsActive: typeof PeriodicSnapshotsActive /** * @group Characteristic Definitions */ - public static PictureMode: typeof PictureMode; + public static PictureMode: typeof PictureMode /** * @group Characteristic Definitions */ - public static Ping: typeof Ping; + public static Ping: typeof Ping /** * @group Characteristic Definitions */ - public static PM10Density: typeof PM10Density; + public static PM10Density: typeof PM10Density /** * @group Characteristic Definitions */ - public static PM2_5Density: typeof PM2_5Density; + public static PM2_5Density: typeof PM2_5Density /** * @group Characteristic Definitions */ - public static PositionState: typeof PositionState; + public static PositionState: typeof PositionState /** * @group Characteristic Definitions */ - public static PowerModeSelection: typeof PowerModeSelection; + public static PowerModeSelection: typeof PowerModeSelection /** * @group Characteristic Definitions */ - public static ProductData: typeof ProductData; + public static ProductData: typeof ProductData /** * @group Characteristic Definitions */ - public static ProgrammableSwitchEvent: typeof ProgrammableSwitchEvent; + public static ProgrammableSwitchEvent: typeof ProgrammableSwitchEvent /** * @group Characteristic Definitions */ - public static ProgrammableSwitchOutputState: typeof ProgrammableSwitchOutputState; + public static ProgrammableSwitchOutputState: typeof ProgrammableSwitchOutputState /** * @group Characteristic Definitions */ - public static ProgramMode: typeof ProgramMode; + public static ProgramMode: typeof ProgramMode /** * @group Characteristic Definitions */ - public static ReceivedSignalStrengthIndication: typeof ReceivedSignalStrengthIndication; + public static ReceivedSignalStrengthIndication: typeof ReceivedSignalStrengthIndication /** * @group Characteristic Definitions */ - public static ReceiverSensitivity: typeof ReceiverSensitivity; + public static ReceiverSensitivity: typeof ReceiverSensitivity /** * @group Characteristic Definitions */ - public static RecordingAudioActive: typeof RecordingAudioActive; + public static RecordingAudioActive: typeof RecordingAudioActive /** * @group Characteristic Definitions */ - public static RelativeHumidityDehumidifierThreshold: typeof RelativeHumidityDehumidifierThreshold; + public static RelativeHumidityDehumidifierThreshold: typeof RelativeHumidityDehumidifierThreshold /** * @group Characteristic Definitions */ - public static RelativeHumidityHumidifierThreshold: typeof RelativeHumidityHumidifierThreshold; + public static RelativeHumidityHumidifierThreshold: typeof RelativeHumidityHumidifierThreshold /** * @group Characteristic Definitions */ - public static RemainingDuration: typeof RemainingDuration; + public static RemainingDuration: typeof RemainingDuration /** * @group Characteristic Definitions */ - public static RemoteKey: typeof RemoteKey; + public static RemoteKey: typeof RemoteKey /** * @group Characteristic Definitions */ - public static ResetFilterIndication: typeof ResetFilterIndication; + public static ResetFilterIndication: typeof ResetFilterIndication /** * @group Characteristic Definitions */ - public static RotationDirection: typeof RotationDirection; + public static RotationDirection: typeof RotationDirection /** * @group Characteristic Definitions */ - public static RotationSpeed: typeof RotationSpeed; + public static RotationSpeed: typeof RotationSpeed /** * @group Characteristic Definitions */ - public static RouterStatus: typeof RouterStatus; + public static RouterStatus: typeof RouterStatus /** * @group Characteristic Definitions */ - public static Saturation: typeof Saturation; + public static Saturation: typeof Saturation /** * @group Characteristic Definitions */ - public static SecuritySystemAlarmType: typeof SecuritySystemAlarmType; + public static SecuritySystemAlarmType: typeof SecuritySystemAlarmType /** * @group Characteristic Definitions */ - public static SecuritySystemCurrentState: typeof SecuritySystemCurrentState; + public static SecuritySystemCurrentState: typeof SecuritySystemCurrentState /** * @group Characteristic Definitions */ - public static SecuritySystemTargetState: typeof SecuritySystemTargetState; + public static SecuritySystemTargetState: typeof SecuritySystemTargetState /** * @group Characteristic Definitions */ - public static SelectedAudioStreamConfiguration: typeof SelectedAudioStreamConfiguration; + public static SelectedAudioStreamConfiguration: typeof SelectedAudioStreamConfiguration /** * @group Characteristic Definitions */ - public static SelectedCameraRecordingConfiguration: typeof SelectedCameraRecordingConfiguration; + public static SelectedCameraRecordingConfiguration: typeof SelectedCameraRecordingConfiguration /** * @group Characteristic Definitions */ - public static SelectedDiagnosticsModes: typeof SelectedDiagnosticsModes; + public static SelectedDiagnosticsModes: typeof SelectedDiagnosticsModes /** * @group Characteristic Definitions */ - public static SelectedRTPStreamConfiguration: typeof SelectedRTPStreamConfiguration; + public static SelectedRTPStreamConfiguration: typeof SelectedRTPStreamConfiguration /** * @group Characteristic Definitions */ - public static SelectedSleepConfiguration: typeof SelectedSleepConfiguration; + public static SelectedSleepConfiguration: typeof SelectedSleepConfiguration /** * @group Characteristic Definitions */ - public static SerialNumber: typeof SerialNumber; + public static SerialNumber: typeof SerialNumber /** * @group Characteristic Definitions */ - public static ServiceLabelIndex: typeof ServiceLabelIndex; + public static ServiceLabelIndex: typeof ServiceLabelIndex /** * @group Characteristic Definitions */ - public static ServiceLabelNamespace: typeof ServiceLabelNamespace; + public static ServiceLabelNamespace: typeof ServiceLabelNamespace /** * @group Characteristic Definitions */ - public static SetDuration: typeof SetDuration; + public static SetDuration: typeof SetDuration /** * @group Characteristic Definitions */ - public static SetupDataStreamTransport: typeof SetupDataStreamTransport; + public static SetupDataStreamTransport: typeof SetupDataStreamTransport /** * @group Characteristic Definitions */ - public static SetupEndpoints: typeof SetupEndpoints; + public static SetupEndpoints: typeof SetupEndpoints /** * @group Characteristic Definitions */ - public static SetupTransferTransport: typeof SetupTransferTransport; + public static SetupTransferTransport: typeof SetupTransferTransport /** * @group Characteristic Definitions */ - public static SignalToNoiseRatio: typeof SignalToNoiseRatio; + public static SignalToNoiseRatio: typeof SignalToNoiseRatio /** * @group Characteristic Definitions */ - public static SiriEnable: typeof SiriEnable; + public static SiriEnable: typeof SiriEnable /** * @group Characteristic Definitions */ - public static SiriEndpointSessionStatus: typeof SiriEndpointSessionStatus; + public static SiriEndpointSessionStatus: typeof SiriEndpointSessionStatus /** * @group Characteristic Definitions */ - public static SiriEngineVersion: typeof SiriEngineVersion; + public static SiriEngineVersion: typeof SiriEngineVersion /** * @group Characteristic Definitions */ - public static SiriInputType: typeof SiriInputType; + public static SiriInputType: typeof SiriInputType /** * @group Characteristic Definitions */ - public static SiriLightOnUse: typeof SiriLightOnUse; + public static SiriLightOnUse: typeof SiriLightOnUse /** * @group Characteristic Definitions */ - public static SiriListening: typeof SiriListening; + public static SiriListening: typeof SiriListening /** * @group Characteristic Definitions */ - public static SiriTouchToUse: typeof SiriTouchToUse; + public static SiriTouchToUse: typeof SiriTouchToUse /** * @group Characteristic Definitions */ - public static SlatType: typeof SlatType; + public static SlatType: typeof SlatType /** * @group Characteristic Definitions */ - public static SleepDiscoveryMode: typeof SleepDiscoveryMode; + public static SleepDiscoveryMode: typeof SleepDiscoveryMode /** * @group Characteristic Definitions */ - public static SleepInterval: typeof SleepInterval; + public static SleepInterval: typeof SleepInterval /** * @group Characteristic Definitions */ - public static SmokeDetected: typeof SmokeDetected; + public static SmokeDetected: typeof SmokeDetected /** * @group Characteristic Definitions */ - public static SoftwareRevision: typeof SoftwareRevision; + public static SoftwareRevision: typeof SoftwareRevision /** * @group Characteristic Definitions */ - public static StagedFirmwareVersion: typeof StagedFirmwareVersion; + public static StagedFirmwareVersion: typeof StagedFirmwareVersion /** * @group Characteristic Definitions */ - public static StatusActive: typeof StatusActive; + public static StatusActive: typeof StatusActive /** * @group Characteristic Definitions */ - public static StatusFault: typeof StatusFault; + public static StatusFault: typeof StatusFault /** * @group Characteristic Definitions */ - public static StatusJammed: typeof StatusJammed; + public static StatusJammed: typeof StatusJammed /** * @group Characteristic Definitions */ - public static StatusLowBattery: typeof StatusLowBattery; + public static StatusLowBattery: typeof StatusLowBattery /** * @group Characteristic Definitions */ - public static StatusTampered: typeof StatusTampered; + public static StatusTampered: typeof StatusTampered /** * @group Characteristic Definitions */ - public static StreamingStatus: typeof StreamingStatus; + public static StreamingStatus: typeof StreamingStatus /** * @group Characteristic Definitions */ - public static SulphurDioxideDensity: typeof SulphurDioxideDensity; + public static SulphurDioxideDensity: typeof SulphurDioxideDensity /** * @group Characteristic Definitions */ - public static SupportedAssetTypes: typeof SupportedAssetTypes; + public static SupportedAssetTypes: typeof SupportedAssetTypes /** * @group Characteristic Definitions */ - public static SupportedAudioRecordingConfiguration: typeof SupportedAudioRecordingConfiguration; + public static SupportedAudioRecordingConfiguration: typeof SupportedAudioRecordingConfiguration /** * @group Characteristic Definitions */ - public static SupportedAudioStreamConfiguration: typeof SupportedAudioStreamConfiguration; + public static SupportedAudioStreamConfiguration: typeof SupportedAudioStreamConfiguration /** * @group Characteristic Definitions */ - public static SupportedCameraRecordingConfiguration: typeof SupportedCameraRecordingConfiguration; + public static SupportedCameraRecordingConfiguration: typeof SupportedCameraRecordingConfiguration /** * @group Characteristic Definitions */ - public static SupportedCharacteristicValueTransitionConfiguration: typeof SupportedCharacteristicValueTransitionConfiguration; + public static SupportedCharacteristicValueTransitionConfiguration: typeof SupportedCharacteristicValueTransitionConfiguration /** * @group Characteristic Definitions */ - public static SupportedDataStreamTransportConfiguration: typeof SupportedDataStreamTransportConfiguration; + public static SupportedDataStreamTransportConfiguration: typeof SupportedDataStreamTransportConfiguration /** * @group Characteristic Definitions */ - public static SupportedDiagnosticsModes: typeof SupportedDiagnosticsModes; + public static SupportedDiagnosticsModes: typeof SupportedDiagnosticsModes /** * @group Characteristic Definitions */ - public static SupportedDiagnosticsSnapshot: typeof SupportedDiagnosticsSnapshot; + public static SupportedDiagnosticsSnapshot: typeof SupportedDiagnosticsSnapshot /** * @group Characteristic Definitions */ - public static SupportedFirmwareUpdateConfiguration: typeof SupportedFirmwareUpdateConfiguration; + public static SupportedFirmwareUpdateConfiguration: typeof SupportedFirmwareUpdateConfiguration /** * @group Characteristic Definitions */ - public static SupportedMetrics: typeof SupportedMetrics; + public static SupportedMetrics: typeof SupportedMetrics /** * @group Characteristic Definitions */ - public static SupportedRouterConfiguration: typeof SupportedRouterConfiguration; + public static SupportedRouterConfiguration: typeof SupportedRouterConfiguration /** * @group Characteristic Definitions */ - public static SupportedRTPConfiguration: typeof SupportedRTPConfiguration; + public static SupportedRTPConfiguration: typeof SupportedRTPConfiguration /** * @group Characteristic Definitions */ - public static SupportedSleepConfiguration: typeof SupportedSleepConfiguration; + public static SupportedSleepConfiguration: typeof SupportedSleepConfiguration /** * @group Characteristic Definitions */ - public static SupportedTransferTransportConfiguration: typeof SupportedTransferTransportConfiguration; + public static SupportedTransferTransportConfiguration: typeof SupportedTransferTransportConfiguration /** * @group Characteristic Definitions */ - public static SupportedVideoRecordingConfiguration: typeof SupportedVideoRecordingConfiguration; + public static SupportedVideoRecordingConfiguration: typeof SupportedVideoRecordingConfiguration /** * @group Characteristic Definitions */ - public static SupportedVideoStreamConfiguration: typeof SupportedVideoStreamConfiguration; + public static SupportedVideoStreamConfiguration: typeof SupportedVideoStreamConfiguration /** * @group Characteristic Definitions */ - public static SwingMode: typeof SwingMode; + public static SwingMode: typeof SwingMode /** * @group Characteristic Definitions */ - public static TapType: typeof TapType; + public static TapType: typeof TapType /** * @group Characteristic Definitions */ - public static TargetAirPurifierState: typeof TargetAirPurifierState; + public static TargetAirPurifierState: typeof TargetAirPurifierState /** * @group Characteristic Definitions */ - public static TargetControlList: typeof TargetControlList; + public static TargetControlList: typeof TargetControlList /** * @group Characteristic Definitions */ - public static TargetControlSupportedConfiguration: typeof TargetControlSupportedConfiguration; + public static TargetControlSupportedConfiguration: typeof TargetControlSupportedConfiguration /** * @group Characteristic Definitions */ - public static TargetDoorState: typeof TargetDoorState; + public static TargetDoorState: typeof TargetDoorState /** * @group Characteristic Definitions */ - public static TargetFanState: typeof TargetFanState; + public static TargetFanState: typeof TargetFanState /** * @group Characteristic Definitions */ - public static TargetHeaterCoolerState: typeof TargetHeaterCoolerState; + public static TargetHeaterCoolerState: typeof TargetHeaterCoolerState /** * @group Characteristic Definitions */ - public static TargetHeatingCoolingState: typeof TargetHeatingCoolingState; + public static TargetHeatingCoolingState: typeof TargetHeatingCoolingState /** * @group Characteristic Definitions */ - public static TargetHorizontalTiltAngle: typeof TargetHorizontalTiltAngle; + public static TargetHorizontalTiltAngle: typeof TargetHorizontalTiltAngle /** * @group Characteristic Definitions */ - public static TargetHumidifierDehumidifierState: typeof TargetHumidifierDehumidifierState; + public static TargetHumidifierDehumidifierState: typeof TargetHumidifierDehumidifierState /** * @group Characteristic Definitions */ - public static TargetMediaState: typeof TargetMediaState; + public static TargetMediaState: typeof TargetMediaState /** * @group Characteristic Definitions */ - public static TargetPosition: typeof TargetPosition; + public static TargetPosition: typeof TargetPosition /** * @group Characteristic Definitions */ - public static TargetRelativeHumidity: typeof TargetRelativeHumidity; + public static TargetRelativeHumidity: typeof TargetRelativeHumidity /** * @group Characteristic Definitions */ - public static TargetTemperature: typeof TargetTemperature; + public static TargetTemperature: typeof TargetTemperature /** * @group Characteristic Definitions */ - public static TargetTiltAngle: typeof TargetTiltAngle; + public static TargetTiltAngle: typeof TargetTiltAngle /** * @group Characteristic Definitions */ - public static TargetVerticalTiltAngle: typeof TargetVerticalTiltAngle; + public static TargetVerticalTiltAngle: typeof TargetVerticalTiltAngle /** * @group Characteristic Definitions */ - public static TargetVisibilityState: typeof TargetVisibilityState; + public static TargetVisibilityState: typeof TargetVisibilityState /** * @group Characteristic Definitions */ - public static TemperatureDisplayUnits: typeof TemperatureDisplayUnits; + public static TemperatureDisplayUnits: typeof TemperatureDisplayUnits /** * @group Characteristic Definitions */ - public static ThirdPartyCameraActive: typeof ThirdPartyCameraActive; + public static ThirdPartyCameraActive: typeof ThirdPartyCameraActive /** * @group Characteristic Definitions */ - public static ThreadControlPoint: typeof ThreadControlPoint; + public static ThreadControlPoint: typeof ThreadControlPoint /** * @group Characteristic Definitions */ - public static ThreadNodeCapabilities: typeof ThreadNodeCapabilities; + public static ThreadNodeCapabilities: typeof ThreadNodeCapabilities /** * @group Characteristic Definitions */ - public static ThreadOpenThreadVersion: typeof ThreadOpenThreadVersion; + public static ThreadOpenThreadVersion: typeof ThreadOpenThreadVersion /** * @group Characteristic Definitions */ - public static ThreadStatus: typeof ThreadStatus; + public static ThreadStatus: typeof ThreadStatus /** * @group Characteristic Definitions */ - public static Token: typeof Token; + public static Token: typeof Token /** * @group Characteristic Definitions */ - public static TransmitPower: typeof TransmitPower; + public static TransmitPower: typeof TransmitPower /** * @group Characteristic Definitions */ - public static ValveType: typeof ValveType; + public static ValveType: typeof ValveType /** * @group Characteristic Definitions */ - public static Version: typeof Version; + public static Version: typeof Version /** * @group Characteristic Definitions */ - public static VideoAnalysisActive: typeof VideoAnalysisActive; + public static VideoAnalysisActive: typeof VideoAnalysisActive /** * @group Characteristic Definitions */ - public static VOCDensity: typeof VOCDensity; + public static VOCDensity: typeof VOCDensity /** * @group Characteristic Definitions */ - public static Volume: typeof Volume; + public static Volume: typeof Volume /** * @group Characteristic Definitions */ - public static VolumeControlType: typeof VolumeControlType; + public static VolumeControlType: typeof VolumeControlType /** * @group Characteristic Definitions */ - public static VolumeSelector: typeof VolumeSelector; + public static VolumeSelector: typeof VolumeSelector /** * @group Characteristic Definitions */ - public static WakeConfiguration: typeof WakeConfiguration; + public static WakeConfiguration: typeof WakeConfiguration /** * @group Characteristic Definitions */ - public static WANConfigurationList: typeof WANConfigurationList; + public static WANConfigurationList: typeof WANConfigurationList /** * @group Characteristic Definitions */ - public static WANStatusList: typeof WANStatusList; + public static WANStatusList: typeof WANStatusList /** * @group Characteristic Definitions */ - public static WaterLevel: typeof WaterLevel; + public static WaterLevel: typeof WaterLevel /** * @group Characteristic Definitions */ - public static WiFiCapabilities: typeof WiFiCapabilities; + public static WiFiCapabilities: typeof WiFiCapabilities /** * @group Characteristic Definitions */ - public static WiFiConfigurationControl: typeof WiFiConfigurationControl; + public static WiFiConfigurationControl: typeof WiFiConfigurationControl /** * @group Characteristic Definitions */ - public static WiFiSatelliteStatus: typeof WiFiSatelliteStatus; + public static WiFiSatelliteStatus: typeof WiFiSatelliteStatus // =-=-=-=-=-=-=-=-=-=-=-=-=-=-= // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions - public displayName: string; - public UUID: string; - iid: Nullable = null; - value: Nullable = null; + public displayName: string + public UUID: string + iid: Nullable = null + value: Nullable = null /** * @private */ - statusCode: HAPStatus = HAPStatus.SUCCESS; - props: CharacteristicProps; + statusCode: HAPStatus = HAPStatus.SUCCESS + props: CharacteristicProps /** * The {@link Characteristic.onGet} handler */ - private getHandler?: CharacteristicGetHandler; + private getHandler?: CharacteristicGetHandler /** * The {@link Characteristic.onSet} handler */ - private setHandler?: CharacteristicSetHandler; + private setHandler?: CharacteristicSetHandler - private subscriptions = 0; + private subscriptions = 0 /** * @private */ - additionalAuthorizationHandler?: AdditionalAuthorizationHandler; + additionalAuthorizationHandler?: AdditionalAuthorizationHandler public constructor(displayName: string, UUID: string, props: CharacteristicProps) { - super(); - this.displayName = displayName; - this.UUID = UUID; + super() + this.displayName = displayName + this.UUID = UUID this.props = { // some weird defaults (with legacy constructor props was optional) format: Formats.INT, perms: [Perms.NOTIFY], - }; + } - this.setProps(props || {}); // ensure sanity checks are called + this.setProps(props || {}) // ensure sanity checks are called } /** @@ -1680,20 +1684,20 @@ export class Characteristic extends EventEmitter { * @param handler */ public onGet(handler: CharacteristicGetHandler): Characteristic { - if (typeof handler !== "function") { - this.characteristicWarning(".onGet handler must be a function"); - return this; + if (typeof handler !== 'function') { + this.characteristicWarning('.onGet handler must be a function') + return this } - this.getHandler = handler; - return this; + this.getHandler = handler + return this } /** * Removes the {@link CharacteristicGetHandler} handler which was configured using {@link onGet}. */ public removeOnGet(): Characteristic { - this.getHandler = undefined; - return this; + this.getHandler = undefined + return this } /** @@ -1711,20 +1715,20 @@ export class Characteristic extends EventEmitter { * @param handler */ public onSet(handler: CharacteristicSetHandler): Characteristic { - if (typeof handler !== "function") { - this.characteristicWarning(".onSet handler must be a function"); - return this; + if (typeof handler !== 'function') { + this.characteristicWarning('.onSet handler must be a function') + return this } - this.setHandler = handler; - return this; + this.setHandler = handler + return this } /** * Removes the {@link CharacteristicSetHandler} which was configured using {@link onSet}. */ public removeOnSet(): Characteristic { - this.setHandler = undefined; - return this; + this.setHandler = undefined + return this } /** @@ -1735,177 +1739,176 @@ export class Characteristic extends EventEmitter { * @param props - Partial properties object with the desired updates. */ public setProps(props: PartialAllowingNull): Characteristic { - assert(props, "props cannot be undefined when setting props"); + assert(props, 'props cannot be undefined when setting props') // TODO calling setProps after publish doesn't lead to a increment in the current configuration number - let formatDidChange = false; + let formatDidChange = false // for every value "null" can be used to reset props, except for required props if (props.format) { - formatDidChange = this.props.format !== props.format; - this.props.format = props.format; + formatDidChange = this.props.format !== props.format + this.props.format = props.format } if (props.perms) { - assert(props.perms.length > 0, "characteristic prop perms cannot be empty array"); - this.props.perms = props.perms; + assert(props.perms.length > 0, 'characteristic prop perms cannot be empty array') + this.props.perms = props.perms } if (props.unit !== undefined) { - this.props.unit = props.unit != null? props.unit: undefined; + this.props.unit = props.unit != null ? props.unit : undefined } if (props.description !== undefined) { - this.props.description = props.description != null? props.description: undefined; + this.props.description = props.description != null ? props.description : undefined } // check minValue is valid for the format type if (props.minValue !== undefined) { if (props.minValue === null) { - props.minValue = undefined; + props.minValue = undefined } else if (!isNumericFormat(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'minValue' can only be set for characteristics with numeric format, but not for " + this.props.format, + `Characteristic Property 'minValue' can only be set for characteristics with numeric format, but not for ${this.props.format}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.minValue = undefined; - } else if (typeof (props.minValue as unknown) !== "number" || !Number.isFinite(props.minValue)) { + ) + props.minValue = undefined + } else if (typeof (props.minValue as unknown) !== 'number' || !Number.isFinite(props.minValue)) { this.characteristicWarning( `Characteristic Property 'minValue' must be a finite number, received "${props.minValue}" (${typeof props.minValue})`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.minValue = undefined; + ) + props.minValue = undefined } else { if (props.minValue < numericLowerBound(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'minValue' was set to " + props.minValue + ", but for numeric format " + - this.props.format + " minimum possible is " + numericLowerBound(this.props.format), + `Characteristic Property 'minValue' was set to ${props.minValue}, but for numeric format ${ + this.props.format} minimum possible is ${numericLowerBound(this.props.format)}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.minValue = numericLowerBound(this.props.format); + ) + props.minValue = numericLowerBound(this.props.format) } else if (props.minValue > numericUpperBound(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'minValue' was set to " + props.minValue + ", but for numeric format " + - this.props.format + " maximum possible is " + numericUpperBound(this.props.format), + `Characteristic Property 'minValue' was set to ${props.minValue}, but for numeric format ${ + this.props.format} maximum possible is ${numericUpperBound(this.props.format)}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.minValue = numericLowerBound(this.props.format); + ) + props.minValue = numericLowerBound(this.props.format) } } - this.props.minValue = props.minValue; + this.props.minValue = props.minValue } // check maxValue is valid for the format type if (props.maxValue !== undefined) { if (props.maxValue === null) { - props.maxValue = undefined; + props.maxValue = undefined } else if (!isNumericFormat(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'maxValue' can only be set for characteristics with numeric format, but not for " + this.props.format, + `Characteristic Property 'maxValue' can only be set for characteristics with numeric format, but not for ${this.props.format}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.maxValue = undefined; - } else if (typeof (props.maxValue as unknown) !== "number" || !Number.isFinite(props.maxValue)) { + ) + props.maxValue = undefined + } else if (typeof (props.maxValue as unknown) !== 'number' || !Number.isFinite(props.maxValue)) { this.characteristicWarning( `Characteristic Property 'maxValue' must be a finite number, received "${props.maxValue}" (${typeof props.maxValue})`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.maxValue = undefined; + ) + props.maxValue = undefined } else { if (props.maxValue > numericUpperBound(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'maxValue' was set to " + props.maxValue + ", but for numeric format " + - this.props.format + " maximum possible is " + numericUpperBound(this.props.format), + `Characteristic Property 'maxValue' was set to ${props.maxValue}, but for numeric format ${ + this.props.format} maximum possible is ${numericUpperBound(this.props.format)}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.maxValue = numericUpperBound(this.props.format); + ) + props.maxValue = numericUpperBound(this.props.format) } else if (props.maxValue < numericLowerBound(this.props.format)) { this.characteristicWarning( - "Characteristic Property 'maxValue' was set to " + props.maxValue + ", but for numeric format " + - this.props.format + " minimum possible is " + numericLowerBound(this.props.format), + `Characteristic Property 'maxValue' was set to ${props.maxValue}, but for numeric format ${ + this.props.format} minimum possible is ${numericLowerBound(this.props.format)}`, CharacteristicWarningType.ERROR_MESSAGE, - ); - props.maxValue = numericUpperBound(this.props.format); + ) + props.maxValue = numericUpperBound(this.props.format) } } - this.props.maxValue = props.maxValue; + this.props.maxValue = props.maxValue } if (props.minStep !== undefined) { if (props.minStep === null) { - this.props.minStep = undefined; + this.props.minStep = undefined } else if (!isNumericFormat(this.props.format)) { this.characteristicWarning( - "Characteristic Property `minStep` can only be set for characteristics with numeric format, but not for " + this.props.format, + `Characteristic Property \`minStep\` can only be set for characteristics with numeric format, but not for ${this.props.format}`, CharacteristicWarningType.ERROR_MESSAGE, - ); + ) } else { if (props.minStep < 1 && isIntegerNumericFormat(this.props.format)) { - this.characteristicWarning("Characteristic Property `minStep` was set to a value lower than 1, " + - "this will have no effect on format `" + this.props.format); + this.characteristicWarning(`Characteristic Property \`minStep\` was set to a value lower than 1, ` + + `this will have no effect on format \`${this.props.format}`) } - this.props.minStep = props.minStep; + this.props.minStep = props.minStep } } if (props.maxLen !== undefined) { if (props.maxLen === null) { - this.props.maxLen = undefined; + this.props.maxLen = undefined } else if (this.props.format !== Formats.STRING) { this.characteristicWarning( - "Characteristic Property `maxLen` can only be set for characteristics with format `STRING`, but not for " + this.props.format, + `Characteristic Property \`maxLen\` can only be set for characteristics with format \`STRING\`, but not for ${this.props.format}`, CharacteristicWarningType.ERROR_MESSAGE, - ); + ) } else { if (props.maxLen > 256) { - this.characteristicWarning("Characteristic Property string `maxLen` cannot be bigger than 256"); - props.maxLen = 256; + this.characteristicWarning('Characteristic Property string `maxLen` cannot be bigger than 256') + props.maxLen = 256 } - this.props.maxLen = props.maxLen; + this.props.maxLen = props.maxLen } } if (props.maxDataLen !== undefined) { if (props.maxDataLen === null) { - this.props.maxDataLen = undefined; + this.props.maxDataLen = undefined } else if (this.props.format !== Formats.DATA) { this.characteristicWarning( - "Characteristic Property `maxDataLen` can only be set for characteristics with format `DATA`, but not for " + this.props.format, + `Characteristic Property \`maxDataLen\` can only be set for characteristics with format \`DATA\`, but not for ${this.props.format}`, CharacteristicWarningType.ERROR_MESSAGE, - ); + ) } else { - this.props.maxDataLen = props.maxDataLen; + this.props.maxDataLen = props.maxDataLen } } if (props.validValues !== undefined) { if (props.validValues === null) { - this.props.validValues = undefined; + this.props.validValues = undefined } else if (!isNumericFormat(this.props.format)) { - this.characteristicWarning("Characteristic Property `validValues` was supplied for non numeric format " + this.props.format); + this.characteristicWarning(`Characteristic Property \`validValues\` was supplied for non numeric format ${this.props.format}`) } else { - assert(props.validValues.length, "characteristic prop validValues cannot be empty array"); - this.props.validValues = props.validValues; + assert(props.validValues.length, 'characteristic prop validValues cannot be empty array') + this.props.validValues = props.validValues } } if (props.validValueRanges !== undefined) { if (props.validValueRanges === null) { - this.props.validValueRanges = undefined; + this.props.validValueRanges = undefined } else if (!isNumericFormat(this.props.format)) { - this.characteristicWarning("Characteristic Property `validValueRanges` was supplied for non numeric format " + this.props.format); + this.characteristicWarning(`Characteristic Property \`validValueRanges\` was supplied for non numeric format ${this.props.format}`) } else { - assert(props.validValueRanges.length === 2, "characteristic prop validValueRanges must have a length of 2"); - this.props.validValueRanges = props.validValueRanges; + assert(props.validValueRanges.length === 2, 'characteristic prop validValueRanges must have a length of 2') + this.props.validValueRanges = props.validValueRanges } } if (props.adminOnlyAccess !== undefined) { - this.props.adminOnlyAccess = props.adminOnlyAccess != null? props.adminOnlyAccess: undefined; + this.props.adminOnlyAccess = props.adminOnlyAccess != null ? props.adminOnlyAccess : undefined } - if (this.props.minValue != null && this.props.maxValue != null) { // the eqeq instead of eqeqeq is important here if (this.props.minValue > this.props.maxValue) { // see https://github.com/homebridge/HAP-NodeJS/issues/690 - this.props.minValue = undefined; - this.props.maxValue = undefined; - throw new Error("Error setting CharacteristicsProps for '" + this.displayName + "': 'minValue' cannot be greater or equal the 'maxValue'!"); + this.props.minValue = undefined + this.props.maxValue = undefined + throw new Error(`Error setting CharacteristicsProps for '${this.displayName}': 'minValue' cannot be greater or equal the 'maxValue'!`) } } @@ -1922,15 +1925,15 @@ export class Characteristic extends EventEmitter { // - Special case for `ProgrammableSwitchEvent` where every change in value is considered an event which would result in ghost button presses // validateUserInput when called from setProps is intended to clamp value withing allowed range. It is why warnings should not be displayed. - const correctedValue = this.validateUserInput(this.value, CharacteristicWarningType.DEBUG_MESSAGE); + const correctedValue = this.validateUserInput(this.value, CharacteristicWarningType.DEBUG_MESSAGE) if (correctedValue !== this.value) { // we don't want to emit a CHANGE event if the value didn't change at all! - this.updateValue(correctedValue); + this.updateValue(correctedValue) } } - return this; + return this } /** @@ -1938,11 +1941,11 @@ export class Characteristic extends EventEmitter { * * The range of valid values can be defined using three different ways via the {@link CharacteristicProps} object * (set via the {@link setProps} method): - * * First method is to specifically list every valid value inside {@link CharacteristicProps.validValues} - * * Second you can specify a range via {@link CharacteristicProps.minValue} and {@link CharacteristicProps.maxValue} (with optionally defining + * First method is to specifically list every valid value inside {@link CharacteristicProps.validValues} + * Second you can specify a range via {@link CharacteristicProps.minValue} and {@link CharacteristicProps.maxValue} (with optionally defining * {@link CharacteristicProps.minStep}) - * * And lastly you can specify a range via {@link CharacteristicProps.validValueRanges} - * * Implicitly a valid value range is predefined for characteristics with Format {@link Formats.UINT8}, {@link Formats.UINT16}, + * And lastly you can specify a range via {@link CharacteristicProps.validValueRanges} + * Implicitly a valid value range is predefined for characteristics with Format {@link Formats.UINT8}, {@link Formats.UINT16}, * {@link Formats.UINT32} and {@link Formats.UINT64}: starting by zero to their respective maximum number * * The method will automatically detect which type of valid values definition is used and provide @@ -1962,10 +1965,9 @@ export class Characteristic extends EventEmitter { * ``` */ public validValuesIterator(): Iterable { - return new ValidValuesIterable(this.props); + return new ValidValuesIterable(this.props) } - // noinspection JSUnusedGlobalSymbols /** * This method can be used to set up additional authorization for a characteristic. * For one, it adds the {@link Perms.ADDITIONAL_AUTHORIZATION} permission to the characteristic @@ -1984,9 +1986,9 @@ export class Characteristic extends EventEmitter { */ public setupAdditionalAuthorization(handler: AdditionalAuthorizationHandler): void { if (!this.props.perms.includes(Perms.ADDITIONAL_AUTHORIZATION)) { - this.props.perms.push(Perms.ADDITIONAL_AUTHORIZATION); + this.props.perms.push(Perms.ADDITIONAL_AUTHORIZATION) } - this.additionalAuthorizationHandler = handler; + this.additionalAuthorizationHandler = handler } /** @@ -2022,10 +2024,10 @@ export class Characteristic extends EventEmitter { * @param error - The error object * * Note: Erroneous state is never **pushed** to the client side. Only, if the HomeKit client requests the current - * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, - * any {@link onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. + * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, + * any {@link onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. */ - setValue(error: HapStatusError | Error): Characteristic; + setValue(error: HapStatusError | Error): Characteristic /** * This updates the value by calling the {@link CharacteristicEventTypes.SET} event handler associated with the characteristic. * This acts the same way as when a HomeKit controller sends a `/characteristics` request to update the characteristic. @@ -2040,47 +2042,47 @@ export class Characteristic extends EventEmitter { * * Note: If you don't want the {@link CharacteristicEventTypes.SET} to be called, refer to {@link updateValue}. */ - setValue(value: CharacteristicValue, context?: CharacteristicContext): Characteristic; + setValue(value: CharacteristicValue, context?: CharacteristicContext): Characteristic setValue(value: CharacteristicValue | Error, callback?: CharacteristicSetCallback, context?: CharacteristicContext): Characteristic { if (value instanceof Error) { - this.statusCode = value instanceof HapStatusError? value.hapStatus: extractHAPStatusFromError(value); + this.statusCode = value instanceof HapStatusError ? value.hapStatus : extractHAPStatusFromError(value) if (callback) { - callback(); + callback() } - return this; + return this } - if (callback && !context && typeof callback !== "function") { - context = callback; - callback = undefined; + if (callback && !context && typeof callback !== 'function') { + context = callback + callback = undefined } try { - value = this.validateUserInput(value)!; + value = this.validateUserInput(value)! } catch (error) { - this.characteristicWarning(error?.message + "", CharacteristicWarningType.ERROR_MESSAGE, error?.stack); + this.characteristicWarning(`${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) if (callback) { - callback(error); + callback(error) } - return this; + return this } - this.handleSetRequest(value, undefined, context).then(value => { + this.handleSetRequest(value, undefined, context).then((value) => { if (callback) { if (value) { // possible write response - callback(null, value); + callback(null, value) } else { - callback(null); + callback(null) } } - }, reason => { + }, (reason) => { if (callback) { - callback(reason); + callback(reason) } - }); + }) - return this; + return this } /** @@ -2091,14 +2093,14 @@ export class Characteristic extends EventEmitter { * * Note: Refer to the respective overloads for {@link CharacteristicValue} or {@link HapStatusError} for respective documentation. */ - updateValue(value: Nullable | Error | HapStatusError): Characteristic; + updateValue(value: Nullable | Error | HapStatusError): Characteristic /** * This updates the value of the characteristic. If the value changed, an event notification will be sent to all connected * HomeKit controllers which are registered to receive event notifications for this characteristic. * * @param value - The new value. */ - updateValue(value: Nullable): Characteristic; + updateValue(value: Nullable): Characteristic /** * Sets the state of the characteristic to an errored state. * If a {@link onGet} or {@link CharacteristicEventTypes.GET} handler is set up, @@ -2115,10 +2117,10 @@ export class Characteristic extends EventEmitter { * @param error - The error object * * Note: Erroneous state is never **pushed** to the client side. Only, if the HomeKit client requests the current - * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, - * any {@link onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. + * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, + * any {@link onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. */ - updateValue(error: Error | HapStatusError): Characteristic; + updateValue(error: Error | HapStatusError): Characteristic /** * This updates the value of the characteristic. If the value changed, an event notification will be sent to all connected * HomeKit controllers which are registered to receive event notifications for this characteristic. @@ -2126,44 +2128,44 @@ export class Characteristic extends EventEmitter { * @param value - The new value. * @param context - Passed to the {@link CharacteristicEventTypes.CHANGE} event handler. */ - updateValue(value: Nullable, context?: CharacteristicContext): Characteristic; + updateValue(value: Nullable, context?: CharacteristicContext): Characteristic updateValue(value: Nullable | Error | HapStatusError, callback?: () => void, context?: CharacteristicContext): Characteristic { if (value instanceof Error) { - this.statusCode = value instanceof HapStatusError? value.hapStatus: extractHAPStatusFromError(value); + this.statusCode = value instanceof HapStatusError ? value.hapStatus : extractHAPStatusFromError(value) if (callback) { - callback(); + callback() } - return this; + return this } - if (callback && !context && typeof callback !== "function") { - context = callback; - callback = undefined; + if (callback && !context && typeof callback !== 'function') { + context = callback + callback = undefined } try { - value = this.validateUserInput(value); + value = this.validateUserInput(value) } catch (error) { - this.characteristicWarning(error?.message + "", CharacteristicWarningType.ERROR_MESSAGE, error?.stack); + this.characteristicWarning(`${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) if (callback) { - callback(); + callback() } - return this; + return this } - this.statusCode = HAPStatus.SUCCESS; + this.statusCode = HAPStatus.SUCCESS - const oldValue = this.value; - this.value = value; + const oldValue = this.value + this.value = value if (callback) { - callback(); + callback() } - this.emit(CharacteristicEventTypes.CHANGE, { originator: undefined, oldValue: oldValue, newValue: value, reason: ChangeReason.UPDATE, context: context }); + this.emit(CharacteristicEventTypes.CHANGE, { originator: undefined, oldValue, newValue: value, reason: ChangeReason.UPDATE, context }) - return this; // for chaining + return this // for chaining } /** @@ -2176,89 +2178,86 @@ export class Characteristic extends EventEmitter { * @param context - Passed to the {@link CharacteristicEventTypes.CHANGE} event handler. */ public sendEventNotification(value: CharacteristicValue, context?: CharacteristicContext): Characteristic { - this.statusCode = HAPStatus.SUCCESS; + this.statusCode = HAPStatus.SUCCESS - value = this.validateUserInput(value)!; - const oldValue = this.value; - this.value = value; + value = this.validateUserInput(value)! + const oldValue = this.value + this.value = value - this.emit(CharacteristicEventTypes.CHANGE, { originator: undefined, oldValue: oldValue, newValue: value, reason: ChangeReason.EVENT, context: context }); + this.emit(CharacteristicEventTypes.CHANGE, { originator: undefined, oldValue, newValue: value, reason: ChangeReason.EVENT, context }) - return this; // for chaining + return this // for chaining } /** * Called when a HAP requests wants to know the current value of the characteristic. * * @param connection - The HAP connection from which the request originated from. - * @param context - Deprecated parameter. There for backwards compatibility. - * @private Used by the Accessory to load the characteristic value + * @param context - Deprecated parameter, is there for backwards compatibility. + * @private */ async handleGetRequest(connection?: HAPConnection, context?: CharacteristicContext): Promise> { if (!this.props.perms.includes(Perms.PAIRED_READ)) { // check if we are allowed to read from this characteristic - throw HAPStatus.WRITE_ONLY_CHARACTERISTIC; + throw HAPStatus.WRITE_ONLY_CHARACTERISTIC } if (this.UUID === Characteristic.ProgrammableSwitchEvent.UUID) { // special workaround for event only programmable switch event, which must always return null - return null; + return null } if (this.getHandler) { if (this.listeners(CharacteristicEventTypes.GET).length > 0) { - this.characteristicWarning("Ignoring on('get') handler as onGet handler was defined instead"); + this.characteristicWarning('Ignoring on(\'get\') handler as onGet handler was defined instead') } try { - let value = await this.getHandler(context, connection); - this.statusCode = HAPStatus.SUCCESS; + let value = await this.getHandler(context, connection) + this.statusCode = HAPStatus.SUCCESS try { - value = this.validateUserInput(value); + value = this.validateUserInput(value) } catch (error) { - this.characteristicWarning(`An illegal value was supplied by the read handler for characteristic: ${error?.message}`, - CharacteristicWarningType.WARN_MESSAGE, error?.stack); - this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE; - return Promise.reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + this.characteristicWarning(`An illegal value was supplied by the read handler for characteristic: ${error?.message}`, CharacteristicWarningType.WARN_MESSAGE, error?.stack) + this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE + return Promise.reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - const oldValue = this.value; - this.value = value; + const oldValue = this.value + this.value = value if (oldValue !== value) { // emit a change event if necessary this.emit( CharacteristicEventTypes.CHANGE, - { originator: connection, oldValue: oldValue, newValue: value, reason: ChangeReason.READ, context: context }, - ); + { originator: connection, oldValue, newValue: value, reason: ChangeReason.READ, context }, + ) } - return value; + return value } catch (error) { - if (typeof error === "number") { - const hapStatusError = new HapStatusError(error); - this.statusCode = hapStatusError.hapStatus; + if (typeof error === 'number') { + const hapStatusError = new HapStatusError(error) + this.statusCode = hapStatusError.hapStatus } else if (error instanceof HapStatusError) { - this.statusCode = error.hapStatus; + this.statusCode = error.hapStatus } else { - this.characteristicWarning(`Unhandled error thrown inside read handler for characteristic: ${error?.message}`, - CharacteristicWarningType.ERROR_MESSAGE, error?.stack); - this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE; + this.characteristicWarning(`Unhandled error thrown inside read handler for characteristic: ${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) + this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE } - throw this.statusCode; + throw this.statusCode } } if (this.listeners(CharacteristicEventTypes.GET).length === 0) { if (this.statusCode) { - throw this.statusCode; + throw this.statusCode } try { - return this.validateUserInput(this.value); + return this.validateUserInput(this.value) } catch (error) { - this.characteristicWarning(`An illegal value was supplied by setting \`value\` for characteristic: ${error?.message}`, - CharacteristicWarningType.WARN_MESSAGE, error?.stack); - return Promise.reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + this.characteristicWarning(`An illegal value was supplied by setting \`value\` for characteristic: ${error?.message}`, CharacteristicWarningType.WARN_MESSAGE, error?.stack) + return Promise.reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } } @@ -2266,41 +2265,40 @@ export class Characteristic extends EventEmitter { try { this.emit(CharacteristicEventTypes.GET, once((status, value) => { if (status) { - if (typeof status === "number") { - const hapStatusError = new HapStatusError(status); - this.statusCode = hapStatusError.hapStatus; + if (typeof status === 'number') { + const hapStatusError = new HapStatusError(status) + this.statusCode = hapStatusError.hapStatus } else if (status instanceof HapStatusError) { - this.statusCode = status.hapStatus; + this.statusCode = status.hapStatus } else { - debug("[%s] Received error from get handler %s", this.displayName, status.stack); - this.statusCode = extractHAPStatusFromError(status); + debug('[%s] Received error from get handler %s', this.displayName, status.stack) + this.statusCode = extractHAPStatusFromError(status) } - reject(this.statusCode); - return; + reject(this.statusCode) + return } - this.statusCode = HAPStatus.SUCCESS; + this.statusCode = HAPStatus.SUCCESS - value = this.validateUserInput(value); - const oldValue = this.value; - this.value = value; + value = this.validateUserInput(value) + const oldValue = this.value + this.value = value - resolve(value); + resolve(value) if (oldValue !== value) { // emit a change event if necessary this.emit( CharacteristicEventTypes.CHANGE, - { originator: connection, oldValue: oldValue, newValue: value, reason: ChangeReason.READ, context: context }, - ); + { originator: connection, oldValue, newValue: value, reason: ChangeReason.READ, context }, + ) } - }), context, connection); + }), context, connection) } catch (error) { - this.characteristicWarning(`Unhandled error thrown inside read handler for characteristic: ${error?.message}`, - CharacteristicWarningType.ERROR_MESSAGE, error?.stack); - this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE; - reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + this.characteristicWarning(`Unhandled error thrown inside read handler for characteristic: ${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) + this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE + reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - }); + }) } /** @@ -2308,117 +2306,113 @@ export class Characteristic extends EventEmitter { * * @param value - The updated value * @param connection - The connection from which the request originated from - * @param context - Deprecated parameter. There for backwards compatibility. + * @param context - Deprecated parameter, is there for backwards compatibility. * @returns Promise resolve to void in normal operation. When characteristic supports write-response, HAP - * requests a write-response and the set handler returns a write-response value, the respective - * write response value is resolved. + * requests a write-response and the set handler returns a write-response value, the respective + * write response value is resolved. * @private */ async handleSetRequest(value: CharacteristicValue, connection?: HAPConnection, context?: CharacteristicContext): Promise { - this.statusCode = HAPStatus.SUCCESS; + this.statusCode = HAPStatus.SUCCESS if (connection !== undefined) { // if connection is undefined, the set "request" comes from the setValue method. // for setValue a value of "null" is allowed and checked via validateUserInput. try { - value = this.validateClientSuppliedValue(value); + value = this.validateClientSuppliedValue(value) } catch (e) { - debug(`[${this.displayName}]`, e.message); - return Promise.reject(HAPStatus.INVALID_VALUE_IN_REQUEST); + debug(`[${this.displayName}]`, e.message) + return Promise.reject(HAPStatus.INVALID_VALUE_IN_REQUEST) } } - const oldValue = this.value; + const oldValue = this.value if (this.setHandler) { if (this.listeners(CharacteristicEventTypes.SET).length > 0) { - this.characteristicWarning("Ignoring on('set') handler as onSet handler was defined instead"); + this.characteristicWarning('Ignoring on(\'set\') handler as onSet handler was defined instead') } try { - const writeResponse = await this.setHandler(value, context, connection); - this.statusCode = HAPStatus.SUCCESS; + const writeResponse = await this.setHandler(value, context, connection) + this.statusCode = HAPStatus.SUCCESS if (writeResponse != null && this.props.perms.includes(Perms.WRITE_RESPONSE)) { - this.value = this.validateUserInput(writeResponse); - return this.value!; + this.value = this.validateUserInput(writeResponse) + return this.value! } else { if (writeResponse != null) { - this.characteristicWarning("SET handler returned write response value, though the characteristic doesn't support write response", - CharacteristicWarningType.DEBUG_MESSAGE); + this.characteristicWarning('SET handler returned write response value, though the characteristic doesn\'t support write response', CharacteristicWarningType.DEBUG_MESSAGE) } - this.value = value; + this.value = value this.emit( CharacteristicEventTypes.CHANGE, - { originator: connection, oldValue: oldValue, newValue: value, reason: ChangeReason.WRITE, context: context }, - ); - return; + { originator: connection, oldValue, newValue: value, reason: ChangeReason.WRITE, context }, + ) + return } } catch (error) { - if (typeof error === "number") { - const hapStatusError = new HapStatusError(error); - this.statusCode = hapStatusError.hapStatus; + if (typeof error === 'number') { + const hapStatusError = new HapStatusError(error) + this.statusCode = hapStatusError.hapStatus } else if (error instanceof HapStatusError) { - this.statusCode = error.hapStatus; + this.statusCode = error.hapStatus } else { - this.characteristicWarning(`Unhandled error thrown inside write handler for characteristic: ${error?.message}`, - CharacteristicWarningType.ERROR_MESSAGE, error?.stack); - this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE; + this.characteristicWarning(`Unhandled error thrown inside write handler for characteristic: ${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) + this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE } - throw this.statusCode; + throw this.statusCode } } if (this.listeners(CharacteristicEventTypes.SET).length === 0) { - this.value = value; - this.emit(CharacteristicEventTypes.CHANGE, { originator: connection, oldValue: oldValue, newValue: value, reason: ChangeReason.WRITE, context: context }); - return Promise.resolve(); + this.value = value + this.emit(CharacteristicEventTypes.CHANGE, { originator: connection, oldValue, newValue: value, reason: ChangeReason.WRITE, context }) + return Promise.resolve() } else { return new Promise((resolve, reject) => { try { this.emit(CharacteristicEventTypes.SET, value, once((status, writeResponse) => { if (status) { - if (typeof status === "number") { - const hapStatusError = new HapStatusError(status); - this.statusCode = hapStatusError.hapStatus; + if (typeof status === 'number') { + const hapStatusError = new HapStatusError(status) + this.statusCode = hapStatusError.hapStatus } else if (status instanceof HapStatusError) { - this.statusCode = status.hapStatus; + this.statusCode = status.hapStatus } else { - debug("[%s] Received error from set handler %s", this.displayName, status.stack); - this.statusCode = extractHAPStatusFromError(status); + debug('[%s] Received error from set handler %s', this.displayName, status.stack) + this.statusCode = extractHAPStatusFromError(status) } - reject(this.statusCode); - return; + reject(this.statusCode) + return } - this.statusCode = HAPStatus.SUCCESS; + this.statusCode = HAPStatus.SUCCESS if (writeResponse != null && this.props.perms.includes(Perms.WRITE_RESPONSE)) { // support write response simply by letting the implementor pass the response as second argument to the callback - this.value = this.validateUserInput(writeResponse); - resolve(this.value!); + this.value = this.validateUserInput(writeResponse) + resolve(this.value!) } else { if (writeResponse != null) { - this.characteristicWarning("SET handler returned write response value, though the characteristic doesn't support write response", - CharacteristicWarningType.DEBUG_MESSAGE); + this.characteristicWarning('SET handler returned write response value, though the characteristic doesn\'t support write response', CharacteristicWarningType.DEBUG_MESSAGE) } - this.value = value; - resolve(); + this.value = value + resolve() this.emit( CharacteristicEventTypes.CHANGE, - { originator: connection, oldValue: oldValue, newValue: value, reason: ChangeReason.WRITE, context: context }, - ); + { originator: connection, oldValue, newValue: value, reason: ChangeReason.WRITE, context }, + ) } - }), context, connection); + }), context, connection) } catch (error) { - this.characteristicWarning(`Unhandled error thrown inside write handler for characteristic: ${error?.message}`, - CharacteristicWarningType.ERROR_MESSAGE, error?.stack); - this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE; - reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + this.characteristicWarning(`Unhandled error thrown inside write handler for characteristic: ${error?.message}`, CharacteristicWarningType.ERROR_MESSAGE, error?.stack) + this.statusCode = HAPStatus.SERVICE_COMMUNICATION_FAILURE + reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - }); + }) } } @@ -2428,9 +2422,9 @@ export class Characteristic extends EventEmitter { */ subscribe(): void { if (this.subscriptions === 0) { - this.emit(CharacteristicEventTypes.SUBSCRIBE); + this.emit(CharacteristicEventTypes.SUBSCRIBE) } - this.subscriptions++; + this.subscriptions++ } /** @@ -2439,57 +2433,56 @@ export class Characteristic extends EventEmitter { * @private */ unsubscribe(): void { - const wasOne = this.subscriptions === 1; - this.subscriptions--; - this.subscriptions = Math.max(this.subscriptions, 0); + const wasOne = this.subscriptions === 1 + this.subscriptions-- + this.subscriptions = Math.max(this.subscriptions, 0) if (wasOne) { - this.emit(CharacteristicEventTypes.UNSUBSCRIBE); + this.emit(CharacteristicEventTypes.UNSUBSCRIBE) } } protected getDefaultValue(): Nullable { - // noinspection JSDeprecatedSymbols switch (this.props.format) { - case Formats.BOOL: - return false; - case Formats.STRING: - switch (this.UUID) { - case Characteristic.Manufacturer.UUID: - return "Default-Manufacturer"; - case Characteristic.Model.UUID: - return "Default-Model"; - case Characteristic.SerialNumber.UUID: - return "Default-SerialNumber"; - case Characteristic.FirmwareRevision.UUID: - return "0.0.0"; - default: - return ""; - } - case Formats.DATA: - return ""; // who knows! - case Formats.TLV8: - return ""; // who knows! - case Formats.INT: - case Formats.FLOAT: - case Formats.UINT8: - case Formats.UINT16: - case Formats.UINT32: - case Formats.UINT64: - switch(this.UUID) { - case Characteristic.CurrentTemperature.UUID: - return 0; // some existing integrations expect this to be 0 by default - default: { - if (this.props.validValues?.length && typeof this.props.validValues[0] === "number") { - return this.props.validValues[0]; + case Formats.BOOL: + return false + case Formats.STRING: + switch (this.UUID) { + case Characteristic.Manufacturer.UUID: + return 'Default-Manufacturer' + case Characteristic.Model.UUID: + return 'Default-Model' + case Characteristic.SerialNumber.UUID: + return 'Default-SerialNumber' + case Characteristic.FirmwareRevision.UUID: + return '0.0.0' + default: + return '' } - if (typeof this.props.minValue === "number" && Number.isFinite(this.props.minValue)) { - return this.props.minValue; + case Formats.DATA: + return '' // who knows! + case Formats.TLV8: + return '' // who knows! + case Formats.INT: + case Formats.FLOAT: + case Formats.UINT8: + case Formats.UINT16: + case Formats.UINT32: + case Formats.UINT64: + switch (this.UUID) { + case Characteristic.CurrentTemperature.UUID: + return 0 // some existing integrations expect this to be 0 by default + default: { + if (this.props.validValues?.length && typeof this.props.validValues[0] === 'number') { + return this.props.validValues[0] + } + if (typeof this.props.minValue === 'number' && Number.isFinite(this.props.minValue)) { + return this.props.minValue + } + return 0 + } } - return 0; - } - } - default: - return 0; + default: + return 0 } } @@ -2501,96 +2494,96 @@ export class Characteristic extends EventEmitter { */ private validateClientSuppliedValue(value?: Nullable): CharacteristicValue { if (value == null) { - throw new Error(`Client supplied invalid value for ${this.props.format}: ${value}`); + throw new Error(`Client supplied invalid value for ${this.props.format}: ${value}`) } switch (this.props.format) { - case Formats.BOOL: { - if (typeof value === "boolean") { - return value; - } - - if (typeof value === "number" && (value === 1 || value === 0)) { - return Boolean(value); - } + case Formats.BOOL: { + if (typeof value === 'boolean') { + return value + } - throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`); - } - case Formats.INT: - case Formats.FLOAT: - case Formats.UINT8: - case Formats.UINT16: - case Formats.UINT32: - case Formats.UINT64: { - if (typeof value === "boolean") { - value = value ? 1 : 0; - } + if (typeof value === 'number' && (value === 1 || value === 0)) { + return Boolean(value) + } - if (typeof value !== "number" || !Number.isFinite(value)) { - throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`); + throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`) } + case Formats.INT: + case Formats.FLOAT: + case Formats.UINT8: + case Formats.UINT16: + case Formats.UINT32: + case Formats.UINT64: { + if (typeof value === 'boolean') { + value = value ? 1 : 0 + } - const numericMin = maxWithUndefined(this.props.minValue, numericLowerBound(this.props.format)); - const numericMax = minWithUndefined(this.props.maxValue, numericUpperBound(this.props.format)); + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new TypeError(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`) + } - if (typeof numericMin === "number" && value < numericMin) { - throw new Error(`Client supplied value of ${value} is less than the minimum allowed value of ${numericMin}`); - } + const numericMin = maxWithUndefined(this.props.minValue, numericLowerBound(this.props.format)) + const numericMax = minWithUndefined(this.props.maxValue, numericUpperBound(this.props.format)) - if (typeof numericMax === "number" && value > numericMax) { - throw new Error(`Client supplied value of ${value} is greater than the maximum allowed value of ${numericMax}`); - } + if (typeof numericMin === 'number' && value < numericMin) { + throw new Error(`Client supplied value of ${value} is less than the minimum allowed value of ${numericMin}`) + } - if (this.props.validValues && !this.props.validValues.includes(value)) { - throw new Error(`Client supplied value of ${value} is not in ${this.props.validValues.toString()}`); - } + if (typeof numericMax === 'number' && value > numericMax) { + throw new Error(`Client supplied value of ${value} is greater than the maximum allowed value of ${numericMax}`) + } - if (this.props.validValueRanges && this.props.validValueRanges.length === 2) { - if (value < this.props.validValueRanges[0]) { - throw new Error(`Client supplied value of ${value} is less than the minimum allowed value of ${this.props.validValueRanges[0]}`); + if (this.props.validValues && !this.props.validValues.includes(value)) { + throw new Error(`Client supplied value of ${value} is not in ${this.props.validValues.toString()}`) } - if (value > this.props.validValueRanges[1]) { - throw new Error(`Client supplied value of ${value} is greater than the maximum allowed value of ${this.props.validValueRanges[1]}`); + + if (this.props.validValueRanges && this.props.validValueRanges.length === 2) { + if (value < this.props.validValueRanges[0]) { + throw new Error(`Client supplied value of ${value} is less than the minimum allowed value of ${this.props.validValueRanges[0]}`) + } + if (value > this.props.validValueRanges[1]) { + throw new Error(`Client supplied value of ${value} is greater than the maximum allowed value of ${this.props.validValueRanges[1]}`) + } } - } - return value; - } - case Formats.STRING: { - if (typeof value !== "string") { - throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`); + return value } + case Formats.STRING: { + if (typeof value !== 'string') { + throw new TypeError(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`) + } - const maxLength = this.props.maxLen != null? this.props.maxLen: 64; // default is 64; max is 256 which is set in setProps - if (value.length > maxLength) { - throw new Error(`Client supplied value length of ${value.length} exceeds maximum length allowed of ${maxLength}`); - } + const maxLength = this.props.maxLen != null ? this.props.maxLen : 64 // default is 64; max is 256 which is set in setProps + if (value.length > maxLength) { + throw new Error(`Client supplied value length of ${value.length} exceeds maximum length allowed of ${maxLength}`) + } - return value; - } - case Formats.DATA: { - if (typeof value !== "string") { - throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`); + return value } + case Formats.DATA: { + if (typeof value !== 'string') { + throw new TypeError(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`) + } - // we don't validate base64 here + // we don't validate base64 here - const maxLength = this.props.maxDataLen != null? this.props.maxDataLen: 0x200000; // default is 0x200000 - if (value.length > maxLength) { - throw new Error(`Client supplied value length of ${value.length} exceeds maximum length allowed of ${maxLength}`); - } + const maxLength = this.props.maxDataLen != null ? this.props.maxDataLen : 0x200000 // default is 0x200000 + if (value.length > maxLength) { + throw new Error(`Client supplied value length of ${value.length} exceeds maximum length allowed of ${maxLength}`) + } - return value; - } - case Formats.TLV8: - if (typeof value !== "string") { - throw new Error(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`); + return value } + case Formats.TLV8: + if (typeof value !== 'string') { + throw new TypeError(`Client supplied invalid type for ${this.props.format}: "${value}" (${typeof value})`) + } - return value; + return value } - return value; + return value } /** @@ -2604,13 +2597,12 @@ export class Characteristic extends EventEmitter { private validateUserInput(value?: Nullable, warningType = CharacteristicWarningType.WARN_MESSAGE): Nullable { if (value === null) { if (this.UUID === Characteristic.Model.UUID || this.UUID === Characteristic.SerialNumber.UUID) { // mirrors the statement in case: Formats.STRING - this.characteristicWarning("characteristic must have a non null value otherwise HomeKit will reject this accessory, ignoring new value", - CharacteristicWarningType.ERROR_MESSAGE); - return this.value; // don't change the value + this.characteristicWarning('characteristic must have a non null value otherwise HomeKit will reject this accessory, ignoring new value', CharacteristicWarningType.ERROR_MESSAGE) + return this.value // don't change the value } if (this.props.format === Formats.DATA || this.props.format === Formats.TLV8) { - return value; // TLV8 and DATA formats are allowed to have null as a value + return value // TLV8 and DATA formats are allowed to have null as a value } /** @@ -2626,164 +2618,164 @@ export class Characteristic extends EventEmitter { if (this.UUID.endsWith(BASE_UUID)) { // we have an apple defined characteristic (at least assuming nobody else uses the UUID namespace) if (this.UUID === Characteristic.ProgrammableSwitchEvent.UUID) { - return value; // null is allowed as a value for ProgrammableSwitchEvent + return value // null is allowed as a value for ProgrammableSwitchEvent } - this.characteristicWarning("characteristic was supplied illegal value: null! Home App will reject null for Apple defined characteristics", - warningType); + this.characteristicWarning('characteristic was supplied illegal value: null! Home App will reject null for Apple defined characteristics', warningType) // if the value has been set previously, return it now, otherwise continue with validation to have a default value set. if (this.value !== null) { - return this.value; + return this.value } } else { // we currently allow null for any non-custom defined characteristics - return value; + return value } } switch (this.props.format) { - case Formats.BOOL: { - if (typeof value === "boolean") { - return value; - } - if (typeof value === "number") { - return value === 1; - } - if (typeof value === "string") { - return value === "1" || value === "true"; - } + case Formats.BOOL: { + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value === 1 + } + if (typeof value === 'string') { + return value === '1' || value === 'true' + } - this.characteristicWarning("characteristic value expected boolean and received " + typeof value, warningType); - return false; - } - case Formats.INT: - case Formats.FLOAT: - case Formats.UINT8: - case Formats.UINT16: - case Formats.UINT32: - case Formats.UINT64: { - if (typeof value === "boolean") { - value = value ? 1 : 0; - } - if (typeof value === "string") { - value = this.props.format === Formats.FLOAT ? parseFloat(value) : parseInt(value, 10); - } - if (typeof value !== "number" || !Number.isFinite(value)) { - this.characteristicWarning(`characteristic value expected valid finite number and received "${value}" (${typeof value})`, warningType); - value = typeof this.value === "number" ? this.value : this.props.minValue || 0; - } + this.characteristicWarning(`characteristic value expected boolean and received ${typeof value}`, warningType) + return false + } + case Formats.INT: + case Formats.FLOAT: + case Formats.UINT8: + case Formats.UINT16: + case Formats.UINT32: + case Formats.UINT64: { + if (typeof value === 'boolean') { + value = value ? 1 : 0 + } + if (typeof value === 'string') { + value = this.props.format === Formats.FLOAT ? Number.parseFloat(value) : Number.parseInt(value, 10) + } + if (typeof value !== 'number' || !Number.isFinite(value)) { + this.characteristicWarning(`characteristic value expected valid finite number and received "${value}" (${typeof value})`, warningType) + value = typeof this.value === 'number' ? this.value : this.props.minValue || 0 + } - const numericMin = maxWithUndefined(this.props.minValue, numericLowerBound(this.props.format)); - const numericMax = minWithUndefined(this.props.maxValue, numericUpperBound(this.props.format)); + const numericMin = maxWithUndefined(this.props.minValue, numericLowerBound(this.props.format)) + const numericMax = minWithUndefined(this.props.maxValue, numericUpperBound(this.props.format)) - let stepValue: number | undefined = undefined; - if (this.props.format === Formats.FLOAT) { - stepValue = this.props.minStep; - } else { - stepValue = maxWithUndefined(this.props.minStep, 1); - } + let stepValue: number | undefined + if (this.props.format === Formats.FLOAT) { + stepValue = this.props.minStep + } else { + stepValue = maxWithUndefined(this.props.minStep, 1) + } - if (stepValue != null && stepValue > 0) { - const minValue = this.props.minValue != null ? this.props.minValue : 0; - value = stepValue * Math.round((value - minValue) / stepValue) + minValue; - } + if (stepValue != null && stepValue > 0) { + const minValue = this.props.minValue != null ? this.props.minValue : 0 + value = stepValue * Math.round((value - minValue) / stepValue) + minValue + } - if (numericMin != null && value < numericMin) { - this.characteristicWarning(`characteristic was supplied illegal value: number ${value} exceeded minimum of ${numericMin}`, warningType); - value = numericMin; - } - if (numericMax != null && value > numericMax) { - this.characteristicWarning(`characteristic was supplied illegal value: number ${value} exceeded maximum of ${numericMax}`, warningType); - value = numericMax; - } + if (numericMin != null && value < numericMin) { + this.characteristicWarning(`characteristic was supplied illegal value: number ${value} exceeded minimum of ${numericMin}`, warningType) + value = numericMin + } + if (numericMax != null && value > numericMax) { + this.characteristicWarning(`characteristic was supplied illegal value: number ${value} exceeded maximum of ${numericMax}`, warningType) + value = numericMax + } - if (this.props.validValues && !this.props.validValues.includes(value)) { - this.characteristicWarning(`characteristic value ${value} is not contained in valid values array`, warningType); - return this.props.validValues.includes(this.value as number) ? this.value : (this.props.validValues[0] || 0); - } + if (this.props.validValues && !this.props.validValues.includes(value)) { + this.characteristicWarning(`characteristic value ${value} is not contained in valid values array`, warningType) + return this.props.validValues.includes(this.value as number) ? this.value : (this.props.validValues[0] || 0) + } - if (this.props.validValueRanges && this.props.validValueRanges.length === 2) { - if (value < this.props.validValueRanges[0]) { - this.characteristicWarning(`characteristic was supplied illegal value: number ${value} not contained in valid value range of ` - + `${this.props.validValueRanges}, supplying illegal values will throw errors in the future`, warningType); - value = this.props.validValueRanges[0]; - } else if (value > this.props.validValueRanges[1]) { - this.characteristicWarning(`characteristic was supplied illegal value: number ${value} not contained in valid value range of ` - + `${this.props.validValueRanges}, supplying illegal values will throw errors in the future`, warningType); - value = this.props.validValueRanges[1]; + if (this.props.validValueRanges && this.props.validValueRanges.length === 2) { + if (value < this.props.validValueRanges[0]) { + this.characteristicWarning(`characteristic was supplied illegal value: number ${value} not contained in valid value range of ` + + `${this.props.validValueRanges}, supplying illegal values will throw errors in the future`, warningType) + value = this.props.validValueRanges[0] + } else if (value > this.props.validValueRanges[1]) { + this.characteristicWarning(`characteristic was supplied illegal value: number ${value} not contained in valid value range of ` + + `${this.props.validValueRanges}, supplying illegal values will throw errors in the future`, warningType) + value = this.props.validValueRanges[1] + } } - } - return value; - } - case Formats.STRING: { - if (typeof value === "number") { - this.characteristicWarning("characteristic was supplied illegal value: number instead of string, " + - "supplying illegal values will throw errors in the future", warningType); - value = String(value); - } - if (typeof value !== "string") { - this.characteristicWarning("characteristic value expected string and received " + (typeof value), warningType); - value = typeof this.value === "string" ? this.value : value + ""; + return value } + case Formats.STRING: { + if (typeof value === 'number') { + this.characteristicWarning('characteristic was supplied illegal value: number instead of string, ' + + 'supplying illegal values will throw errors in the future', warningType) + value = String(value) + } + if (typeof value !== 'string') { + this.characteristicWarning(`characteristic value expected string and received ${typeof value}`, warningType) + value = typeof this.value === 'string' ? this.value : `${value}` + } - // mirrors the case value = null at the beginning - if (value.length <= 1 && (this.UUID === Characteristic.Model.UUID || this.UUID === Characteristic.SerialNumber.UUID)) { - this.characteristicWarning(`[${this.displayName}] characteristic must have a length of more than 1 character otherwise` - + ` HomeKit will reject this accessory, ignoring new value ${warningType}`); - return this.value; // just return the current value - } + // mirrors the case value = null at the beginning + if (value.length <= 1 && (this.UUID === Characteristic.Model.UUID || this.UUID === Characteristic.SerialNumber.UUID)) { + this.characteristicWarning(`[${this.displayName}] characteristic must have a length of more than 1 character otherwise` + + ` HomeKit will reject this accessory, ignoring new value ${warningType}`) + return this.value // just return the current value + } - const maxLength = this.props.maxLen ?? 64; // default is 64 (max is 256 which is set in setProps) - if (value.length > maxLength) { - this.characteristicWarning(`characteristic was supplied illegal value: string '${value}' exceeded max length of ${maxLength}`, warningType); - value = value.substring(0, maxLength); - } + const maxLength = this.props.maxLen ?? 64 // default is 64 (max is 256 which is set in setProps) + if (value.length > maxLength) { + this.characteristicWarning(`characteristic was supplied illegal value: string '${value}' exceeded max length of ${maxLength}`, warningType) + value = value.substring(0, maxLength) + } - if (value.length > 0 && this.UUID === Characteristic.ConfiguredName.UUID) { - checkName(this.displayName, "ConfiguredName", value); - } + if (value.length > 0 && this.UUID === Characteristic.ConfiguredName.UUID) { + checkName(this.displayName, 'ConfiguredName', value) + } - return value; - } - case Formats.DATA: - if (typeof value !== "string") { - throw new Error("characteristic with DATA format must have string value"); + return value } + case Formats.DATA: + if (typeof value !== 'string') { + throw new TypeError('characteristic with DATA format must have string value') + } - if (this.props.maxDataLen != null && value.length > this.props.maxDataLen) { - // can't cut it as we would basically set binary rubbish afterwards - throw new Error("characteristic with DATA format exceeds specified maxDataLen"); - } - return value; - case Formats.TLV8: - if (value === undefined) { - this.characteristicWarning("characteristic was supplied illegal value: undefined", warningType); - return this.value; - } - return value; // we trust that this is valid tlv8 + if (this.props.maxDataLen != null && value.length > this.props.maxDataLen) { + // can't cut it as we would basically set binary rubbish afterward + throw new Error('characteristic with DATA format exceeds specified maxDataLen') + } + return value + case Formats.TLV8: + if (value === undefined) { + this.characteristicWarning('characteristic was supplied illegal value: undefined', warningType) + return this.value + } + return value // we trust that this is valid tlv8 } // hopefully it shouldn't get to this point if (value === undefined) { - this.characteristicWarning("characteristic was supplied illegal value: undefined", CharacteristicWarningType.ERROR_MESSAGE); - return this.value; + this.characteristicWarning('characteristic was supplied illegal value: undefined', CharacteristicWarningType.ERROR_MESSAGE) + return this.value } - return value; + return value } /** - * @private used to assign iid to characteristic + * @private */ _assignID(identifierCache: IdentifierCache, accessoryName: string, serviceUUID: string, serviceSubtype?: string): void { // generate our IID based on our UUID - this.iid = identifierCache.getIID(accessoryName, serviceUUID, serviceSubtype, this.UUID); + this.iid = identifierCache.getIID(accessoryName, serviceUUID, serviceSubtype, this.UUID) } + // eslint-disable-next-line unicorn/error-message private characteristicWarning(message: string, type = CharacteristicWarningType.WARN_MESSAGE, stack = new Error().stack): void { - this.emit(CharacteristicEventTypes.CHARACTERISTIC_WARNING, type, message, stack); + this.emit(CharacteristicEventTypes.CHARACTERISTIC_WARNING, type, message, stack) } /** @@ -2792,10 +2784,10 @@ export class Characteristic extends EventEmitter { */ removeAllListeners(event?: string | symbol): this { if (!event) { - this.removeOnGet(); - this.removeOnSet(); + this.removeOnGet() + this.removeOnSet() } - return super.removeAllListeners(event); + return super.removeAllListeners(event) } /** @@ -2803,137 +2795,138 @@ export class Characteristic extends EventEmitter { * @private */ replaceBy(characteristic: Characteristic): void { - this.props = characteristic.props; - this.updateValue(characteristic.value); + this.props = characteristic.props + this.updateValue(characteristic.value) - const getListeners = characteristic.listeners(CharacteristicEventTypes.GET); + const getListeners = characteristic.listeners(CharacteristicEventTypes.GET) if (getListeners.length) { // the callback can only be called once, so we remove all old listeners - this.removeAllListeners(CharacteristicEventTypes.GET); + this.removeAllListeners(CharacteristicEventTypes.GET) // @ts-expect-error: force type - getListeners.forEach(listener => this.addListener(CharacteristicEventTypes.GET, listener)); + getListeners.forEach(listener => this.addListener(CharacteristicEventTypes.GET, listener)) } - this.removeOnGet(); + this.removeOnGet() if (characteristic.getHandler) { - this.onGet(characteristic.getHandler); + this.onGet(characteristic.getHandler) } - const setListeners = characteristic.listeners(CharacteristicEventTypes.SET); + const setListeners = characteristic.listeners(CharacteristicEventTypes.SET) if (setListeners.length) { // the callback can only be called once, so we remove all old listeners - this.removeAllListeners(CharacteristicEventTypes.SET); + this.removeAllListeners(CharacteristicEventTypes.SET) // @ts-expect-error: force type - setListeners.forEach(listener => this.addListener(CharacteristicEventTypes.SET, listener)); + setListeners.forEach(listener => this.addListener(CharacteristicEventTypes.SET, listener)) } - this.removeOnSet(); + this.removeOnSet() if (characteristic.setHandler) { - this.onSet(characteristic.setHandler); + this.onSet(characteristic.setHandler) } } /** * Returns a JSON representation of this characteristic suitable for delivering to HAP clients. - * @private used to generate response to /accessories query + * @private */ async toHAP(connection: HAPConnection, contactGetHandlers = true): Promise { - const object = this.internalHAPRepresentation(); + const object = this.internalHAPRepresentation() if (!this.props.perms.includes(Perms.PAIRED_READ)) { - object.value = undefined; + object.value = undefined } else if (this.UUID === Characteristic.ProgrammableSwitchEvent.UUID) { // special workaround for event only programmable switch event, which must always return null - object.value = null; + object.value = null } else { // query the current value const value = contactGetHandlers ? await this.handleGetRequest(connection).catch(() => { - const value = this.getDefaultValue(); - debug("[%s] Error getting value for characteristic on /accessories request. Returning default value instead: %s", this.displayName, `${value}`); - return value; // use default value + const value = this.getDefaultValue() + debug('[%s] Error getting value for characteristic on /accessories request. Returning default value instead: %s', this.displayName, `${value}`) + return value // use default value }) - : this.value; + : this.value - object.value = formatOutgoingCharacteristicValue(value, this.props); + object.value = formatOutgoingCharacteristicValue(value, this.props) } - return object; + return object } /** * Returns a JSON representation of this characteristic without the value. - * @private used to generate the config hash + * @private */ internalHAPRepresentation(): CharacteristicJsonObject { - assert(this.iid,"iid cannot be undefined for characteristic '" + this.displayName + "'"); + assert(this.iid, `iid cannot be undefined for characteristic '${this.displayName}'`) // TODO include the value for characteristics of the AccessoryInformation service return { - type: toShortForm(this.UUID), - iid: this.iid!, - value: null, - perms: this.props.perms, - description: this.props.description || this.displayName, - format: this.props.format, - unit: this.props.unit, - minValue: this.props.minValue, - maxValue: this.props.maxValue, - minStep: this.props.minStep, - maxLen: this.props.maxLen, - maxDataLen: this.props.maxDataLen, - "valid-values": this.props.validValues, - "valid-values-range": this.props.validValueRanges, - }; + 'type': toShortForm(this.UUID), + 'iid': this.iid!, + 'value': null, + 'perms': this.props.perms, + 'description': this.props.description || this.displayName, + 'format': this.props.format, + 'unit': this.props.unit, + 'minValue': this.props.minValue, + 'maxValue': this.props.maxValue, + 'minStep': this.props.minStep, + 'maxLen': this.props.maxLen, + 'maxDataLen': this.props.maxDataLen, + 'valid-values': this.props.validValues, + 'valid-values-range': this.props.validValueRanges, + } } /** * Serialize characteristic into json string. * * @param characteristic - Characteristic object. - * @private used to store characteristic on disk + * @private */ static serialize(characteristic: Characteristic): SerializedCharacteristic { - let constructorName: string | undefined; - if (characteristic.constructor.name !== "Characteristic") { - constructorName = characteristic.constructor.name; + let constructorName: string | undefined + if (characteristic.constructor.name !== 'Characteristic') { + constructorName = characteristic.constructor.name } return { displayName: characteristic.displayName, UUID: characteristic.UUID, eventOnlyCharacteristic: characteristic.UUID === Characteristic.ProgrammableSwitchEvent.UUID, // support downgrades for now - constructorName: constructorName, + constructorName, value: characteristic.value, props: clone({}, characteristic.props), - }; + } } /** * Deserialize characteristic from json string. * * @param json - Json string representing a characteristic. - * @private used to recreate characteristic from disk + * @private */ static deserialize(json: SerializedCharacteristic): Characteristic { - let characteristic: Characteristic; + let characteristic: Characteristic if (json.constructorName && json.constructorName.charAt(0).toUpperCase() === json.constructorName.charAt(0) && Characteristic[json.constructorName as keyof (typeof Characteristic)]) { // MUST start with uppercase character and must exist on Characteristic object - const constructor = Characteristic[json.constructorName as keyof (typeof Characteristic)] as { new(): Characteristic }; - characteristic = new constructor(); - characteristic.displayName = json.displayName; - characteristic.setProps(json.props); + const constructor = Characteristic[json.constructorName as keyof (typeof Characteristic)] as { new(): Characteristic } + characteristic = new constructor() + characteristic.displayName = json.displayName + characteristic.setProps(json.props) } else { - characteristic = new Characteristic(json.displayName, json.UUID, json.props); + characteristic = new Characteristic(json.displayName, json.UUID, json.props) } - characteristic.value = json.value; + characteristic.value = json.value - return characteristic; + return characteristic } - } // We have a cyclic dependency problem. Within this file we have the definitions of "./definitions" as // type imports only (in order to define the static properties). Setting those properties is done outside // this file, within the definition files. Therefore, we import it at the end of this file. Seems weird, but is important. -import "./definitions/CharacteristicDefinitions"; +(async () => { + await import('./definitions/CharacteristicDefinitions.js') +})() diff --git a/src/lib/HAPServer.spec.ts b/src/lib/HAPServer.spec.ts index eb0b5d65c..c6923d772 100644 --- a/src/lib/HAPServer.spec.ts +++ b/src/lib/HAPServer.spec.ts @@ -1,287 +1,298 @@ -import axios, { AxiosError, AxiosResponse } from "axios"; -import crypto from "crypto"; -import { Agent } from "http"; -import tweetnacl from "tweetnacl"; -import { PairingStates, TLVValues } from "../internal-types"; -import { HAPHTTPClient } from "../test-utils/HAPHTTPClient"; -import { PairSetupClient } from "../test-utils/PairSetupClient"; -import { PairVerifyClient } from "../test-utils/PairVerifyClient"; -import { +import type { AxiosResponse } from 'axios' + +import type { AccessoriesResponse, CharacteristicId, CharacteristicReadData, CharacteristicsWriteRequest, CharacteristicsWriteResponse, ResourceRequest, - ResourceRequestType, -} from "../types"; -import { Accessory } from "./Accessory"; -import { Characteristic, Formats, Perms } from "./Characteristic"; +} from '../types' +import type { IdentifyCallback } from './HAPServer' +import type { PairingInformation } from './model/AccessoryInfo' +import type { HAPConnection, HAPEncryption } from './util/eventedhttp' + +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' +import { Agent } from 'node:http' + +import axios, { AxiosError } from 'axios' +import tweetnacl from 'tweetnacl' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { PairingStates, TLVValues } from '../internal-types.js' +import { HAPHTTPClient } from '../test-utils/HAPHTTPClient.js' +import { PairSetupClient } from '../test-utils/PairSetupClient.js' +import { PairVerifyClient } from '../test-utils/PairVerifyClient.js' +import { ResourceRequestType } from '../types.js' +import { Accessory } from './Accessory.js' +import { Characteristic, Formats, Perms } from './Characteristic.js' import { HAPHTTPCode, HAPPairingHTTPCode, HAPServer, HAPServerEventTypes, HAPStatus, - IdentifyCallback, - IsKnownHAPStatusError, + isKnownHAPStatusError, TLVErrorCode, -} from "./HAPServer"; -import { AccessoryInfo, PairingInformation, PermissionTypes } from "./model/AccessoryInfo"; -import { Service } from "./Service"; -import { HAPConnection, HAPEncryption } from "./util/eventedhttp"; -import { awaitEventOnce, PromiseTimeout } from "./util/promise-utils"; -import * as tlv from "./util/tlv"; - -describe("HAPServer", () => { - const serverUsername = "AA:AA:AA:AA:AA:AA"; - const clientUsername = "BB:BB:BB:BB:BB:BB"; - const thirdUsername = "CC:CC:CC:CC:CC:CC"; - - let accessoryInfoUnpaired: AccessoryInfo; +} from './HAPServer.js' +import { AccessoryInfo, PermissionTypes } from './model/AccessoryInfo.js' +import { Service } from './Service.js' +import { awaitEventOnce, PromiseTimeout } from './util/promise-utils.js' +import { decode } from './util/tlv.js' + +describe('hAPServer', () => { + const serverUsername = 'AA:AA:AA:AA:AA:AA' + const clientUsername = 'BB:BB:BB:BB:BB:BB' + const thirdUsername = 'CC:CC:CC:CC:CC:CC' + + let accessoryInfoUnpaired: AccessoryInfo const serverInfoUnpaired = { username: serverUsername, publicKey: Buffer.alloc(0), - }; + } - let accessoryInfoPaired: AccessoryInfo; + let accessoryInfoPaired: AccessoryInfo const serverInfoPaired = { username: serverUsername, publicKey: Buffer.alloc(0), - }; + } const clientInfo = { username: clientUsername, privateKey: Buffer.alloc(0), publicKey: Buffer.alloc(0), - }; + } - let httpAgent: Agent; - let server: HAPServer; + let httpAgent: Agent + let server: HAPServer - async function bindServer(server: HAPServer, port = 0, host = "localhost"): Promise<[port: number, string: string]> { - const listenPromise: Promise<[number, string]> = awaitEventOnce(server, HAPServerEventTypes.LISTENING); - server.listen(port, host); - return await listenPromise; + async function bindServer(server: HAPServer, port = 0, host = 'localhost'): Promise<[port: number, string: string]> { + const listenPromise: Promise<[number, string]> = awaitEventOnce(server, HAPServerEventTypes.LISTENING) + server.listen(port, host) + return await listenPromise } beforeEach(() => { - accessoryInfoUnpaired = AccessoryInfo.create(serverUsername); + accessoryInfoUnpaired = AccessoryInfo.create(serverUsername) // @ts-expect-error: private access - accessoryInfoUnpaired.setupID = Accessory._generateSetupID(); - accessoryInfoUnpaired.displayName = "Outlet"; - accessoryInfoUnpaired.category = 7; - accessoryInfoUnpaired.pincode = " 031-45-154"; - serverInfoUnpaired.publicKey = accessoryInfoUnpaired.signPk; + accessoryInfoUnpaired.setupID = Accessory._generateSetupID() + accessoryInfoUnpaired.displayName = 'Outlet' + accessoryInfoUnpaired.category = 7 + accessoryInfoUnpaired.pincode = ' 031-45-154' + serverInfoUnpaired.publicKey = accessoryInfoUnpaired.signPk - accessoryInfoPaired = AccessoryInfo.create(serverUsername); + accessoryInfoPaired = AccessoryInfo.create(serverUsername) // @ts-expect-error: private access - accessoryInfoPaired.setupID = Accessory._generateSetupID(); - accessoryInfoPaired.displayName = "Outlet"; - accessoryInfoPaired.category = 7; - accessoryInfoPaired.pincode = " 031-45-154"; - serverInfoPaired.publicKey = accessoryInfoPaired.signPk; + accessoryInfoPaired.setupID = Accessory._generateSetupID() + accessoryInfoPaired.displayName = 'Outlet' + accessoryInfoPaired.category = 7 + accessoryInfoPaired.pincode = ' 031-45-154' + serverInfoPaired.publicKey = accessoryInfoPaired.signPk - const clientKeyPair = tweetnacl.sign.keyPair(); - clientInfo.privateKey = Buffer.from(clientKeyPair.secretKey); - clientInfo.publicKey = Buffer.from(clientKeyPair.publicKey); - accessoryInfoPaired.addPairedClient(clientUsername, clientInfo.publicKey, PermissionTypes.ADMIN); + const clientKeyPair = tweetnacl.sign.keyPair() + clientInfo.privateKey = Buffer.from(clientKeyPair.secretKey) + clientInfo.publicKey = Buffer.from(clientKeyPair.publicKey) + accessoryInfoPaired.addPairedClient(clientUsername, clientInfo.publicKey, PermissionTypes.ADMIN) // used to do long living http connections without own tcp interface httpAgent = new Agent({ keepAlive: true, - }); - }); + }) + }) afterEach(() => { - server?.stop(); - server?.destroy(); - }); + server?.stop() + server?.destroy() + }) - test("simple unpaired identify", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('simple unpaired identify', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) - const promise: Promise = awaitEventOnce(server, HAPServerEventTypes.IDENTIFY); + const promise: Promise = awaitEventOnce(server, HAPServerEventTypes.IDENTIFY) - const request: Promise> = axios.post(`http://localhost:${port}/identify`, { httpAgent }); + const request: Promise> = axios.post(`http://localhost:${port}/identify`, { httpAgent }) - const callback = await promise; - callback(); // signal successful identify! + const callback = await promise + callback() // signal successful identify! - const response = await request; - expect(response.data).toBeFalsy(); - }); + const response = await request + expect(response.data).toBeFalsy() + }) - test("reject unpaired identify on paired server", async () => { - server = new HAPServer(accessoryInfoPaired); - const [port] = await bindServer(server); + it('reject unpaired identify on paired server', async () => { + server = new HAPServer(accessoryInfoPaired) + const [port] = await bindServer(server) - try { - const response = await axios.post(`http://localhost:${port}/identify`, { httpAgent }); - fail(`Expected erroneous response, got ${response}`); - } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPPairingHTTPCode.BAD_REQUEST); - expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES }); - } - }); + await expect(axios.post(`http://localhost:${port}/identify`, { httpAgent })).rejects.toThrow(AxiosError) + await expect(axios.post(`http://localhost:${port}/identify`, { httpAgent })).rejects.toMatchObject({ + response: { + status: HAPPairingHTTPCode.BAD_REQUEST, + data: { status: HAPStatus.INSUFFICIENT_PRIVILEGES }, + }, + }) + }) - test("test-utils successful /pair-setup", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('test-utils successful /pair-setup', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) server.on(HAPServerEventTypes.PAIR, (username, clientLTPK, callback) => { - expect(username).toEqual(clientUsername); - expect(clientLTPK).toEqual(clientInfo.publicKey); - callback(); - }); + expect(username).toEqual(clientUsername) + expect(clientLTPK).toEqual(clientInfo.publicKey) + callback() + }) - const pairSetup = new PairSetupClient(port, httpAgent); + const pairSetup = new PairSetupClient(port, httpAgent) - const M6 = await pairSetup.sendPairSetup(accessoryInfoPaired.pincode, clientInfo); + const M6 = await pairSetup.sendPairSetup(accessoryInfoPaired.pincode, clientInfo) - expect(M6.accessoryIdentifier).toEqual(serverUsername); - expect(M6.accessoryLTPK).toEqual(accessoryInfoUnpaired.signPk); - }); + expect(M6.accessoryIdentifier).toEqual(serverUsername) + expect(M6.accessoryLTPK).toEqual(accessoryInfoUnpaired.signPk) + }) - test("reject pair-setup after already paired", async () => { - server = new HAPServer(accessoryInfoPaired); - const [port] = await bindServer(server); + it('reject pair-setup after already paired', async () => { + server = new HAPServer(accessoryInfoPaired) + const [port] = await bindServer(server) - const pairSetup = new PairSetupClient(port, httpAgent); + const pairSetup = new PairSetupClient(port, httpAgent) - const response = await pairSetup.sendM1(); - const objectsM2 = tlv.decode(response.data); - expect(objectsM2[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2); - expect(objectsM2[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNAVAILABLE); - }); + const response = await pairSetup.sendM1() + const objectsM2 = decode(response.data) + expect(objectsM2[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2) + expect(objectsM2[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNAVAILABLE) + }) - test("reject pair-setup with state M3", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('reject pair-setup with state M3', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) - const pairSetup = new PairSetupClient(port, httpAgent); + const pairSetup = new PairSetupClient(port, httpAgent) const M3 = await pairSetup.prepareM3({ - salt: crypto.randomBytes(16), - serverPublicKey: crypto.randomBytes(384), - }, accessoryInfoUnpaired.pincode); + salt: randomBytes(16), + serverPublicKey: randomBytes(384), + }, accessoryInfoUnpaired.pincode) + let response try { - const response = await pairSetup.sendM3(M3); - fail(`Expected erroneous response, got ${response}`); + response = await pairSetup.sendM3(M3) } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPHTTPCode.BAD_REQUEST); + expect(error).toBeInstanceOf(AxiosError) + expect(error.response?.status).toBe(HAPHTTPCode.BAD_REQUEST) + + const objectsM4 = decode(error.response?.data) + expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M4) + expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNKNOWN) + } - const objectsM4 = tlv.decode(error.response?.data); - expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M4); - expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNKNOWN); + if (response) { + throw new Error(`Expected erroneous response, got ${response}`) } - }); + }) - test("reject pair-setup with state M5", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('reject pair-setup with state M5', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) - const pairSetup = new PairSetupClient(port, httpAgent); + const pairSetup = new PairSetupClient(port, httpAgent) - const M5 = await pairSetup.prepareM5({ sharedSecret: crypto.randomBytes(256) }, clientInfo); + const M5 = pairSetup.prepareM5({ sharedSecret: randomBytes(256) }, clientInfo) try { - const response = await pairSetup.sendM5(M5); - fail(`Expected erroneous response, got ${response}`); + const response = await pairSetup.sendM5(M5) + throw new Error(`Expected erroneous response, got ${response}`) } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPHTTPCode.BAD_REQUEST); + expect(error).toBeInstanceOf(AxiosError) + expect(error.response?.status).toBe(HAPHTTPCode.BAD_REQUEST) - const objectsM4 = tlv.decode(error.response?.data); - expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M6); - expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNKNOWN); + const objectsM4 = decode(error.response?.data) + expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M6) + expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.UNKNOWN) } - }); + }) - test("test successful /pair-verify", async () => { - server = new HAPServer(accessoryInfoPaired); - const [port] = await bindServer(server); + it('test successful /pair-verify', async () => { + server = new HAPServer(accessoryInfoPaired) + const [port] = await bindServer(server) - const pairVerify = new PairVerifyClient(port, httpAgent); - await pairVerify.sendPairVerify(serverInfoPaired, clientInfo); - }); + const pairVerify = new PairVerifyClient(port, httpAgent) + await pairVerify.sendPairVerify(serverInfoPaired, clientInfo) + }) - test("test /pair-verify with not being paired", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('test /pair-verify with not being paired', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) - const pairVerify = new PairVerifyClient(port, httpAgent); + const pairVerify = new PairVerifyClient(port, httpAgent) // M1 - const responseM1 = await pairVerify.sendM1(); + const responseM1 = await pairVerify.sendM1() // M2 - const M2 = pairVerify.parseM2(responseM1.data, serverInfoUnpaired); + const M2 = pairVerify.parseM2(responseM1.data, serverInfoUnpaired) // M3 - const M3 = pairVerify.prepareM3(M2, clientInfo); + const M3 = pairVerify.prepareM3(M2, clientInfo) - const responseM3 = await pairVerify.sendM3(M3); - const objectsM4 = tlv.decode(responseM3.data); + const responseM3 = await pairVerify.sendM3(M3) + const objectsM4 = decode(responseM3.data) - expect(objectsM4[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M4); - expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.AUTHENTICATION); - }); + expect(objectsM4[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M4) + expect(objectsM4[TLVValues.ERROR_CODE].readUInt8(0)).toEqual(TLVErrorCode.AUTHENTICATION) + }) - describe("tests with paired and pair-verified connection", () => { - let port: number; - let address: string; - let encryption: HAPEncryption; - let client: HAPHTTPClient; + describe('tests with paired and pair-verified connection', () => { + let port: number + let address: string + let encryption: HAPEncryption + let client: HAPHTTPClient beforeEach(async () => { - server = new HAPServer(accessoryInfoPaired); - const addressInformation = await bindServer(server); - port = addressInformation[0]; - address = addressInformation[1]; + server = new HAPServer(accessoryInfoPaired) + const addressInformation = await bindServer(server) + port = addressInformation[0] + address = addressInformation[1] - const pairVerify = new PairVerifyClient(port, httpAgent); - encryption = await pairVerify.sendPairVerify(serverInfoPaired, clientInfo); + const pairVerify = new PairVerifyClient(port, httpAgent) + encryption = await pairVerify.sendPairVerify(serverInfoPaired, clientInfo) - client = new HAPHTTPClient(httpAgent, address, port); - client.attachSocket(); + client = new HAPHTTPClient(httpAgent, address, port) + client.attachSocket() - client.enableEncryption(encryption); - }); + client.enableEncryption(encryption) + }) afterEach(() => { - client.releaseSocket(); - }); + client.releaseSocket() + }) - test("test /pairings ADD_PAIRING", async () => { - const exampleKey = crypto.randomBytes(32); + it('test /pairings ADD_PAIRING', async () => { + const exampleKey = randomBytes(32) server.on(HAPServerEventTypes.ADD_PAIRING, (connection, username, publicKey, permission, callback) => { - expect(connection.encryption).toBeDefined(); - expect(username).toEqual(thirdUsername); - expect(publicKey).toEqual(exampleKey); - expect(permission).toEqual(PermissionTypes.ADMIN); - callback(0); - }); + expect(connection.encryption).toBeDefined() + expect(username).toEqual(thirdUsername) + expect(publicKey).toEqual(exampleKey) + expect(permission).toEqual(PermissionTypes.ADMIN) + callback(0) + }) - await client.sendAddPairingRequest(thirdUsername, exampleKey, PermissionTypes.ADMIN); - }); + await client.sendAddPairingRequest(thirdUsername, exampleKey, PermissionTypes.ADMIN) + }) - test("test /pairings REMOVE_PAIRING", async () => { + it('test /pairings REMOVE_PAIRING', async () => { server.on(HAPServerEventTypes.REMOVE_PAIRING, (connection, username, callback) => { - expect(connection.encryption).toBeDefined(); - expect(username).toEqual(thirdUsername); - callback(0); - }); + expect(connection.encryption).toBeDefined() + expect(username).toEqual(thirdUsername) + callback(0) + }) - await client.sendRemovePairingRequest(thirdUsername); - }); + await client.sendRemovePairingRequest(thirdUsername) + }) - test("test /pairings LIST_PAIRINGS", async () => { + it('test /pairings LIST_PAIRINGS', async () => { const list: PairingInformation[] = [ { username: clientUsername, @@ -290,21 +301,21 @@ describe("HAPServer", () => { }, { username: thirdUsername, - publicKey: crypto.randomBytes(32), + publicKey: randomBytes(32), permission: PermissionTypes.USER, }, - ]; + ] server.on(HAPServerEventTypes.LIST_PAIRINGS, (connection, callback) => { - expect(connection.encryption).toBeDefined(); - callback(0, list); - }); + expect(connection.encryption).toBeDefined() + callback(0, list) + }) - const response = await client.sendListPairingsRequest(); - expect(response).toEqual(list); - }); + const response = await client.sendListPairingsRequest() + expect(response).toEqual(list) + }) - test("test /accessories", async () => { + it('test /accessories', async () => { const accessoryResponse: AccessoriesResponse = { accessories: [{ aid: 1, @@ -315,28 +326,28 @@ describe("HAPServer", () => { iid: 3, format: Formats.STRING, perms: [Perms.PAIRED_READ], - value: "Hello World", + value: 'Hello World', type: Characteristic.Name.UUID, }], }], }], - }; + } server.on(HAPServerEventTypes.ACCESSORIES, (connection, callback) => { - expect(connection.encryption).toBeDefined(); - callback(undefined, accessoryResponse); - }); + expect(connection.encryption).toBeDefined() + callback(undefined, accessoryResponse) + }) - const accessories = await client.sendAccessoriesRequest(); - expect(accessories).toEqual(accessoryResponse); - }); + const accessories = await client.sendAccessoriesRequest() + expect(accessories).toEqual(accessoryResponse) + }) - test("test successful GET /characteristics", async () => { - const ids: CharacteristicId[] = [ { aid: 1, iid: 9 }, { aid: 2, iid: 14 } ]; + it('test successful GET /characteristics', async () => { + const ids: CharacteristicId[] = [{ aid: 1, iid: 9 }, { aid: 2, iid: 14 }] const readData: CharacteristicReadData[] = [ { ...ids[0], - value: "Hello World", + value: 'Hello World', type: Characteristic.Name.UUID, ev: true, }, @@ -346,48 +357,48 @@ describe("HAPServer", () => { type: Characteristic.Active.UUID, ev: false, }, - ]; + ] server.on(HAPServerEventTypes.GET_CHARACTERISTICS, (connection, request, callback) => { - expect(connection.encryption).toBeDefined(); - expect(request.ids).toEqual(ids); - expect(request.includeMeta).toBeFalsy(); - expect(request.includePerms).toBeFalsy(); - expect(request.includeType).toBeTruthy(); - expect(request.includeEvent).toBeTruthy(); - callback(undefined, { characteristics: readData }); - }); - - const httpResponse = await client.sendCharacteristicRead(ids, false, false, true, true); - expect(httpResponse.statusCode).toEqual(HAPHTTPCode.OK); - expect(httpResponse.body).toEqual({ characteristics: readData }); - }); - - test("test GET /characteristics with errors", async () => { - const ids: CharacteristicId[] = [ { aid: 1, iid: 9 }, { aid: 2, iid: 14 } ]; + expect(connection.encryption).toBeDefined() + expect(request.ids).toEqual(ids) + expect(request.includeMeta).toBeFalsy() + expect(request.includePerms).toBeFalsy() + expect(request.includeType).toBeTruthy() + expect(request.includeEvent).toBeTruthy() + callback(undefined, { characteristics: readData }) + }) + + const httpResponse = await client.sendCharacteristicRead(ids, false, false, true, true) + expect(httpResponse.statusCode).toEqual(HAPHTTPCode.OK) + expect(httpResponse.body).toEqual({ characteristics: readData }) + }) + + it('test GET /characteristics with errors', async () => { + const ids: CharacteristicId[] = [{ aid: 1, iid: 9 }, { aid: 2, iid: 14 }] const readData: CharacteristicReadData[] = [ { ...ids[0], - value: "Hello World", + value: 'Hello World', }, { ...ids[1], status: HAPStatus.SERVICE_COMMUNICATION_FAILURE, }, - ]; + ] server.on(HAPServerEventTypes.GET_CHARACTERISTICS, (connection, request, callback) => { - expect(connection.encryption).toBeDefined(); - expect(request.ids).toEqual(ids); - expect(request.includeMeta).toBeFalsy(); - expect(request.includePerms).toBeFalsy(); - expect(request.includeType).toBeFalsy(); - expect(request.includeEvent).toBeFalsy(); - callback(undefined, { characteristics: readData }); - }); - - const httpResponse = await client.sendCharacteristicRead(ids); - expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS); + expect(connection.encryption).toBeDefined() + expect(request.ids).toEqual(ids) + expect(request.includeMeta).toBeFalsy() + expect(request.includePerms).toBeFalsy() + expect(request.includeType).toBeFalsy() + expect(request.includeEvent).toBeFalsy() + callback(undefined, { characteristics: readData }) + }) + + const httpResponse = await client.sendCharacteristicRead(ids) + expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS) expect(httpResponse.body).toEqual({ characteristics: [ { @@ -396,16 +407,16 @@ describe("HAPServer", () => { }, readData[1], ], - }); - }); + }) + }) - test("test successful PUT /characteristics value write NO_CONTENT", async () => { + it('test successful PUT /characteristics value write NO_CONTENT', async () => { const writeRequest: CharacteristicsWriteRequest = { characteristics: [ { aid: 1, iid: 9, - value: "Hello World", + value: 'Hello World', }, { aid: 2, @@ -413,7 +424,7 @@ describe("HAPServer", () => { value: true, }, ], - }; + } const hapResponse: CharacteristicsWriteResponse = { characteristics: [ @@ -428,25 +439,25 @@ describe("HAPServer", () => { status: HAPStatus.SUCCESS, }, ], - }; + } server.on(HAPServerEventTypes.SET_CHARACTERISTICS, (connection, request, callback) => { - expect(connection.encryption).toBeDefined(); - expect(request).toEqual(writeRequest); - callback(undefined, hapResponse); - }); + expect(connection.encryption).toBeDefined() + expect(request).toEqual(writeRequest) + callback(undefined, hapResponse) + }) - const httpResponse = await client.sendCharacteristicWrite(writeRequest); - expect(httpResponse.statusCode).toEqual(HAPHTTPCode.NO_CONTENT); - }); + const httpResponse = await client.sendCharacteristicWrite(writeRequest) + expect(httpResponse.statusCode).toEqual(HAPHTTPCode.NO_CONTENT) + }) - test("test PUT /characteristics value with MULTI_STATUS", async () => { - const ids: CharacteristicId[] = [ { aid: 1, iid: 9 }, { aid: 2, iid: 14 } ]; + it('test PUT /characteristics value with MULTI_STATUS', async () => { + const ids: CharacteristicId[] = [{ aid: 1, iid: 9 }, { aid: 2, iid: 14 }] const writeRequest: CharacteristicsWriteRequest = { characteristics: [ { ...ids[0], - value: "Hello World", + value: 'Hello World', }, { ...ids[1], @@ -455,7 +466,7 @@ describe("HAPServer", () => { }, ], pid: 1337, - }; + } const hapResponse: CharacteristicsWriteResponse = { characteristics: [ @@ -468,180 +479,175 @@ describe("HAPServer", () => { status: HAPStatus.SERVICE_COMMUNICATION_FAILURE, }, ], - }; + } server.on(HAPServerEventTypes.SET_CHARACTERISTICS, (connection, request, callback) => { - expect(connection.encryption).toBeDefined(); - expect(request).toEqual(writeRequest); - callback(undefined, hapResponse); - }); + expect(connection.encryption).toBeDefined() + expect(request).toEqual(writeRequest) + callback(undefined, hapResponse) + }) - const httpResponse = await client.sendCharacteristicWrite(writeRequest); - expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS); - }); + const httpResponse = await client.sendCharacteristicWrite(writeRequest) + expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS) + }) - test("test PUT /characteristics value with write response", async () => { - const ids: CharacteristicId[] = [ { aid: 1, iid: 9 } ]; + it('test PUT /characteristics value with write response', async () => { + const ids: CharacteristicId[] = [{ aid: 1, iid: 9 }] const writeRequest: CharacteristicsWriteRequest = { characteristics: [ { ...ids[0], - value: "Hello", + value: 'Hello', }, ], - }; + } const hapResponse: CharacteristicsWriteResponse = { characteristics: [ { ...ids[0], status: HAPStatus.SUCCESS, - value: "World", + value: 'World', }, ], - }; + } server.on(HAPServerEventTypes.SET_CHARACTERISTICS, (connection, request, callback) => { - expect(connection.encryption).toBeDefined(); - expect(request).toEqual(writeRequest); - callback(undefined, hapResponse); - }); + expect(connection.encryption).toBeDefined() + expect(request).toEqual(writeRequest) + callback(undefined, hapResponse) + }) - const httpResponse = await client.sendCharacteristicWrite(writeRequest); - expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS); - }); + const httpResponse = await client.sendCharacteristicWrite(writeRequest) + expect(httpResponse.statusCode).toEqual(HAPHTTPCode.MULTI_STATUS) + }) - test("test prepare write request", async () => { + it('test prepare write request', async () => { await client.sendPrepareWrite({ pid: 1337, ttl: 1000, - }); + }) - await PromiseTimeout(50); + await PromiseTimeout(50) // testing that pid is overwritten! await client.sendPrepareWrite({ pid: 13337, ttl: 100, - }); + }) // we just do a /accessories request to get hold onto the HAPConnection object - let hapConnection!: HAPConnection; + let hapConnection!: HAPConnection server.on(HAPServerEventTypes.ACCESSORIES, (connection, callback) => { - hapConnection = connection; - callback(undefined, { accessories:[] }); - }); - await client.sendAccessoriesRequest(); + hapConnection = connection + callback(undefined, { accessories: [] }) + }) + await client.sendAccessoriesRequest() - expect(hapConnection.timedWriteTimeout).toBeDefined(); - expect(hapConnection.timedWritePid).toBe(13337); + expect(hapConnection.timedWriteTimeout).toBeDefined() + expect(hapConnection.timedWritePid).toBe(13337) - await PromiseTimeout(120); + await PromiseTimeout(120) - expect(hapConnection.timedWriteTimeout).toBeUndefined(); - expect(hapConnection.timedWritePid).toBeUndefined(); - }); + expect(hapConnection.timedWriteTimeout).toBeUndefined() + expect(hapConnection.timedWritePid).toBeUndefined() + }) - test("test resource request", async () => { - const image = crypto.randomBytes(256); + it('test resource request', async () => { + const image = randomBytes(256) const request: ResourceRequest = { - "image-height": 256, - "image-width": 512, - "resource-type": ResourceRequestType.IMAGE, - }; + 'image-height': 256, + 'image-width': 512, + 'resource-type': ResourceRequestType.IMAGE, + } server.on(HAPServerEventTypes.REQUEST_RESOURCE, (resource, callback) => { - expect(resource).toEqual(request); - callback(undefined, image); - }); + expect(resource).toEqual(request) + callback(undefined, image) + }) - const result = await client.sendResourceRequest(request); - expect(result).toEqual(image); - }); - }); + const result = await client.sendResourceRequest(request) + expect(result).toEqual(image) + }) + }) - test.each(["pairings", "accessories", "characteristics", "prepare", "resource"])( - "request to \"/%s\" should be rejected in unpaired and unverified state", + it.each(['pairings', 'accessories', 'characteristics', 'prepare', 'resource'])( + 'request to "/%s" should be rejected in unpaired and unverified state', async (route: string) => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) try { - const response = await axios.post(`http://localhost:${port}/${route}`, { httpAgent }); - fail(`Expected erroneous response, got ${response}`); + const response = await axios.post(`http://localhost:${port}/${route}`, { httpAgent }) + throw new Error(`Expected erroneous response, got ${response}`) } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED); - expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES }); + expect(error).toBeInstanceOf(AxiosError) + expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED) + expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES }) } }, - ); + ) - test.each(["pairings", "accessories", "characteristics", "prepare", "resource"])( - "request to \"/%s\" should be rejected in paired and unverified state", + it.each(['pairings', 'accessories', 'characteristics', 'prepare', 'resource'])( + 'request to "/%s" should be rejected in paired and unverified state', async (route: string) => { - server = new HAPServer(accessoryInfoPaired); - const [port] = await bindServer(server); + server = new HAPServer(accessoryInfoPaired) + const [port] = await bindServer(server) try { - const response = await axios.post(`http://localhost:${port}/${route}`, { httpAgent }); - fail(`Expected erroneous response, got ${response}`); + const response = await axios.post(`http://localhost:${port}/${route}`, { httpAgent }) + throw new Error(`Expected erroneous response, got ${response}`) } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED); - expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES }); + expect(error).toBeInstanceOf(AxiosError) + expect(error.response?.status).toBe(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED) + expect(error.response?.data).toEqual({ status: HAPStatus.INSUFFICIENT_PRIVILEGES }) } }, - ); + ) - test("test non-existence resource", async () => { - server = new HAPServer(accessoryInfoUnpaired); - const [port] = await bindServer(server); + it('test non-existence resource', async () => { + server = new HAPServer(accessoryInfoUnpaired) + const [port] = await bindServer(server) try { - const response = await axios.post(`http://localhost:${port}/non-existent`, { httpAgent }); - fail(`Expected erroneous response, got ${response}`); + const response = await axios.post(`http://localhost:${port}/non-existent`, { httpAgent }) + throw new Error(`Expected erroneous response, got ${response}`) } catch (error) { - expect(error).toBeInstanceOf(AxiosError); - expect(error.response?.status).toBe(HAPHTTPCode.NOT_FOUND); - expect(error.response?.data).toEqual({ status: HAPStatus.RESOURCE_DOES_NOT_EXIST }); + expect(error).toBeInstanceOf(AxiosError) + expect(error.response?.status).toBe(HAPHTTPCode.NOT_FOUND) + expect(error.response?.data).toEqual({ status: HAPStatus.RESOURCE_DOES_NOT_EXIST }) } - }); + }) +}) -}); - -describe(IsKnownHAPStatusError, () => { - it("should approve all defined error codes", () => { +describe(isKnownHAPStatusError, () => { + it('should approve all defined error codes', () => { // @ts-expect-error: forceConsistentCasingInFileNames compiler option const errorValues = Object.values(HAPStatus) - .filter(error => typeof error === "number") // .values will actually include both the enum values and enum names - .filter(error => error !== 0); // filter out HAPStatus.SUCCESS + .filter(error => typeof error === 'number') // .values will actually include both the enum values and enum names + .filter(error => error !== 0) // filter out HAPStatus.SUCCESS for (const error of errorValues) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore:next-line - This was @ts-expect-error: type mismatch, but it triggered build errors - // Summary of all failing tests src/lib/HAPServer.spec.ts:621:7 - error TS2578: Unused '@ts-expect-error' directive. - - const result = IsKnownHAPStatusError(error); + const result = isKnownHAPStatusError(error) if (!result) { - fail("IsKnownHAPStatusError does not return true for error code " + error); + throw new Error(`isKnownHAPStatusError does not return true for error code ${error}`) } } - }); + }) - it("should reject non defined error codes", () => { - expect(IsKnownHAPStatusError(23 as HAPStatus)).toBe(false); - expect(IsKnownHAPStatusError(-3 as HAPStatus)).toBe(false); - expect(IsKnownHAPStatusError(-72037 as HAPStatus)).toBe(false); - expect(IsKnownHAPStatusError(HAPStatus.SUCCESS as HAPStatus)).toBe(false); - }); + it('should reject non defined error codes', () => { + expect(isKnownHAPStatusError(23 as HAPStatus)).toBe(false) + expect(isKnownHAPStatusError(-3 as HAPStatus)).toBe(false) + expect(isKnownHAPStatusError(-72037 as HAPStatus)).toBe(false) + expect(isKnownHAPStatusError(HAPStatus.SUCCESS as HAPStatus)).toBe(false) + }) - it("should reject invalid user input", () => { + it('should reject invalid user input', () => { // @ts-expect-error: deliberate illegal input - expect(IsKnownHAPStatusError("aaaa")).toBe(false); + expect(isKnownHAPStatusError('aaaa')).toBe(false) // @ts-expect-error: deliberate illegal input - expect(IsKnownHAPStatusError({ "key": "value" })).toBe(false); + expect(isKnownHAPStatusError({ key: 'value' })).toBe(false) // @ts-expect-error: deliberate illegal input - expect(IsKnownHAPStatusError([])).toBe(false); - }); -}); + expect(isKnownHAPStatusError([])).toBe(false) + }) +}) diff --git a/src/lib/HAPServer.ts b/src/lib/HAPServer.ts index de3e29fa6..161333d5f 100644 --- a/src/lib/HAPServer.ts +++ b/src/lib/HAPServer.ts @@ -1,12 +1,6 @@ -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { SRP, SrpServer } from "fast-srp-hap"; -import { IncomingMessage, ServerResponse } from "http"; -import tweetnacl from "tweetnacl"; -import { URL } from "url"; -import { consideredTrue, HAPMimeTypes, PairingStates, PairMethods, TLVValues } from "../internal-types"; -import { +import type { IncomingMessage, ServerResponse } from 'node:http' + +import type { AccessoriesResponse, CharacteristicId, CharacteristicsReadRequest, @@ -18,38 +12,56 @@ import { PrepareWriteRequest, ResourceRequest, VoidCallback, -} from "../types"; -import { AccessoryInfo, PairingInformation, PermissionTypes } from "./model/AccessoryInfo"; -import { EventedHTTPServer, EventedHTTPServerEvent, HAPConnection, HAPEncryption, HAPUsername } from "./util/eventedhttp"; -import * as hapCrypto from "./util/hapCrypto"; -import { once } from "./util/once"; -import * as tlv from "./util/tlv"; +} from '../types' +import type { AccessoryInfo, PairingInformation, PermissionTypes } from './model/AccessoryInfo' +import type { HAPConnection, HAPUsername } from './util/eventedhttp' + +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { URL } from 'node:url' + +import createDebug from 'debug' +import { SRP, SrpServer } from 'fast-srp-hap' +import tweetnacl from 'tweetnacl' -const debug = createDebug("HAP-NodeJS:HAPServer"); +import { consideredTrue, HAPMimeTypes, PairingStates, PairMethods, TLVValues } from '../internal-types.js' +import { EventedHTTPServer, EventedHTTPServerEvent, HAPEncryption } from './util/eventedhttp.js' +import { + chacha20_poly1305_decryptAndVerify, + chacha20_poly1305_encryptAndSeal, + generateCurve25519KeyPair, + generateCurve25519SharedSecKey, + HKDF, +} from './util/hapCrypto.js' +import { once } from './util/once.js' +import { decode, encode } from './util/tlv.js' + +const debug = createDebug('HAP-NodeJS:HAPServer') /** * TLV error codes for the `TLVValues.ERROR_CODE` field. * * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum TLVErrorCode { - // noinspection JSUnusedGlobalSymbols UNKNOWN = 0x01, INVALID_REQUEST = 0x02, - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + + // eslint-disable-next-line ts/no-duplicate-enum-values AUTHENTICATION = 0x02, // setup code or signature verification failed BACKOFF = 0x03, // // client must look at retry delay tlv item MAX_PEERS = 0x04, // server cannot accept any more pairings MAX_TRIES = 0x05, // server reached maximum number of authentication attempts UNAVAILABLE = 0x06, // server pairing method is unavailable - BUSY = 0x07 // cannot accept pairing request at this time + BUSY = 0x07, // cannot accept pairing request at this time } /** * @group HAP Accessory Server */ -export const enum HAPStatus { - // noinspection JSUnusedGlobalSymbols +export enum HAPStatus { /** * Success of the request. @@ -104,7 +116,7 @@ export const enum HAPStatus { */ NOT_ALLOWED_IN_CURRENT_STATE = -70412, - // when adding new status codes, remember to update bounds in IsKnownHAPStatusError below + // when adding new status codes, remember to update bounds in isKnownHAPStatusError below } /** @@ -112,13 +124,13 @@ export const enum HAPStatus { * * @group HAP Accessory Server */ -export function IsKnownHAPStatusError(status: HAPStatus): boolean { +export function isKnownHAPStatusError(status: HAPStatus): boolean { return ( // Lower bound (most negative error code) - status >= HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE && + status >= HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE // Upper bound (negative error code closest to zero) - status <= HAPStatus.INSUFFICIENT_PRIVILEGES - ); + && status <= HAPStatus.INSUFFICIENT_PRIVILEGES + ) } /** @@ -131,8 +143,8 @@ export function IsKnownHAPStatusError(status: HAPStatus): boolean { * * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPHTTPCode { - // noinspection JSUnusedGlobalSymbols OK = 200, NO_CONTENT = 204, MULTI_STATUS = 207, @@ -153,8 +165,8 @@ export const enum HAPHTTPCode { * * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPPairingHTTPCode { - // noinspection JSUnusedGlobalSymbols OK = 200, BAD_REQUEST = 400, // e.g. bad tlv, state errors, etc @@ -165,153 +177,155 @@ export const enum HAPPairingHTTPCode { INTERNAL_SERVER_ERROR = 500, } -type HAPRequestHandler = (connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse) => void; +type HAPRequestHandler = (connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse) => void /** * @group HAP Accessory Server */ -export type IdentifyCallback = VoidCallback; +export type IdentifyCallback = VoidCallback /** * @group HAP Accessory Server */ -export type HAPHttpError = { httpCode: HAPHTTPCode, status: HAPStatus}; +export interface HAPHttpError { httpCode: HAPHTTPCode, status: HAPStatus } /** * @group HAP Accessory Server */ -export type PairingsCallback = (error: TLVErrorCode | 0, data?: T) => void; +export type PairingsCallback = (error: TLVErrorCode | 0, data?: T) => void /** * @group HAP Accessory Server */ -export type AddPairingCallback = PairingsCallback; +export type AddPairingCallback = PairingsCallback /** * @group HAP Accessory Server */ -export type RemovePairingCallback = PairingsCallback; +export type RemovePairingCallback = PairingsCallback /** * @group HAP Accessory Server */ -export type ListPairingsCallback = PairingsCallback; +export type ListPairingsCallback = PairingsCallback /** * @group HAP Accessory Server */ -export type PairCallback = VoidCallback; +export type PairCallback = VoidCallback /** * @group HAP Accessory Server */ -export type AccessoriesCallback = (error: HAPHttpError | undefined, result?: AccessoriesResponse) => void; +export type AccessoriesCallback = (error: HAPHttpError | undefined, result?: AccessoriesResponse) => void /** * @group HAP Accessory Server */ -export type ReadCharacteristicsCallback = (error: HAPHttpError | undefined, response?: CharacteristicsReadResponse) => void; +export type ReadCharacteristicsCallback = (error: HAPHttpError | undefined, response?: CharacteristicsReadResponse) => void /** * @group HAP Accessory Server */ -export type WriteCharacteristicsCallback = (error: HAPHttpError | undefined, response?: CharacteristicsWriteResponse) => void; +export type WriteCharacteristicsCallback = (error: HAPHttpError | undefined, response?: CharacteristicsWriteResponse) => void /** * @group HAP Accessory Server */ -export type ResourceRequestCallback = (error: HAPHttpError | undefined, resource?: Buffer) => void; +export type ResourceRequestCallback = (error: HAPHttpError | undefined, resource?: Buffer) => void /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPServerEventTypes { /** * Emitted when the server is fully set up and ready to receive connections. */ - LISTENING = "listening", + LISTENING = 'listening', /** * Emitted when a client wishes for this server to identify itself before pairing. You must call the * callback to respond to the client with success. */ - IDENTIFY = "identify", - ADD_PAIRING = "add-pairing", - REMOVE_PAIRING = "remove-pairing", - LIST_PAIRINGS = "list-pairings", + IDENTIFY = 'identify', + ADD_PAIRING = 'add-pairing', + REMOVE_PAIRING = 'remove-pairing', + LIST_PAIRINGS = 'list-pairings', /** * This event is emitted when a client completes the "pairing" process and exchanges encryption keys. * Note that this does not mean the "Add Accessory" process in iOS has completed. * You must call the callback to complete the process. */ - PAIR = "pair", + PAIR = 'pair', /** * This event is emitted when a client requests the complete representation of Accessory data for * this Accessory (for instance, what services, characteristics, etc. are supported) and any bridged * Accessories in the case of a Bridge Accessory. The listener must call the provided callback function * when the accessory data is ready. We will automatically JSON.stringify the data. */ - ACCESSORIES = "accessories", + ACCESSORIES = 'accessories', /** * This event is emitted when a client wishes to retrieve the current value of one or more characteristics. * The listener must call the provided callback function when the values are ready. iOS clients can typically * wait up to 10 seconds for this call to return. We will automatically JSON.stringify the data (which must * be an array) and wrap it in an object with a top-level "characteristics" property. */ - GET_CHARACTERISTICS = "get-characteristics", + GET_CHARACTERISTICS = 'get-characteristics', /** * This event is emitted when a client wishes to set the current value of one or more characteristics and/or * subscribe to one or more events. The 'events' param is an initially-empty object, associated with the current * connection, on which you may store event registration keys for later processing. The listener must call * the provided callback when the request has been processed. */ - SET_CHARACTERISTICS = "set-characteristics", - REQUEST_RESOURCE = "request-resource", - CONNECTION_CLOSED = "connection-closed", + SET_CHARACTERISTICS = 'set-characteristics', + REQUEST_RESOURCE = 'request-resource', + CONNECTION_CLOSED = 'connection-closed', } /** * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface HAPServer { - on(event: "listening", listener: (port: number, address: string) => void): this; - on(event: "identify", listener: (callback: IdentifyCallback) => void): this; + /* eslint-disable ts/method-signature-style */ + on(event: 'listening', listener: (port: number, address: string) => void): this + on(event: 'identify', listener: (callback: IdentifyCallback) => void): this on( - event: "add-pairing", + event: 'add-pairing', listener: (connection: HAPConnection, username: HAPUsername, publicKey: Buffer, permission: PermissionTypes, callback: AddPairingCallback) => void - ): this; - on(event: "remove-pairing", listener: (connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback) => void): this; - on(event: "list-pairings", listener: (connection: HAPConnection, callback: ListPairingsCallback) => void): this; - on(event: "pair", listener: (username: HAPUsername, clientLTPK: Buffer, callback: PairCallback) => void): this; + ): this + on(event: 'remove-pairing', listener: (connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback) => void): this + on(event: 'list-pairings', listener: (connection: HAPConnection, callback: ListPairingsCallback) => void): this + on(event: 'pair', listener: (username: HAPUsername, clientLTPK: Buffer, callback: PairCallback) => void): this - on(event: "accessories", listener: (connection: HAPConnection, callback: AccessoriesCallback) => void): this; + on(event: 'accessories', listener: (connection: HAPConnection, callback: AccessoriesCallback) => void): this on( - event: "get-characteristics", + event: 'get-characteristics', listener: (connection: HAPConnection, request: CharacteristicsReadRequest, callback: ReadCharacteristicsCallback) => void - ): this; + ): this on( - event: "set-characteristics", + event: 'set-characteristics', listener: (connection: HAPConnection, request: CharacteristicsWriteRequest, callback: WriteCharacteristicsCallback) => void - ): this; - on(event: "request-resource", listener: (resource: ResourceRequest, callback: ResourceRequestCallback) => void): this; - - on(event: "connection-closed", listener: (connection: HAPConnection) => void): this; + ): this + on(event: 'request-resource', listener: (resource: ResourceRequest, callback: ResourceRequestCallback) => void): this + on(event: 'connection-closed', listener: (connection: HAPConnection) => void): this - emit(event: "listening", port: number, address: string): boolean; - emit(event: "identify", callback : IdentifyCallback): boolean; + emit(event: 'listening', port: number, address: string): boolean + emit(event: 'identify', callback: IdentifyCallback): boolean emit( - event: "add-pairing", + event: 'add-pairing', connection: HAPConnection, username: HAPUsername, publicKey: Buffer, permission: PermissionTypes, callback: AddPairingCallback - ): boolean; - emit(event: "remove-pairing", connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback): boolean; - emit(event: "list-pairings", connection: HAPConnection, callback: ListPairingsCallback): boolean; - emit(event: "pair", username: HAPUsername, clientLTPK: Buffer, callback: PairCallback): boolean; - - emit(event: "accessories", connection: HAPConnection, callback : AccessoriesCallback): boolean; - emit(event: "get-characteristics", connection: HAPConnection, request: CharacteristicsReadRequest, callback: ReadCharacteristicsCallback): boolean; - emit(event: "set-characteristics", connection: HAPConnection, request: CharacteristicsWriteRequest, callback: WriteCharacteristicsCallback): boolean; - emit(event: "request-resource", resource: ResourceRequest, callback: ResourceRequestCallback): boolean; - - emit(event: "connection-closed", connection: HAPConnection): boolean; + ): boolean + emit(event: 'remove-pairing', connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback): boolean + emit(event: 'list-pairings', connection: HAPConnection, callback: ListPairingsCallback): boolean + emit(event: 'pair', username: HAPUsername, clientLTPK: Buffer, callback: PairCallback): boolean + + emit(event: 'accessories', connection: HAPConnection, callback: AccessoriesCallback): boolean + emit(event: 'get-characteristics', connection: HAPConnection, request: CharacteristicsReadRequest, callback: ReadCharacteristicsCallback): boolean + emit(event: 'set-characteristics', connection: HAPConnection, request: CharacteristicsWriteRequest, callback: WriteCharacteristicsCallback): boolean + emit(event: 'request-resource', resource: ResourceRequest, callback: ResourceRequestCallback): boolean + + emit(event: 'connection-closed', connection: HAPConnection): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -335,43 +349,42 @@ export declare interface HAPServer { * * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class HAPServer extends EventEmitter { + private accessoryInfo: AccessoryInfo + private httpServer: EventedHTTPServer + private unsuccessfulPairAttempts = 0 // after 100 unsuccessful attempts the server won't accept any further attempts. Will currently be reset on a reboot - private accessoryInfo: AccessoryInfo; - private httpServer: EventedHTTPServer; - private unsuccessfulPairAttempts = 0; // after 100 unsuccessful attempts the server won't accept any further attempts. Will currently be reset on a reboot - - allowInsecureRequest: boolean; + allowInsecureRequest: boolean constructor(accessoryInfo: AccessoryInfo) { - super(); - this.accessoryInfo = accessoryInfo; - this.allowInsecureRequest = false; + super() + this.accessoryInfo = accessoryInfo + this.allowInsecureRequest = false // internal server that does all the actual communication - this.httpServer = new EventedHTTPServer(); - this.httpServer.on(EventedHTTPServerEvent.LISTENING, this.onListening.bind(this)); - this.httpServer.on(EventedHTTPServerEvent.REQUEST, this.handleRequestOnHAPConnection.bind(this)); - this.httpServer.on(EventedHTTPServerEvent.CONNECTION_CLOSED, this.handleConnectionClosed.bind(this)); + this.httpServer = new EventedHTTPServer() + this.httpServer.on(EventedHTTPServerEvent.LISTENING, this.onListening.bind(this)) + this.httpServer.on(EventedHTTPServerEvent.REQUEST, this.handleRequestOnHAPConnection.bind(this)) + this.httpServer.on(EventedHTTPServerEvent.CONNECTION_CLOSED, this.handleConnectionClosed.bind(this)) } public listen(port = 0, host?: string): void { - if (host === "::") { + if (host === '::') { // this will work around "EAFNOSUPPORT: address family not supported" errors // on systems where IPv6 is not supported/enabled, we just use the node default then by supplying undefined - host = undefined; + host = undefined } - this.httpServer.listen(port, host); + this.httpServer.listen(port, host) } public stop(): void { - this.httpServer.stop(); + this.httpServer.stop() } public destroy(): void { - this.stop(); - this.removeAllListeners(); + this.stop() + this.removeAllListeners() } /** @@ -383,72 +396,72 @@ export class HAPServer extends EventEmitter { * @param value - The newly set value of the characteristic. * @param originator - If specified, the connection will not get an event message. * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately. - * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. + * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. */ public sendEventNotifications(aid: number, iid: number, value: Nullable, originator?: HAPConnection, immediateDelivery?: boolean): void { try { - this.httpServer.broadcastEvent(aid, iid, value, originator, immediateDelivery); + this.httpServer.broadcastEvent(aid, iid, value, originator, immediateDelivery) } catch (error) { - console.warn("[" + this.accessoryInfo.username + "] Error when sending event notifications: " + error.message); + console.warn(`[${this.accessoryInfo.username}] Error when sending event notifications: ${error.message}`) } } private onListening(port: number, hostname: string): void { - this.emit(HAPServerEventTypes.LISTENING, port, hostname); + this.emit(HAPServerEventTypes.LISTENING, port, hostname) } // Called when an HTTP request was detected. private handleRequestOnHAPConnection(connection: HAPConnection, request: IncomingMessage, response: ServerResponse): void { - debug("[%s] HAP Request: %s %s", this.accessoryInfo.username, request.method, request.url); - const buffers: Buffer[] = []; - request.on("data", data => buffers.push(data)); + debug('[%s] HAP Request: %s %s', this.accessoryInfo.username, request.method, request.url) + const buffers: Buffer[] = [] + request.on('data', data => buffers.push(data)) - request.on("end", () => { - const url = new URL(request.url!, "http://hap-nodejs.local"); // parse the url (query strings etc) + request.on('end', () => { + const url = new URL(request.url!, 'http://hap-nodejs.local') // parse the url (query strings etc) - const handler = this.getHandler(url); + const handler = this.getHandler(url) if (!handler) { - debug("[%s] WARNING: Handler for %s not implemented", this.accessoryInfo.username, request.url); - response.writeHead(HAPHTTPCode.NOT_FOUND, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.RESOURCE_DOES_NOT_EXIST })); + debug('[%s] WARNING: Handler for %s not implemented', this.accessoryInfo.username, request.url) + response.writeHead(HAPHTTPCode.NOT_FOUND, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.RESOURCE_DOES_NOT_EXIST })) } else { - const data = Buffer.concat(buffers); + const data = Buffer.concat(buffers) try { - handler(connection, url, request, data, response); + handler(connection, url, request, data, response) } catch (error) { - debug("[%s] Error executing route handler: %s", this.accessoryInfo.username, error.stack); - response.writeHead(HAPHTTPCode.INTERNAL_SERVER_ERROR, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.RESOURCE_BUSY })); // resource busy try again, does somehow fit? + debug('[%s] Error executing route handler: %s', this.accessoryInfo.username, error.stack) + response.writeHead(HAPHTTPCode.INTERNAL_SERVER_ERROR, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.RESOURCE_BUSY })) // resource busy try again, does somehow fit? } } - }); + }) } private handleConnectionClosed(connection: HAPConnection): void { - this.emit(HAPServerEventTypes.CONNECTION_CLOSED, connection); + this.emit(HAPServerEventTypes.CONNECTION_CLOSED, connection) } private getHandler(url: URL): HAPRequestHandler | undefined { switch (url.pathname.toLowerCase()) { - case "/identify": - return this.handleIdentifyRequest.bind(this); - case "/pair-setup": - return this.handlePairSetup.bind(this); - case "/pair-verify": - return this.handlePairVerify.bind(this); - case "/pairings": - return this.handlePairings.bind(this); - case "/accessories": - return this.handleAccessories.bind(this); - case "/characteristics": - return this.handleCharacteristics.bind(this); - case "/prepare": - return this.handlePrepareWrite.bind(this); - case "/resource": - return this.handleResource.bind(this); - default: - return undefined; + case '/identify': + return this.handleIdentifyRequest.bind(this) + case '/pair-setup': + return this.handlePairSetup.bind(this) + case '/pair-verify': + return this.handlePairVerify.bind(this) + case '/pairings': + return this.handlePairings.bind(this) + case '/accessories': + return this.handleAccessories.bind(this) + case '/characteristics': + return this.handleCharacteristics.bind(this) + case '/prepare': + return this.handlePrepareWrite.bind(this) + case '/resource': + return this.handleResource.bind(this) + default: + return undefined } } @@ -458,131 +471,129 @@ export class HAPServer extends EventEmitter { private handleIdentifyRequest(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { // POST body is empty if (this.accessoryInfo.paired() && !this.allowInsecureRequest) { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } - this.emit(HAPServerEventTypes.IDENTIFY, once(err => { + this.emit(HAPServerEventTypes.IDENTIFY, once((err) => { if (!err) { - debug("[%s] Identification success", this.accessoryInfo.username); - response.writeHead(HAPHTTPCode.NO_CONTENT); - response.end(); + debug('[%s] Identification success', this.accessoryInfo.username) + response.writeHead(HAPHTTPCode.NO_CONTENT) + response.end() } else { - debug("[%s] Identification error: %s", this.accessoryInfo.username, err.message); - response.writeHead(HAPHTTPCode.INTERNAL_SERVER_ERROR, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.RESOURCE_BUSY })); + debug('[%s] Identification error: %s', this.accessoryInfo.username, err.message) + response.writeHead(HAPHTTPCode.INTERNAL_SERVER_ERROR, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.RESOURCE_BUSY })) } - })); + })) } private handlePairSetup(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { // Can only be directly paired with one iOS device if (!this.allowInsecureRequest && this.accessoryInfo.paired()) { - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.UNAVAILABLE)); - return; + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.UNAVAILABLE)) + return } if (this.unsuccessfulPairAttempts > 100) { - debug("[%s] Reached maximum amount of unsuccessful pair attempts!", this.accessoryInfo.username); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.MAX_TRIES)); - return; + debug('[%s] Reached maximum amount of unsuccessful pair attempts!', this.accessoryInfo.username) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.MAX_TRIES)) + return } - const tlvData = tlv.decode(data); - const sequence = tlvData[TLVValues.SEQUENCE_NUM][0]; // value is single byte with sequence number + const tlvData = decode(data) + const sequence = tlvData[TLVValues.SEQUENCE_NUM][0] // value is single byte with sequence number if (sequence === PairingStates.M1) { - this.handlePairSetupM1(connection, request, response); + this.handlePairSetupM1(connection, request, response) } else if (sequence === PairingStates.M3 && connection._pairSetupState === PairingStates.M2) { - this.handlePairSetupM3(connection, request, response, tlvData); + this.handlePairSetupM3(connection, request, response, tlvData) } else if (sequence === PairingStates.M5 && connection._pairSetupState === PairingStates.M4) { - this.handlePairSetupM5(connection, request, response, tlvData); + this.handlePairSetupM5(connection, request, response, tlvData) } else { // Invalid state/sequence number - response.writeHead(HAPPairingHTTPCode.BAD_REQUEST, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, sequence + 1, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)); - return; + response.writeHead(HAPPairingHTTPCode.BAD_REQUEST, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, sequence + 1, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)) } } private handlePairSetupM1(connection: HAPConnection, request: IncomingMessage, response: ServerResponse): void { - debug("[%s] Pair step 1/5", this.accessoryInfo.username); - const salt = crypto.randomBytes(16 ); + debug('[%s] Pair step 1/5', this.accessoryInfo.username) + const salt = randomBytes(16) - const srpParams = SRP.params.hap; - SRP.genKey(32).then(key => { + const srpParams = SRP.params.hap + SRP.genKey(32).then((key) => { // create a new SRP server - const srpServer = new SrpServer(srpParams, salt, Buffer.from("Pair-Setup"), Buffer.from(this.accessoryInfo.pincode), key); - const srpB = srpServer.computeB(); + const srpServer = new SrpServer(srpParams, salt, Buffer.from('Pair-Setup'), Buffer.from(this.accessoryInfo.pincode), key) + const srpB = srpServer.computeB() // attach it to the current TCP session - connection.srpServer = srpServer; - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M2, TLVValues.SALT, salt, TLVValues.PUBLIC_KEY, srpB)); - connection._pairSetupState = PairingStates.M2; - }).catch(error => { - debug("[%s] Error occurred when generating srp key: %s", this.accessoryInfo.username, error.message); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)); - return; - }); + connection.srpServer = srpServer + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M2, TLVValues.SALT, salt, TLVValues.PUBLIC_KEY, srpB)) + connection._pairSetupState = PairingStates.M2 + }).catch((error) => { + debug('[%s] Error occurred when generating srp key: %s', this.accessoryInfo.username, error.message) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)) + }) } private handlePairSetupM3(connection: HAPConnection, request: IncomingMessage, response: ServerResponse, tlvData: Record): void { - debug("[%s] Pair step 2/5", this.accessoryInfo.username); - const A = tlvData[TLVValues.PUBLIC_KEY]; // "A is a public key that exists only for a single login session." - const M1 = tlvData[TLVValues.PASSWORD_PROOF]; // "M1 is the proof that you actually know your own password." + debug('[%s] Pair step 2/5', this.accessoryInfo.username) + const A = tlvData[TLVValues.PUBLIC_KEY] // "A is a public key that exists only for a single login session." + const M1 = tlvData[TLVValues.PASSWORD_PROOF] // "M1 is the proof that you actually know your own password." // pull the SRP server we created in stepOne out of the current session - const srpServer = connection.srpServer!; - srpServer.setA(A); + const srpServer = connection.srpServer! + srpServer.setA(A) try { - srpServer.checkM1(M1); + srpServer.checkM1(M1) } catch (err) { // most likely the client supplied an incorrect pincode. - this.unsuccessfulPairAttempts++; - debug("[%s] Error while checking pincode: %s", this.accessoryInfo.username, err.message); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairSetupState = undefined; - return; + this.unsuccessfulPairAttempts++ + debug('[%s] Error while checking pincode: %s', this.accessoryInfo.username, err.message) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairSetupState = undefined + return } // "M2 is the proof that the server actually knows your password." - const M2 = srpServer.computeM2(); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.PASSWORD_PROOF, M2)); - connection._pairSetupState = PairingStates.M4; + const M2 = srpServer.computeM2() + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.PASSWORD_PROOF, M2)) + connection._pairSetupState = PairingStates.M4 } private handlePairSetupM5(connection: HAPConnection, request: IncomingMessage, response: ServerResponse, tlvData: Record): void { - debug("[%s] Pair step 3/5", this.accessoryInfo.username); + debug('[%s] Pair step 3/5', this.accessoryInfo.username) // pull the SRP server we created in stepOne out of the current session - const srpServer = connection.srpServer!; - const encryptedData = tlvData[TLVValues.ENCRYPTED_DATA]; - const messageData = Buffer.alloc(encryptedData.length - 16); - const authTagData = Buffer.alloc(16); - encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); - encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); - const S_private = srpServer.computeK(); - const encSalt = Buffer.from("Pair-Setup-Encrypt-Salt"); - const encInfo = Buffer.from("Pair-Setup-Encrypt-Info"); - const outputKey = hapCrypto.HKDF("sha512", encSalt, S_private, encInfo, 32); - - let plaintext; + const srpServer = connection.srpServer! + const encryptedData = tlvData[TLVValues.ENCRYPTED_DATA] + const messageData = Buffer.alloc(encryptedData.length - 16) + const authTagData = Buffer.alloc(16) + encryptedData.copy(messageData, 0, 0, encryptedData.length - 16) + encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length) + const S_private = srpServer.computeK() + const encSalt = Buffer.from('Pair-Setup-Encrypt-Salt') + const encInfo = Buffer.from('Pair-Setup-Encrypt-Info') + const outputKey = HKDF('sha512', encSalt, S_private, encInfo, 32) + + let plaintext try { - plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(outputKey, Buffer.from("PS-Msg05"), null, messageData, authTagData); + plaintext = chacha20_poly1305_decryptAndVerify(outputKey, Buffer.from('PS-Msg05'), null, messageData, authTagData) } catch (error) { - debug("[%s] Error while decrypting and verifying M5 subTlv: %s", this.accessoryInfo.username); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairSetupState = undefined; - return; + debug('[%s] Error while decrypting and verifying M5 subTlv: %s', this.accessoryInfo.username) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairSetupState = undefined + return } // decode the client payload and pass it on to the next step - const M5Packet = tlv.decode(plaintext); - const clientUsername = M5Packet[TLVValues.USERNAME]; - const clientLTPK = M5Packet[TLVValues.PUBLIC_KEY]; - const clientProof = M5Packet[TLVValues.PROOF]; - this.handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, outputKey); + const M5Packet = decode(plaintext) + const clientUsername = M5Packet[TLVValues.USERNAME] + const clientLTPK = M5Packet[TLVValues.PUBLIC_KEY] + const clientProof = M5Packet[TLVValues.PROOF] + this.handlePairSetupM5_2(connection, request, response, clientUsername, clientLTPK, clientProof, outputKey) } // M5-2 @@ -595,20 +606,20 @@ export class HAPServer extends EventEmitter { clientProof: Buffer, hkdfEncKey: Buffer, ): void { - debug("[%s] Pair step 4/5", this.accessoryInfo.username); - const S_private = connection.srpServer!.computeK(); - const controllerSalt = Buffer.from("Pair-Setup-Controller-Sign-Salt"); - const controllerInfo = Buffer.from("Pair-Setup-Controller-Sign-Info"); - const outputKey = hapCrypto.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32); - const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]); + debug('[%s] Pair step 4/5', this.accessoryInfo.username) + const S_private = connection.srpServer!.computeK() + const controllerSalt = Buffer.from('Pair-Setup-Controller-Sign-Salt') + const controllerInfo = Buffer.from('Pair-Setup-Controller-Sign-Info') + const outputKey = HKDF('sha512', controllerSalt, S_private, controllerInfo, 32) + const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]) if (!tweetnacl.sign.detached.verify(completeData, clientProof, clientLTPK)) { - debug("[%s] Invalid signature", this.accessoryInfo.username); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairSetupState = undefined; - return; + debug('[%s] Invalid signature', this.accessoryInfo.username) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairSetupState = undefined + return } - this.handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey); + this.handlePairSetupM5_3(connection, request, response, clientUsername, clientLTPK, hkdfEncKey) } // M5 - F + M6 @@ -620,275 +631,279 @@ export class HAPServer extends EventEmitter { clientLTPK: Buffer, hkdfEncKey: Buffer, ): void { - debug("[%s] Pair step 5/5", this.accessoryInfo.username); - const S_private = connection.srpServer!.computeK(); - const accessorySalt = Buffer.from("Pair-Setup-Accessory-Sign-Salt"); - const accessoryInfo = Buffer.from("Pair-Setup-Accessory-Sign-Info"); - const outputKey = hapCrypto.HKDF("sha512", accessorySalt, S_private, accessoryInfo, 32); - const serverLTPK = this.accessoryInfo.signPk; - const usernameData = Buffer.from(this.accessoryInfo.username); - const material = Buffer.concat([outputKey, usernameData, serverLTPK]); - const privateKey = Buffer.from(this.accessoryInfo.signSk); - const serverProof = tweetnacl.sign.detached(material, privateKey); - const message = tlv.encode(TLVValues.USERNAME, usernameData, TLVValues.PUBLIC_KEY, serverLTPK, TLVValues.PROOF, serverProof); - - const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from("PS-Msg06"), null, message); + debug('[%s] Pair step 5/5', this.accessoryInfo.username) + const S_private = connection.srpServer!.computeK() + const accessorySalt = Buffer.from('Pair-Setup-Accessory-Sign-Salt') + const accessoryInfo = Buffer.from('Pair-Setup-Accessory-Sign-Info') + const outputKey = HKDF('sha512', accessorySalt, S_private, accessoryInfo, 32) + const serverLTPK = this.accessoryInfo.signPk + const usernameData = Buffer.from(this.accessoryInfo.username) + const material = Buffer.concat([outputKey, usernameData, serverLTPK]) + const privateKey = Buffer.from(this.accessoryInfo.signSk) + const serverProof = tweetnacl.sign.detached(material, privateKey) + const message = encode(TLVValues.USERNAME, usernameData, TLVValues.PUBLIC_KEY, serverLTPK, TLVValues.PROOF, serverProof) + + const encrypted = chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from('PS-Msg06'), null, message) // finally, notify listeners that we have been paired with a client - this.emit(HAPServerEventTypes.PAIR, clientUsername.toString(), clientLTPK, once(err => { + this.emit(HAPServerEventTypes.PAIR, clientUsername.toString(), clientLTPK, once((err) => { if (err) { - debug("[%s] Error adding pairing info: %s", this.accessoryInfo.username, err.message); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)); - connection._pairSetupState = undefined; - return; + debug('[%s] Error adding pairing info: %s', this.accessoryInfo.username, err.message) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)) + connection._pairSetupState = undefined + return } // send final pairing response to client - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ENCRYPTED_DATA, Buffer.concat([encrypted.ciphertext, encrypted.authTag]))); - connection._pairSetupState = undefined; - })); + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ENCRYPTED_DATA, Buffer.concat([encrypted.ciphertext, encrypted.authTag]))) + connection._pairSetupState = undefined + })) } private handlePairVerify(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { - const tlvData = tlv.decode(data); - const sequence = tlvData[TLVValues.SEQUENCE_NUM][0]; // value is single byte with sequence number + const tlvData = decode(data) + const sequence = tlvData[TLVValues.SEQUENCE_NUM][0] // value is single byte with sequence number if (sequence === PairingStates.M1) { - this.handlePairVerifyM1(connection, request, response, tlvData); + this.handlePairVerifyM1(connection, request, response, tlvData) } else if (sequence === PairingStates.M3 && connection._pairVerifyState === PairingStates.M2) { - this.handlePairVerifyM3(connection, request, response, tlvData); + this.handlePairVerifyM3(connection, request, response, tlvData) } else { // Invalid state/sequence number - response.writeHead(HAPPairingHTTPCode.BAD_REQUEST, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, sequence + 1, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)); - return; + response.writeHead(HAPPairingHTTPCode.BAD_REQUEST, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, sequence + 1, TLVValues.ERROR_CODE, TLVErrorCode.UNKNOWN)) } } private handlePairVerifyM1(connection: HAPConnection, request: IncomingMessage, response: ServerResponse, tlvData: Record): void { - debug("[%s] Pair verify step 1/2", this.accessoryInfo.username); - const clientPublicKey = tlvData[TLVValues.PUBLIC_KEY]; // Buffer + debug('[%s] Pair verify step 1/2', this.accessoryInfo.username) + const clientPublicKey = tlvData[TLVValues.PUBLIC_KEY] // Buffer // generate new encryption keys for this session - const keyPair = hapCrypto.generateCurve25519KeyPair(); - const secretKey = Buffer.from(keyPair.secretKey); - const publicKey = Buffer.from(keyPair.publicKey); - const sharedSec = Buffer.from(hapCrypto.generateCurve25519SharedSecKey(secretKey, clientPublicKey)); - const usernameData = Buffer.from(this.accessoryInfo.username); - const material = Buffer.concat([publicKey, usernameData, clientPublicKey]); - const privateKey = Buffer.from(this.accessoryInfo.signSk); - const serverProof = tweetnacl.sign.detached(material, privateKey); - const encSalt = Buffer.from("Pair-Verify-Encrypt-Salt"); - const encInfo = Buffer.from("Pair-Verify-Encrypt-Info"); - const outputKey = hapCrypto.HKDF("sha512", encSalt, sharedSec, encInfo, 32).slice(0, 32); - - connection.encryption = new HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSec, outputKey); + const keyPair = generateCurve25519KeyPair() + const secretKey = Buffer.from(keyPair.secretKey) + const publicKey = Buffer.from(keyPair.publicKey) + const sharedSec = Buffer.from(generateCurve25519SharedSecKey(secretKey, clientPublicKey)) + const usernameData = Buffer.from(this.accessoryInfo.username) + const material = Buffer.concat([publicKey, usernameData, clientPublicKey]) + const privateKey = Buffer.from(this.accessoryInfo.signSk) + const serverProof = tweetnacl.sign.detached(material, privateKey) + const encSalt = Buffer.from('Pair-Verify-Encrypt-Salt') + const encInfo = Buffer.from('Pair-Verify-Encrypt-Info') + const outputKey = HKDF('sha512', encSalt, sharedSec, encInfo, 32).subarray(0, 32) + + connection.encryption = new HAPEncryption(clientPublicKey, secretKey, publicKey, sharedSec, outputKey) // compose the response data in TLV format - const message = tlv.encode(TLVValues.USERNAME, usernameData, TLVValues.PROOF, serverProof); - - const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(outputKey, Buffer.from("PV-Msg02"), null, message); - - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode( - TLVValues.SEQUENCE_NUM, PairingStates.M2, - TLVValues.ENCRYPTED_DATA, Buffer.concat([encrypted.ciphertext, encrypted.authTag]), - TLVValues.PUBLIC_KEY, publicKey, - )); - connection._pairVerifyState = PairingStates.M2; + const message = encode(TLVValues.USERNAME, usernameData, TLVValues.PROOF, serverProof) + + const encrypted = chacha20_poly1305_encryptAndSeal(outputKey, Buffer.from('PV-Msg02'), null, message) + + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode( + TLVValues.SEQUENCE_NUM, + PairingStates.M2, + TLVValues.ENCRYPTED_DATA, + Buffer.concat([encrypted.ciphertext, encrypted.authTag]), + TLVValues.PUBLIC_KEY, + publicKey, + )) + connection._pairVerifyState = PairingStates.M2 } private handlePairVerifyM3(connection: HAPConnection, request: IncomingMessage, response: ServerResponse, objects: Record): void { - debug("[%s] Pair verify step 2/2", this.accessoryInfo.username); - const encryptedData = objects[TLVValues.ENCRYPTED_DATA]; - const messageData = Buffer.alloc(encryptedData.length - 16); - const authTagData = Buffer.alloc(16); - encryptedData.copy(messageData, 0, 0, encryptedData.length - 16); - encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length); + debug('[%s] Pair verify step 2/2', this.accessoryInfo.username) + const encryptedData = objects[TLVValues.ENCRYPTED_DATA] + const messageData = Buffer.alloc(encryptedData.length - 16) + const authTagData = Buffer.alloc(16) + encryptedData.copy(messageData, 0, 0, encryptedData.length - 16) + encryptedData.copy(authTagData, 0, encryptedData.length - 16, encryptedData.length) // instance of HAPEncryption (created in handlePairVerifyStepOne) - const enc = connection.encryption!; + const enc = connection.encryption! - let plaintext; + let plaintext try { - plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(enc.hkdfPairEncryptionKey, Buffer.from("PV-Msg03"), null, messageData, authTagData); + plaintext = chacha20_poly1305_decryptAndVerify(enc.hkdfPairEncryptionKey, Buffer.from('PV-Msg03'), null, messageData, authTagData) } catch (error) { - debug("[%s] M3: Failed to decrypt and/or verify", this.accessoryInfo.username); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairVerifyState = undefined; - return; + debug('[%s] M3: Failed to decrypt and/or verify', this.accessoryInfo.username) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairVerifyState = undefined + return } - const decoded = tlv.decode(plaintext); - const clientUsername = decoded[TLVValues.USERNAME]; - const proof = decoded[TLVValues.PROOF]; - const material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]); + const decoded = decode(plaintext) + const clientUsername = decoded[TLVValues.USERNAME] + const proof = decoded[TLVValues.PROOF] + const material = Buffer.concat([enc.clientPublicKey, clientUsername, enc.publicKey]) // since we're paired, we should have the public key stored for this client - const clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString()); + const clientPublicKey = this.accessoryInfo.getClientPublicKey(clientUsername.toString()) // if we're not actually paired, then there's nothing to verify - this client thinks it's paired with us, but we // disagree. Respond with invalid request (seems to match HomeKit Accessory Simulator behavior) if (!clientPublicKey) { - debug("[%s] Client %s attempting to verify, but we are not paired; rejecting client", this.accessoryInfo.username, clientUsername); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairVerifyState = undefined; - return; + debug('[%s] Client %s attempting to verify, but we are not paired; rejecting client', this.accessoryInfo.username, clientUsername) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairVerifyState = undefined + return } if (!tweetnacl.sign.detached.verify(material, proof, clientPublicKey)) { - debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)); - connection._pairVerifyState = undefined; - return; + debug('[%s] Client %s provided an invalid signature', this.accessoryInfo.username, clientUsername) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION)) + connection._pairVerifyState = undefined + return } - debug("[%s] Client %s verification complete", this.accessoryInfo.username, clientUsername); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M4)); + debug('[%s] Client %s verification complete', this.accessoryInfo.username, clientUsername) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.SEQUENCE_NUM, PairingStates.M4)) // now that the client has been verified, we must "upgrade" our pseudo-HTTP connection to include // TCP-level encryption. We'll do this by adding some more encryption vars to the session, and using them // in future calls to onEncrypt, onDecrypt. - const encSalt = Buffer.from("Control-Salt"); - const infoRead = Buffer.from("Control-Read-Encryption-Key"); - const infoWrite = Buffer.from("Control-Write-Encryption-Key"); - enc.accessoryToControllerKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoRead, 32); - enc.controllerToAccessoryKey = hapCrypto.HKDF("sha512", encSalt, enc.sharedSecret, infoWrite, 32); + const encSalt = Buffer.from('Control-Salt') + const infoRead = Buffer.from('Control-Read-Encryption-Key') + const infoWrite = Buffer.from('Control-Write-Encryption-Key') + enc.accessoryToControllerKey = HKDF('sha512', encSalt, enc.sharedSecret, infoRead, 32) + enc.controllerToAccessoryKey = HKDF('sha512', encSalt, enc.sharedSecret, infoWrite, 32) // Our connection is now completely setup. We now want to subscribe this connection to special - connection.connectionAuthenticated(clientUsername.toString()); - connection._pairVerifyState = undefined; + connection.connectionAuthenticated(clientUsername.toString()) + connection._pairVerifyState = undefined } private handlePairings(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { // Only accept /pairing request if there is a secure session if (!this.allowInsecureRequest && !connection.isAuthenticated()) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } - const objects = tlv.decode(data); - const method = objects[TLVValues.METHOD][0]; // value is single byte with request type + const objects = decode(data) + const method = objects[TLVValues.METHOD][0] // value is single byte with request type - const state = objects[TLVValues.STATE][0]; + const state = objects[TLVValues.STATE][0] if (state !== PairingStates.M1) { - return; + return } if (method === PairMethods.ADD_PAIRING) { - const identifier = objects[TLVValues.IDENTIFIER].toString(); - const publicKey = objects[TLVValues.PUBLIC_KEY]; - const permissions = objects[TLVValues.PERMISSIONS][0] as PermissionTypes; + const identifier = objects[TLVValues.IDENTIFIER].toString() + const publicKey = objects[TLVValues.PUBLIC_KEY] + const permissions = objects[TLVValues.PERMISSIONS][0] as PermissionTypes this.emit(HAPServerEventTypes.ADD_PAIRING, connection, identifier, publicKey, permissions, once((error: TLVErrorCode | 0) => { if (error > 0) { - debug("[%s] Pairings: failed ADD_PAIRING with code %d", this.accessoryInfo.username, error); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)); - return; + debug('[%s] Pairings: failed ADD_PAIRING with code %d', this.accessoryInfo.username, error) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)) + return } - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2)); - debug("[%s] Pairings: successfully executed ADD_PAIRING", this.accessoryInfo.username); - })); + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2)) + debug('[%s] Pairings: successfully executed ADD_PAIRING', this.accessoryInfo.username) + })) } else if (method === PairMethods.REMOVE_PAIRING) { - const identifier = objects[TLVValues.IDENTIFIER].toString(); + const identifier = objects[TLVValues.IDENTIFIER].toString() this.emit(HAPServerEventTypes.REMOVE_PAIRING, connection, identifier, once((error: TLVErrorCode | 0) => { if (error > 0) { - debug("[%s] Pairings: failed REMOVE_PAIRING with code %d", this.accessoryInfo.username, error); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)); - return; + debug('[%s] Pairings: failed REMOVE_PAIRING with code %d', this.accessoryInfo.username, error) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)) + return } - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2)); - debug("[%s] Pairings: successfully executed REMOVE_PAIRING", this.accessoryInfo.username); - })); + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2)) + debug('[%s] Pairings: successfully executed REMOVE_PAIRING', this.accessoryInfo.username) + })) } else if (method === PairMethods.LIST_PAIRINGS) { this.emit(HAPServerEventTypes.LIST_PAIRINGS, connection, once((error: TLVErrorCode | 0, data?: PairingInformation[]) => { if (error > 0) { - debug("[%s] Pairings: failed LIST_PAIRINGS with code %d", this.accessoryInfo.username, error); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" }); - response.end(tlv.encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)); - return; + debug('[%s] Pairings: failed LIST_PAIRINGS with code %d', this.accessoryInfo.username, error) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': 'application/pairing+tlv8' }) + response.end(encode(TLVValues.STATE, PairingStates.M2, TLVValues.ERROR_CODE, error)) + return } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tlvList = [] as any[]; + const tlvList = [] as any[] data!.forEach((value: PairingInformation, index: number) => { if (index > 0) { - tlvList.push(TLVValues.SEPARATOR, Buffer.alloc(0)); + tlvList.push(TLVValues.SEPARATOR, Buffer.alloc(0)) } tlvList.push( - TLVValues.IDENTIFIER, value.username, - TLVValues.PUBLIC_KEY, value.publicKey, - TLVValues.PERMISSIONS, value.permission, - ); - }); - - const list = tlv.encode(TLVValues.STATE, PairingStates.M2, ...tlvList); - response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": HAPMimeTypes.PAIRING_TLV8 }); - response.end(list); - debug("[%s] Pairings: successfully executed LIST_PAIRINGS", this.accessoryInfo.username); - })); + TLVValues.IDENTIFIER, + value.username, + TLVValues.PUBLIC_KEY, + value.publicKey, + TLVValues.PERMISSIONS, + value.permission, + ) + }) + + const list = encode(TLVValues.STATE, PairingStates.M2, ...tlvList) + response.writeHead(HAPPairingHTTPCode.OK, { 'Content-Type': HAPMimeTypes.PAIRING_TLV8 }) + response.end(list) + debug('[%s] Pairings: successfully executed LIST_PAIRINGS', this.accessoryInfo.username) + })) } } private handleAccessories(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } // call out to listeners to retrieve the latest accessories JSON this.emit(HAPServerEventTypes.ACCESSORIES, connection, once((error, result) => { if (error) { - response.writeHead(error.httpCode, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: error.status })); + response.writeHead(error.httpCode, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: error.status })) } else { - response.writeHead(HAPHTTPCode.OK, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify(result)); + response.writeHead(HAPHTTPCode.OK, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify(result)) } - })); + })) } private handleCharacteristics(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } - if (request.method === "GET") { - const searchParams = url.searchParams; + if (request.method === 'GET') { + const searchParams = url.searchParams - const idParam = searchParams.get("id"); + const idParam = searchParams.get('id') if (!idParam) { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); - return; + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) + return } - const ids: CharacteristicId[] = []; - for (const entry of idParam.split(",")) { // ["1.9","2.14"] - const split = entry.split("."); // ["1","9"] + const ids: CharacteristicId[] = [] + for (const entry of idParam.split(',')) { // ["1.9","2.14"] + const split = entry.split('.') // ["1","9"] ids.push({ - aid: parseInt(split[0], 10), // accessory id - iid: parseInt(split[1], 10), // (characteristic) instance id - }); + aid: Number.parseInt(split[0], 10), // accessory id + iid: Number.parseInt(split[1], 10), // (characteristic) instance id + }) } const readRequest: CharacteristicsReadRequest = { - ids: ids, - includeMeta: consideredTrue(searchParams.get("meta")), - includePerms: consideredTrue(searchParams.get("perms")), - includeType: consideredTrue(searchParams.get("type")), - includeEvent: consideredTrue(searchParams.get("ev")), - }; + ids, + includeMeta: consideredTrue(searchParams.get('meta')), + includePerms: consideredTrue(searchParams.get('perms')), + includeType: consideredTrue(searchParams.get('type')), + includeEvent: consideredTrue(searchParams.get('ev')), + } this.emit( HAPServerEventTypes.GET_CHARACTERISTICS, @@ -896,49 +911,49 @@ export class HAPServer extends EventEmitter { readRequest, once((error, readResponse) => { if (error) { - response.writeHead(error.httpCode, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: error.status })); - return; + response.writeHead(error.httpCode, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: error.status })) + return } - const characteristics = readResponse!.characteristics; + const characteristics = readResponse!.characteristics - let errorOccurred = false; // determine if we send a 207 Multi-Status + let errorOccurred = false // determine if we send a 207 Multi-Status for (const data of characteristics) { if (data.status) { - errorOccurred = true; - break; + errorOccurred = true + break } } if (errorOccurred) { // on a 207 Multi-Status EVERY characteristic MUST include a status property for (const data of characteristics) { if (!data.status) { // a status is undefined if the request was successful - data.status = HAPStatus.SUCCESS; // a value of zero indicates success + data.status = HAPStatus.SUCCESS // a value of zero indicates success } } } // 207 "multi-status" is returned when an error occurs reading a characteristic. otherwise 200 is returned - response.writeHead(errorOccurred? HAPHTTPCode.MULTI_STATUS: HAPHTTPCode.OK, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ characteristics: characteristics })); + response.writeHead(errorOccurred ? HAPHTTPCode.MULTI_STATUS : HAPHTTPCode.OK, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ characteristics })) }), - ); - } else if (request.method === "PUT") { + ) + } else if (request.method === 'PUT') { if (!connection.isAuthenticated()) { if (!request.headers || (request.headers && request.headers.authorization !== this.accessoryInfo.pincode)) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } } if (data.length === 0) { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); - return; + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) + return } - const writeRequest = JSON.parse(data.toString("utf8")) as CharacteristicsWriteRequest; + const writeRequest = JSON.parse(data.toString('utf8')) as CharacteristicsWriteRequest this.emit( HAPServerEventTypes.SET_CHARACTERISTICS, @@ -946,112 +961,110 @@ export class HAPServer extends EventEmitter { writeRequest, once((error, writeResponse) => { if (error) { - response.writeHead(error.httpCode, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: error.status })); - return; + response.writeHead(error.httpCode, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: error.status })) + return } - const characteristics = writeResponse!.characteristics; + const characteristics = writeResponse!.characteristics - let multiStatus = false; + let multiStatus = false for (const data of characteristics) { if (data.status || data.value !== undefined) { // also send multiStatus on write response requests - multiStatus = true; - break; + multiStatus = true + break } } if (multiStatus) { // 207 is "multi-status" since HomeKit may be setting multiple things and any one can fail independently - response.writeHead(HAPHTTPCode.MULTI_STATUS, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ characteristics: characteristics })); + response.writeHead(HAPHTTPCode.MULTI_STATUS, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ characteristics })) } else { // if everything went fine send 204 no content response - response.writeHead(HAPHTTPCode.NO_CONTENT); - response.end(); + response.writeHead(HAPHTTPCode.NO_CONTENT) + response.end() } }), - ); + ) } else { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); // method not allowed - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) // method not allowed + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) } } private handlePrepareWrite(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { if (!this.allowInsecureRequest && !connection.isAuthenticated()) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } - if (request.method === "PUT") { + if (request.method === 'PUT') { if (data.length === 0) { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); - return; + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) + return } - const prepareRequest = JSON.parse(data.toString()) as PrepareWriteRequest; + const prepareRequest = JSON.parse(data.toString()) as PrepareWriteRequest if (prepareRequest.pid && prepareRequest.ttl) { - debug("[%s] Received prepare write request with pid %d and ttl %d", this.accessoryInfo.username, prepareRequest.pid, prepareRequest.ttl); + debug('[%s] Received prepare write request with pid %d and ttl %d', this.accessoryInfo.username, prepareRequest.pid, prepareRequest.ttl) if (connection.timedWriteTimeout) { // clear any currently existing timeouts - clearTimeout(connection.timedWriteTimeout); + clearTimeout(connection.timedWriteTimeout) } - connection.timedWritePid = prepareRequest.pid; + connection.timedWritePid = prepareRequest.pid connection.timedWriteTimeout = setTimeout(() => { - debug("[%s] Timed write request timed out for pid %d", this.accessoryInfo.username, prepareRequest.pid); - connection.timedWritePid = undefined; - connection.timedWriteTimeout = undefined; - }, prepareRequest.ttl); - - response.writeHead(HAPHTTPCode.OK, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.SUCCESS })); - return; + debug('[%s] Timed write request timed out for pid %d', this.accessoryInfo.username, prepareRequest.pid) + connection.timedWritePid = undefined + connection.timedWriteTimeout = undefined + }, prepareRequest.ttl) + + response.writeHead(HAPHTTPCode.OK, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.SUCCESS })) } else { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) } } else { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) } } private handleResource(connection: HAPConnection, url: URL, request: IncomingMessage, data: Buffer, response: ServerResponse): void { if (!connection.isAuthenticated()) { if (!(this.allowInsecureRequest && request.headers && request.headers.authorization === this.accessoryInfo.pincode)) { - response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })); - return; + response.writeHead(HAPPairingHTTPCode.CONNECTION_AUTHORIZATION_REQUIRED, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INSUFFICIENT_PRIVILEGES })) + return } } - if (request.method === "POST") { + if (request.method === 'POST') { if (data.length === 0) { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); - return; + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) + return } - const resourceRequest = JSON.parse(data.toString()) as ResourceRequest; + const resourceRequest = JSON.parse(data.toString()) as ResourceRequest // call out to listeners to retrieve the resource, snapshot only right now this.emit(HAPServerEventTypes.REQUEST_RESOURCE, resourceRequest, once((error, resource) => { if (error) { - response.writeHead(error.httpCode, { "Content-Type": HAPMimeTypes.HAP_JSON }); - response.end(JSON.stringify({ status: error.status })); + response.writeHead(error.httpCode, { 'Content-Type': HAPMimeTypes.HAP_JSON }) + response.end(JSON.stringify({ status: error.status })) } else { - response.writeHead(HAPHTTPCode.OK, { "Content-Type": HAPMimeTypes.IMAGE_JPEG }); - response.end(resource); + response.writeHead(HAPHTTPCode.OK, { 'Content-Type': HAPMimeTypes.IMAGE_JPEG }) + response.end(resource) } - })); + })) } else { - response.writeHead(HAPHTTPCode.BAD_REQUEST, { "Content-Type": HAPMimeTypes.HAP_JSON }); // method not allowed - response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })); + response.writeHead(HAPHTTPCode.BAD_REQUEST, { 'Content-Type': HAPMimeTypes.HAP_JSON }) // method not allowed + response.end(JSON.stringify({ status: HAPStatus.INVALID_VALUE_IN_REQUEST })) } } - } diff --git a/src/lib/Service.spec.ts b/src/lib/Service.spec.ts index d1fb4cf00..a17910e83 100644 --- a/src/lib/Service.spec.ts +++ b/src/lib/Service.spec.ts @@ -1,136 +1,138 @@ -import { Characteristic } from "./Characteristic"; -import { SerializedService, Service } from "./Service"; -import * as uuid from "./util/uuid"; +import type { SerializedService } from './Service' +import { describe, expect, it } from 'vitest' -const createService = () => { - return new Service("Test", uuid.generate("Foo"), "subtype"); -}; +import { Characteristic } from './Characteristic.js' +import { Service } from './Service.js' +import { generate } from './util/uuid.js' -describe("Service", () => { +function createService() { + return new Service('Test', generate('Foo'), 'subtype') +} - describe("#constructor()", () => { - it("should set the name characteristic to the display name", () => { - const service = createService(); - expect(service.getCharacteristic(Characteristic.Name)!.value).toEqual("Test"); - }); +describe('service', () => { + describe('#constructor()', () => { + it('should set the name characteristic to the display name', () => { + const service = createService() + expect(service.getCharacteristic(Characteristic.Name)!.value).toEqual('Test') + }) - it("should fail to load with no UUID", () => { + it('should fail to load with no UUID', () => { expect(() => { - new Service("Test", "", "subtype"); - }).toThrow("valid UUID"); - }); - }); - - describe("#serialize", () => { - it("should serialize service", () => { - const service = new Service.Lightbulb("TestLight", "subTypeLight"); - service.isHiddenService = true; - service.isPrimaryService = true; - - const json = Service.serialize(service); - expect(json.displayName).toEqual(service.displayName); - expect(json.UUID).toEqual(service.UUID); - expect(json.subtype).toEqual(service.subtype); - expect(json.hiddenService).toEqual(service.isHiddenService); - expect(json.primaryService).toEqual(service.isPrimaryService); + new Service('Test', '', 'subtype') // eslint-disable-line no-new + }).toThrow('valid UUID') + }) + }) + + describe('#serialize', () => { + it('should serialize service', () => { + const service = new Service.Lightbulb('TestLight', 'subTypeLight') + service.isHiddenService = true + service.isPrimaryService = true + + const json = Service.serialize(service) + expect(json.displayName).toEqual(service.displayName) + expect(json.UUID).toEqual(service.UUID) + expect(json.subtype).toEqual(service.subtype) + expect(json.hiddenService).toEqual(service.isHiddenService) + expect(json.primaryService).toEqual(service.isPrimaryService) // just count the elements. If those characteristics are serialized correctly is tested in the Characteristic.spec - expect(service.characteristics).toBeDefined(); - expect(json.characteristics.length).toEqual(service.characteristics.length); - expect(service.optionalCharacteristics).toBeDefined(); - expect(json.optionalCharacteristics!.length).toEqual(service.optionalCharacteristics.length); - }); - - it("should serialize service with proper constructor name", () => { - const service = new Service.Speaker("Speaker Name"); - - const json = Service.serialize(service); - expect(json.constructorName).toBe("Speaker"); - }); - }); - - describe("#deserialize", () => { - it("should deserialize legacy json from homebridge", () => { - const json = JSON.parse("{\"displayName\":\"Test Light\",\"UUID\":\"00000043-0000-1000-8000-0026BB765291\"," + - "\"characteristics\":[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"Test Light\",\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"On\",\"UUID\":\"00000025-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":false,\"eventOnlyCharacteristic\":false}]}"); - const service = Service.deserialize(json); - - expect(service.displayName).toEqual(json.displayName); - expect(service.UUID).toEqual(json.UUID); - expect(service.subtype).toBeUndefined(); - expect(service.isHiddenService).toEqual(false); - expect(service.isPrimaryService).toEqual(false); + expect(service.characteristics).toBeDefined() + expect(json.characteristics.length).toEqual(service.characteristics.length) + expect(service.optionalCharacteristics).toBeDefined() + expect(json.optionalCharacteristics!.length).toEqual(service.optionalCharacteristics.length) + }) + + it('should serialize service with proper constructor name', () => { + const service = new Service.Speaker('Speaker Name') + + const json = Service.serialize(service) + expect(json.constructorName).toBe('Speaker') + }) + }) + + describe('#deserialize', () => { + it('should deserialize legacy json from homebridge', () => { + const json = JSON.parse('{"displayName":"Test Light","UUID":"00000043-0000-1000-8000-0026BB765291",' + + '"characteristics":[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"Test Light","eventOnlyCharacteristic":false},' + + '{"displayName":"On","UUID":"00000025-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr","pw","ev"]},' + + '"value":false,"eventOnlyCharacteristic":false}]}') + const service = Service.deserialize(json) + + expect(service.displayName).toEqual(json.displayName) + expect(service.UUID).toEqual(json.UUID) + expect(service.subtype).toBeUndefined() + expect(service.isHiddenService).toEqual(false) + expect(service.isPrimaryService).toEqual(false) // just count the elements. If those characteristics are serialized correctly is tested in the Characteristic.spec - expect(service.characteristics).toBeDefined(); - expect(service.characteristics.length).toEqual(2); - expect(service.optionalCharacteristics).toBeDefined(); - expect(service.optionalCharacteristics!.length).toEqual(0); // homebridge didn't save those - }); + expect(service.characteristics).toBeDefined() + expect(service.characteristics.length).toEqual(2) + expect(service.optionalCharacteristics).toBeDefined() + expect(service.optionalCharacteristics!.length).toEqual(0) // homebridge didn't save those + }) - it("should deserialize complete json", () => { + it('should deserialize complete json', () => { // json for a light accessory - const json = JSON.parse("{\"displayName\":\"TestLight\",\"UUID\":\"00000043-0000-1000-8000-0026BB765291\"," + - "\"subtype\":\"subTypeLight\",\"hiddenService\":true,\"primaryService\":true," + - "\"characteristics\":[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"TestLight\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"On\",\"UUID\":\"00000025-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"bool\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":false,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]," + - "\"optionalCharacteristics\":[{\"displayName\":\"Brightness\",\"UUID\":\"00000008-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"int\",\"unit\":\"percentage\",\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Hue\",\"UUID\":\"00000013-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"float\",\"unit\":\"arcdegrees\",\"minValue\":0,\"maxValue\":360,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Saturation\",\"UUID\":\"0000002F-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"float\",\"unit\":\"percentage\",\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":0,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"string\",\"unit\":null,\"minValue\":null,\"maxValue\":null,\"minStep\":null,\"perms\":[\"pr\"]}," + - "\"value\":\"\",\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}," + - "{\"displayName\":\"Color Temperature\",\"UUID\":\"000000CE-0000-1000-8000-0026BB765291\"," + - "\"props\":{\"format\":\"uint32\",\"unit\":null,\"minValue\":140,\"maxValue\":500,\"minStep\":1,\"perms\":[\"pr\",\"pw\",\"ev\"]}," + - "\"value\":140,\"accessRestrictedToAdmins\":[],\"eventOnlyCharacteristic\":false}]}"); - - const service = Service.deserialize(json); - - expect(service.displayName).toEqual(json.displayName); - expect(service.UUID).toEqual(json.UUID); - expect(service.subtype).toEqual(json.subtype); - expect(service.isHiddenService).toEqual(true); - expect(service.isPrimaryService).toEqual(true); + const json = JSON.parse('{"displayName":"TestLight","UUID":"00000043-0000-1000-8000-0026BB765291",' + + '"subtype":"subTypeLight","hiddenService":true,"primaryService":true,' + + '"characteristics":[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"TestLight","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"On","UUID":"00000025-0000-1000-8000-0026BB765291",' + + '"props":{"format":"bool","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr","pw","ev"]},' + + '"value":false,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}],' + + '"optionalCharacteristics":[{"displayName":"Brightness","UUID":"00000008-0000-1000-8000-0026BB765291",' + + '"props":{"format":"int","unit":"percentage","minValue":0,"maxValue":100,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Hue","UUID":"00000013-0000-1000-8000-0026BB765291",' + + '"props":{"format":"float","unit":"arcdegrees","minValue":0,"maxValue":360,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Saturation","UUID":"0000002F-0000-1000-8000-0026BB765291",' + + '"props":{"format":"float","unit":"percentage","minValue":0,"maxValue":100,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":0,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291",' + + '"props":{"format":"string","unit":null,"minValue":null,"maxValue":null,"minStep":null,"perms":["pr"]},' + + '"value":"","accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false},' + + '{"displayName":"Color Temperature","UUID":"000000CE-0000-1000-8000-0026BB765291",' + + '"props":{"format":"uint32","unit":null,"minValue":140,"maxValue":500,"minStep":1,"perms":["pr","pw","ev"]},' + + '"value":140,"accessRestrictedToAdmins":[],"eventOnlyCharacteristic":false}]}') + + const service = Service.deserialize(json) + + expect(service.displayName).toEqual(json.displayName) + expect(service.UUID).toEqual(json.UUID) + expect(service.subtype).toEqual(json.subtype) + expect(service.isHiddenService).toEqual(true) + expect(service.isPrimaryService).toEqual(true) // just count the elements. If those characteristics are serialized correctly is tested in the Characteristic.spec - expect(service.characteristics).toBeDefined(); - expect(service.characteristics.length).toEqual(2); // On, Name - expect(service.optionalCharacteristics).toBeDefined(); - expect(service.optionalCharacteristics!.length).toEqual(5); // as defined in the Lightbulb service - }); - - it("should deserialize from json with constructor name", () => { - const json: SerializedService = JSON.parse("{\"displayName\":\"Speaker Name\",\"UUID\":\"00000113-0000-1000-8000-0026BB765291\"," + - "\"constructorName\":\"Speaker\",\"hiddenService\":false,\"primaryService\":false,\"characteristics\":" + - "[{\"displayName\":\"Name\",\"UUID\":\"00000023-0000-1000-8000-0026BB765291\",\"eventOnlyCharacteristic\":false,\"constructorName\":\"Name\"," + - "\"value\":\"Speaker Name\",\"props\":{\"format\":\"string\",\"perms\":[\"pr\"],\"maxLen\":64}},{\"displayName\":\"Mute\"," + - "\"UUID\":\"0000011A-0000-1000-8000-0026BB765291\",\"eventOnlyCharacteristic\":false," + - "\"constructorName\":\"Mute\",\"value\":false,\"props\":{\"format\":\"bool\",\"perms\":[\"ev\",\"pr\",\"pw\"]}}]," + - "\"optionalCharacteristics\":[{\"displayName\":\"Active\",\"UUID\":\"000000B0-0000-1000-8000-0026BB765291\"," + - "\"eventOnlyCharacteristic\":false,\"constructorName\":\"Active\",\"value\":0,\"props\":{\"format\":\"uint8\",\"perms\":[\"ev\",\"pr\",\"pw\"]," + - "\"minValue\":0,\"maxValue\":1,\"minStep\":1}},{\"displayName\":\"Volume\",\"UUID\":\"00000119-0000-1000-8000-0026BB765291\"," + - "\"eventOnlyCharacteristic\":false,\"constructorName\":\"Volume\",\"value\":0,\"props\":{\"format\":\"uint8\",\"perms\":[\"ev\",\"pr\",\"pw\"]," + - "\"unit\":\"percentage\",\"minValue\":0,\"maxValue\":100,\"minStep\":1}}]}"); - - const service = Service.deserialize(json); - - expect(service instanceof Service.Speaker).toBeTruthy(); - }); - }); -}); + expect(service.characteristics).toBeDefined() + expect(service.characteristics.length).toEqual(2) // On, Name + expect(service.optionalCharacteristics).toBeDefined() + expect(service.optionalCharacteristics!.length).toEqual(5) // as defined in the Lightbulb service + }) + + it('should deserialize from json with constructor name', () => { + const json: SerializedService = JSON.parse('{"displayName":"Speaker Name","UUID":"00000113-0000-1000-8000-0026BB765291",' + + '"constructorName":"Speaker","hiddenService":false,"primaryService":false,"characteristics":' + + '[{"displayName":"Name","UUID":"00000023-0000-1000-8000-0026BB765291","eventOnlyCharacteristic":false,"constructorName":"Name",' + + '"value":"Speaker Name","props":{"format":"string","perms":["pr"],"maxLen":64}},{"displayName":"Mute",' + + '"UUID":"0000011A-0000-1000-8000-0026BB765291","eventOnlyCharacteristic":false,' + + '"constructorName":"Mute","value":false,"props":{"format":"bool","perms":["ev","pr","pw"]}}],' + + '"optionalCharacteristics":[{"displayName":"Active","UUID":"000000B0-0000-1000-8000-0026BB765291",' + + '"eventOnlyCharacteristic":false,"constructorName":"Active","value":0,"props":{"format":"uint8","perms":["ev","pr","pw"],' + + '"minValue":0,"maxValue":1,"minStep":1}},{"displayName":"Volume","UUID":"00000119-0000-1000-8000-0026BB765291",' + + '"eventOnlyCharacteristic":false,"constructorName":"Volume","value":0,"props":{"format":"uint8","perms":["ev","pr","pw"],' + + '"unit":"percentage","minValue":0,"maxValue":100,"minStep":1}}]}') + + const service = Service.deserialize(json) + + expect(service instanceof Service.Speaker).toBeTruthy() + }) + }) +}) diff --git a/src/lib/Service.ts b/src/lib/Service.ts index 9548883aa..f5de07878 100644 --- a/src/lib/Service.ts +++ b/src/lib/Service.ts @@ -1,9 +1,7 @@ -import assert from "assert"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { CharacteristicValue, Nullable, ServiceJsonObject, WithUUID } from "../types"; -import { CharacteristicWarning, CharacteristicWarningType } from "./Accessory"; -import { Characteristic, CharacteristicChange, CharacteristicEventTypes, SerializedCharacteristic } from "./Characteristic"; +/* global NodeJS */ +import type { CharacteristicValue, Nullable, ServiceJsonObject, WithUUID } from '../types' +import type { CharacteristicWarning } from './Accessory' +import type { CharacteristicChange, SerializedCharacteristic } from './Characteristic' import type { AccessCode, AccessControl, @@ -76,34 +74,42 @@ import type { WiFiTransport, Window, WindowCovering, -} from "./definitions"; -import { IdentifierCache } from "./model/IdentifierCache"; -import { HAPConnection } from "./util/eventedhttp"; -import { HapStatusError } from "./util/hapStatusError"; -import { toShortForm } from "./util/uuid"; -import { checkName } from "./util/checkName"; +} from './definitions' +import type { IdentifierCache } from './model/IdentifierCache' +import type { HAPConnection } from './util/eventedhttp' +import type { HapStatusError } from './util/hapStatusError' -const debug = createDebug("HAP-NodeJS:Service"); +import assert from 'node:assert' +import { EventEmitter } from 'node:events' + +import createDebug from 'debug' + +import { CharacteristicWarningType } from './Accessory.js' +import { Characteristic, CharacteristicEventTypes } from './Characteristic.js' +import { checkName } from './util/checkName.js' +import { toShortForm } from './util/uuid.js' + +const debug = createDebug('HAP-NodeJS:Service') /** * HAP spec allows a maximum of 100 characteristics per service! */ -const MAX_CHARACTERISTICS = 100; +const MAX_CHARACTERISTICS = 100 /** * @group Service */ export interface SerializedService { - displayName: string, - UUID: string, - subtype?: string, - constructorName?: string, + displayName: string + UUID: string + subtype?: string + constructorName?: string - hiddenService?: boolean, - primaryService?: boolean, + hiddenService?: boolean + primaryService?: boolean - characteristics: SerializedCharacteristic[], - optionalCharacteristics?: SerializedCharacteristic[], + characteristics: SerializedCharacteristic[] + optionalCharacteristics?: SerializedCharacteristic[] } /** @@ -111,34 +117,36 @@ export interface SerializedService { * * @group Service */ -export type ServiceId = string; +export type ServiceId = string /** * @group Service */ -export type ServiceCharacteristicChange = CharacteristicChange & { characteristic: Characteristic }; +export type ServiceCharacteristicChange = CharacteristicChange & { characteristic: Characteristic } /** * @group Service */ +// eslint-disable-next-line no-restricted-syntax export const enum ServiceEventTypes { - CHARACTERISTIC_CHANGE = "characteristic-change", - SERVICE_CONFIGURATION_CHANGE = "service-configurationChange", - CHARACTERISTIC_WARNING = "characteristic-warning", + CHARACTERISTIC_CHANGE = 'characteristic-change', + SERVICE_CONFIGURATION_CHANGE = 'service-configurationChange', + CHARACTERISTIC_WARNING = 'characteristic-warning', } /** * @group Service */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface Service { - on(event: "characteristic-change", listener: (change: ServiceCharacteristicChange) => void): this; - on(event: "service-configurationChange", listener: () => void): this; - on(event: "characteristic-warning", listener: (warning: CharacteristicWarning) => void): this; - - emit(event: "characteristic-change", change: ServiceCharacteristicChange): boolean; - emit(event: "service-configurationChange"): boolean; - emit(event: "characteristic-warning", warning: CharacteristicWarning): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'characteristic-change', listener: (change: ServiceCharacteristicChange) => void): this + on(event: 'service-configurationChange', listener: () => void): this + on(event: 'characteristic-warning', listener: (warning: CharacteristicWarning) => void): this + emit(event: 'characteristic-change', change: ServiceCharacteristicChange): boolean + emit(event: 'service-configurationChange'): boolean + emit(event: 'characteristic-warning', warning: CharacteristicWarning): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -163,7 +171,7 @@ export declare interface Service { * * @group Service */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class Service extends EventEmitter { // Service MUST NOT have any other static variables @@ -172,328 +180,328 @@ export class Service extends EventEmitter { /** * @group Service Definitions */ - public static AccessCode: typeof AccessCode; + public static AccessCode: typeof AccessCode /** * @group Service Definitions */ - public static AccessControl: typeof AccessControl; + public static AccessControl: typeof AccessControl /** * @group Service Definitions */ - public static AccessoryInformation: typeof AccessoryInformation; + public static AccessoryInformation: typeof AccessoryInformation /** * @group Service Definitions */ - public static AccessoryMetrics: typeof AccessoryMetrics; + public static AccessoryMetrics: typeof AccessoryMetrics /** * @group Service Definitions */ - public static AccessoryRuntimeInformation: typeof AccessoryRuntimeInformation; + public static AccessoryRuntimeInformation: typeof AccessoryRuntimeInformation /** * @group Service Definitions */ - public static AirPurifier: typeof AirPurifier; + public static AirPurifier: typeof AirPurifier /** * @group Service Definitions */ - public static AirQualitySensor: typeof AirQualitySensor; + public static AirQualitySensor: typeof AirQualitySensor /** * @group Service Definitions */ - public static AssetUpdate: typeof AssetUpdate; + public static AssetUpdate: typeof AssetUpdate /** * @group Service Definitions */ - public static Assistant: typeof Assistant; + public static Assistant: typeof Assistant /** * @group Service Definitions */ - public static AudioStreamManagement: typeof AudioStreamManagement; + public static AudioStreamManagement: typeof AudioStreamManagement /** * @group Service Definitions */ - public static Battery: typeof Battery; + public static Battery: typeof Battery /** * @group Service Definitions */ - public static CameraOperatingMode: typeof CameraOperatingMode; + public static CameraOperatingMode: typeof CameraOperatingMode /** * @group Service Definitions */ - public static CameraRecordingManagement: typeof CameraRecordingManagement; + public static CameraRecordingManagement: typeof CameraRecordingManagement /** * @group Service Definitions */ - public static CameraRTPStreamManagement: typeof CameraRTPStreamManagement; + public static CameraRTPStreamManagement: typeof CameraRTPStreamManagement /** * @group Service Definitions */ - public static CarbonDioxideSensor: typeof CarbonDioxideSensor; + public static CarbonDioxideSensor: typeof CarbonDioxideSensor /** * @group Service Definitions */ - public static CarbonMonoxideSensor: typeof CarbonMonoxideSensor; + public static CarbonMonoxideSensor: typeof CarbonMonoxideSensor /** * @group Service Definitions */ - public static ContactSensor: typeof ContactSensor; + public static ContactSensor: typeof ContactSensor /** * @group Service Definitions */ - public static DataStreamTransportManagement: typeof DataStreamTransportManagement; + public static DataStreamTransportManagement: typeof DataStreamTransportManagement /** * @group Service Definitions */ - public static Diagnostics: typeof Diagnostics; + public static Diagnostics: typeof Diagnostics /** * @group Service Definitions */ - public static Door: typeof Door; + public static Door: typeof Door /** * @group Service Definitions */ - public static Doorbell: typeof Doorbell; + public static Doorbell: typeof Doorbell /** * @group Service Definitions */ - public static Fan: typeof Fan; + public static Fan: typeof Fan /** * @group Service Definitions */ - public static Fanv2: typeof Fanv2; + public static Fanv2: typeof Fanv2 /** * @group Service Definitions */ - public static Faucet: typeof Faucet; + public static Faucet: typeof Faucet /** * @group Service Definitions */ - public static FilterMaintenance: typeof FilterMaintenance; + public static FilterMaintenance: typeof FilterMaintenance /** * @group Service Definitions */ - public static FirmwareUpdate: typeof FirmwareUpdate; + public static FirmwareUpdate: typeof FirmwareUpdate /** * @group Service Definitions */ - public static GarageDoorOpener: typeof GarageDoorOpener; + public static GarageDoorOpener: typeof GarageDoorOpener /** * @group Service Definitions */ - public static HeaterCooler: typeof HeaterCooler; + public static HeaterCooler: typeof HeaterCooler /** * @group Service Definitions */ - public static HumidifierDehumidifier: typeof HumidifierDehumidifier; + public static HumidifierDehumidifier: typeof HumidifierDehumidifier /** * @group Service Definitions */ - public static HumiditySensor: typeof HumiditySensor; + public static HumiditySensor: typeof HumiditySensor /** * @group Service Definitions */ - public static InputSource: typeof InputSource; + public static InputSource: typeof InputSource /** * @group Service Definitions */ - public static IrrigationSystem: typeof IrrigationSystem; + public static IrrigationSystem: typeof IrrigationSystem /** * @group Service Definitions */ - public static LeakSensor: typeof LeakSensor; + public static LeakSensor: typeof LeakSensor /** * @group Service Definitions */ - public static Lightbulb: typeof Lightbulb; + public static Lightbulb: typeof Lightbulb /** * @group Service Definitions */ - public static LightSensor: typeof LightSensor; + public static LightSensor: typeof LightSensor /** * @group Service Definitions */ - public static LockManagement: typeof LockManagement; + public static LockManagement: typeof LockManagement /** * @group Service Definitions */ - public static LockMechanism: typeof LockMechanism; + public static LockMechanism: typeof LockMechanism /** * @group Service Definitions */ - public static Microphone: typeof Microphone; + public static Microphone: typeof Microphone /** * @group Service Definitions */ - public static MotionSensor: typeof MotionSensor; + public static MotionSensor: typeof MotionSensor /** * @group Service Definitions */ - public static NFCAccess: typeof NFCAccess; + public static NFCAccess: typeof NFCAccess /** * @group Service Definitions */ - public static OccupancySensor: typeof OccupancySensor; + public static OccupancySensor: typeof OccupancySensor /** * @group Service Definitions */ - public static Outlet: typeof Outlet; + public static Outlet: typeof Outlet /** * @group Service Definitions */ - public static Pairing: typeof Pairing; + public static Pairing: typeof Pairing /** * @group Service Definitions */ - public static PowerManagement: typeof PowerManagement; + public static PowerManagement: typeof PowerManagement /** * @group Service Definitions */ - public static ProtocolInformation: typeof ProtocolInformation; + public static ProtocolInformation: typeof ProtocolInformation /** * @group Service Definitions */ - public static SecuritySystem: typeof SecuritySystem; + public static SecuritySystem: typeof SecuritySystem /** * @group Service Definitions */ - public static ServiceLabel: typeof ServiceLabel; + public static ServiceLabel: typeof ServiceLabel /** * @group Service Definitions */ - public static Siri: typeof Siri; + public static Siri: typeof Siri /** * @group Service Definitions */ - public static SiriEndpoint: typeof SiriEndpoint; + public static SiriEndpoint: typeof SiriEndpoint /** * @group Service Definitions */ - public static Slats: typeof Slats; + public static Slats: typeof Slats /** * @group Service Definitions */ - public static SmartSpeaker: typeof SmartSpeaker; + public static SmartSpeaker: typeof SmartSpeaker /** * @group Service Definitions */ - public static SmokeSensor: typeof SmokeSensor; + public static SmokeSensor: typeof SmokeSensor /** * @group Service Definitions */ - public static Speaker: typeof Speaker; + public static Speaker: typeof Speaker /** * @group Service Definitions */ - public static StatefulProgrammableSwitch: typeof StatefulProgrammableSwitch; + public static StatefulProgrammableSwitch: typeof StatefulProgrammableSwitch /** * @group Service Definitions */ - public static StatelessProgrammableSwitch: typeof StatelessProgrammableSwitch; + public static StatelessProgrammableSwitch: typeof StatelessProgrammableSwitch /** * @group Service Definitions */ - public static Switch: typeof Switch; + public static Switch: typeof Switch /** * @group Service Definitions */ - public static TapManagement: typeof TapManagement; + public static TapManagement: typeof TapManagement /** * @group Service Definitions */ - public static TargetControl: typeof TargetControl; + public static TargetControl: typeof TargetControl /** * @group Service Definitions */ - public static TargetControlManagement: typeof TargetControlManagement; + public static TargetControlManagement: typeof TargetControlManagement /** * @group Service Definitions */ - public static Television: typeof Television; + public static Television: typeof Television /** * @group Service Definitions */ - public static TelevisionSpeaker: typeof TelevisionSpeaker; + public static TelevisionSpeaker: typeof TelevisionSpeaker /** * @group Service Definitions */ - public static TemperatureSensor: typeof TemperatureSensor; + public static TemperatureSensor: typeof TemperatureSensor /** * @group Service Definitions */ - public static Thermostat: typeof Thermostat; + public static Thermostat: typeof Thermostat /** * @group Service Definitions */ - public static ThreadTransport: typeof ThreadTransport; + public static ThreadTransport: typeof ThreadTransport /** * @group Service Definitions */ - public static TransferTransportManagement: typeof TransferTransportManagement; + public static TransferTransportManagement: typeof TransferTransportManagement /** * @group Service Definitions */ - public static Valve: typeof Valve; + public static Valve: typeof Valve /** * @group Service Definitions */ - public static WiFiRouter: typeof WiFiRouter; + public static WiFiRouter: typeof WiFiRouter /** * @group Service Definitions */ - public static WiFiSatellite: typeof WiFiSatellite; + public static WiFiSatellite: typeof WiFiSatellite /** * @group Service Definitions */ - public static WiFiTransport: typeof WiFiTransport; + public static WiFiTransport: typeof WiFiTransport /** * @group Service Definitions */ - public static Window: typeof Window; + public static Window: typeof Window /** * @group Service Definitions */ - public static WindowCovering: typeof WindowCovering; + public static WindowCovering: typeof WindowCovering // =-=-=-=-=-=-=-=-=-=-=-=-=-=-= // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions - public displayName: string; - public UUID: string; - subtype?: string; - iid: Nullable = null; // assigned later by our containing Accessory - name: Nullable = null; - characteristics: Characteristic[] = []; - optionalCharacteristics: Characteristic[] = []; + public displayName: string + public UUID: string + subtype?: string + iid: Nullable = null // assigned later by our containing Accessory + name: Nullable = null + characteristics: Characteristic[] = [] + optionalCharacteristics: Characteristic[] = [] /** * @private */ - isHiddenService = false; + isHiddenService = false /** * @private */ - isPrimaryService = false; // do not write to this directly + isPrimaryService = false // do not write to this directly /** * @private */ - linkedServices: Service[] = []; + linkedServices: Service[] = [] - public constructor(displayName = "", UUID: string, subtype?: string) { - super(); - assert(UUID, "Services must be created with a valid UUID."); - this.displayName = displayName; - this.UUID = UUID; - this.subtype = subtype; + public constructor(displayName = '', UUID: string, subtype?: string) { + super() + assert(UUID, 'Services must be created with a valid UUID.') + this.displayName = displayName + this.UUID = UUID + this.subtype = subtype // every service has an optional Characteristic.Name property - we'll set it to our displayName // if one was given // if you don't provide a display name, some HomeKit apps may choose to hide the device. if (displayName) { // create the characteristic if necessary - checkName(this.displayName, "Name", displayName); - const nameCharacteristic = - this.getCharacteristic(Characteristic.Name) || - this.addCharacteristic(Characteristic.Name); + checkName(this.displayName, 'Name', displayName) + const nameCharacteristic + = this.getCharacteristic(Characteristic.Name) + || this.addCharacteristic(Characteristic.Name) - nameCharacteristic.updateValue(displayName); + nameCharacteristic.updateValue(displayName) } } @@ -505,40 +513,39 @@ export class Service extends EventEmitter { * @returns the serviceId */ public getServiceId(): ServiceId { - return this.UUID + (this.subtype || ""); + return this.UUID + (this.subtype || '') } public addCharacteristic(input: Characteristic): Characteristic - // eslint-disable-next-line @typescript-eslint/no-explicit-any public addCharacteristic(input: { new (...args: any[]): Characteristic }, ...constructorArgs: any[]): Characteristic - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public addCharacteristic(input: Characteristic | {new (...args: any[]): Characteristic}, ...constructorArgs: any[]): Characteristic { + public addCharacteristic(input: Characteristic | { new (...args: any[]): Characteristic }, ...constructorArgs: any[]): Characteristic { // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance of Characteristic. Coerce if necessary. - const characteristic = typeof input === "function"? new input(...constructorArgs): input; + // eslint-disable-next-line new-cap + const characteristic = typeof input === 'function' ? new input(...constructorArgs) : input // check for UUID conflict for (const existing of this.characteristics) { if (existing.UUID === characteristic.UUID) { - if (characteristic.UUID === "00000052-0000-1000-8000-0026BB765291") { - //This is a special workaround for the Firmware Revision characteristic. - return existing; + if (characteristic.UUID === '00000052-0000-1000-8000-0026BB765291') { + // This is a special workaround for the Firmware Revision characteristic. + return existing } - throw new Error("Cannot add a Characteristic with the same UUID as another Characteristic in this Service: " + existing.UUID); + throw new Error(`Cannot add a Characteristic with the same UUID as another Characteristic in this Service: ${existing.UUID}`) } } if (this.characteristics.length >= MAX_CHARACTERISTICS) { - throw new Error("Cannot add more than " + MAX_CHARACTERISTICS + " characteristics to a single service!"); + throw new Error(`Cannot add more than ${MAX_CHARACTERISTICS} characteristics to a single service!`) } - this.setupCharacteristicEventHandlers(characteristic); + this.setupCharacteristicEventHandlers(characteristic) - this.characteristics.push(characteristic); + this.characteristics.push(characteristic) - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) - return characteristic; + return characteristic } /** @@ -550,8 +557,8 @@ export class Service extends EventEmitter { * @param isPrimary - optional boolean (default true) if the service should be the primary service */ public setPrimaryService(isPrimary = true): void { - this.isPrimaryService = isPrimary; - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.isPrimaryService = isPrimary + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) } /** @@ -560,8 +567,8 @@ export class Service extends EventEmitter { * @param isHidden - optional boolean (default true) if the service should be marked hidden */ public setHiddenService(isHidden = true): void { - this.isHiddenService = isHidden; - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.isHiddenService = isHidden + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) } /** @@ -571,11 +578,11 @@ export class Service extends EventEmitter { * @param service - The service this service should link to */ public addLinkedService(service: Service): void { - //TODO: Add a check if the service is on the same accessory. + // TODO: Add a check if the service is on the same accessory. if (!this.linkedServices.includes(service)) { - this.linkedServices.push(service); + this.linkedServices.push(service) } - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) } /** @@ -584,76 +591,75 @@ export class Service extends EventEmitter { * @param service - Previously linked service */ public removeLinkedService(service: Service): void { - //TODO: Add a check if the service is on the same accessory. - const index = this.linkedServices.indexOf(service); + // TODO: Add a check if the service is on the same accessory. + const index = this.linkedServices.indexOf(service) if (index !== -1) { - this.linkedServices.splice(index, 1); + this.linkedServices.splice(index, 1) } - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) } public removeCharacteristic(characteristic: Characteristic): void { - const index = this.characteristics.indexOf(characteristic); + const index = this.characteristics.indexOf(characteristic) if (index !== -1) { - this.characteristics.splice(index, 1); - characteristic.removeAllListeners(); + this.characteristics.splice(index, 1) + characteristic.removeAllListeners() - this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE); + this.emit(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE) } } // If a Characteristic constructor is passed a Characteristic object will always be returned - public getCharacteristic(constructor: WithUUID<{new (): Characteristic}>): Characteristic + public getCharacteristic(constructor: WithUUID<{ new (): Characteristic }>): Characteristic // Still support using a Characteristic constructor or a name so "passing though" a value still works // https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types - public getCharacteristic(name: string | WithUUID<{new (): Characteristic}>): Characteristic | undefined - public getCharacteristic(name: string | WithUUID<{new (): Characteristic}>): Characteristic | undefined { + public getCharacteristic(name: string | WithUUID<{ new (): Characteristic }>): Characteristic | undefined + public getCharacteristic(name: string | WithUUID<{ new (): Characteristic }>): Characteristic | undefined { // returns a characteristic object from the service // If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic, // but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it. for (const characteristic of this.characteristics) { - if (typeof name === "string" && characteristic.displayName === name) { - return characteristic; + if (typeof name === 'string' && characteristic.displayName === name) { + return characteristic } else { // @ts-expect-error ('UUID' does not exist on type 'never') - if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { - return characteristic; + if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { + return characteristic } } } - if (typeof name === "function") { + if (typeof name === 'function') { for (const characteristic of this.optionalCharacteristics) { // @ts-expect-error ('UUID' does not exist on type 'never') if ((characteristic instanceof name) || (name.UUID === characteristic.UUID)) { - return this.addCharacteristic(name); + return this.addCharacteristic(name) } } - const instance = this.addCharacteristic(name); + const instance = this.addCharacteristic(name) // Not found in optional Characteristics. Adding anyway, but warning about it if it isn't the Name. if (name.UUID !== Characteristic.Name.UUID) { - this.emitCharacteristicWarningEvent(instance, CharacteristicWarningType.WARN_MESSAGE, - "Characteristic not in required or optional characteristic section for service " + this.constructor.name + ". Adding anyway."); + this.emitCharacteristicWarningEvent(instance, CharacteristicWarningType.WARN_MESSAGE, `Characteristic not in required or optional characteristic section for service ${this.constructor.name}. Adding anyway.`) } - return instance; + return instance } } public testCharacteristic>(name: string | T): boolean { // checks for the existence of a characteristic object in the service for (const characteristic of this.characteristics) { - if (typeof name === "string" && characteristic.displayName === name) { - return true; + if (typeof name === 'string' && characteristic.displayName === name) { + return true } else { // @ts-expect-error ('UUID' does not exist on type 'never') - if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { - return true; + if (typeof name === 'function' && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { + return true } } } - return false; + return false } /** @@ -670,7 +676,7 @@ export class Service extends EventEmitter { * * Note: If you don't want the {@link CharacteristicEventTypes.SET} to be called, refer to {@link updateCharacteristic}. */ - public setCharacteristic>(name: string | T, value: CharacteristicValue): Service; + public setCharacteristic>(name: string | T, value: CharacteristicValue): Service /** * Sets the state of the characteristic to an errored state. * @@ -691,17 +697,17 @@ export class Service extends EventEmitter { * @param error - The error object * * Note: Erroneous state is never **pushed** to the client side. Only, if the HomeKit client requests the current - * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, - * any {@link Characteristic.onGet} or {@link CharacteristicEventTypes.GET} handlers have preference. + * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, + * any {@link Characteristic.onGet} or {@link CharacteristicEventTypes.GET} handlers have preference. */ - public setCharacteristic>(name: string | T, error: HapStatusError | Error): Service - public setCharacteristic>( + public setCharacteristic>(name: string | T, error: HapStatusError | Error): Service + public setCharacteristic>( name: string | T, value: CharacteristicValue | HapStatusError | Error, ): Service { // @ts-expect-error: We know that both overloads exists individually. There is just no publicly exposed type for that! - this.getCharacteristic(name)!.setValue(value); - return this; // for chaining + this.getCharacteristic(name)!.setValue(value) + return this // for chaining } /** @@ -711,7 +717,7 @@ export class Service extends EventEmitter { * @param name - The name or the constructor of the desired {@link Characteristic}. * @param value - The new value. */ - public updateCharacteristic>(name: string | T, value: Nullable): Service; + public updateCharacteristic>(name: string | T, value: Nullable): Service /** * Sets the state of the characteristic to an errored state. * If a {@link Characteristic.onGet} or {@link CharacteristicEventTypes.GET} handler is set up, @@ -729,29 +735,28 @@ export class Service extends EventEmitter { * guide for more information on how to present erroneous state to the user. * * Note: Erroneous state is never **pushed** to the client side. Only, if the HomeKit client requests the current - * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, - * any {@link Characteristic.onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. + * state of the Characteristic, the corresponding {@link HapStatusError} is returned. As described above, + * any {@link Characteristic.onGet} or {@link CharacteristicEventTypes.GET} handlers have precedence. */ - public updateCharacteristic>(name: string | T, error: HapStatusError | Error): Service - public updateCharacteristic>( + public updateCharacteristic>(name: string | T, error: HapStatusError | Error): Service + public updateCharacteristic>( name: string | T, value: Nullable | HapStatusError | Error, ): Service { - this.getCharacteristic(name)!.updateValue(value); - return this; + this.getCharacteristic(name)!.updateValue(value) + return this } - public addOptionalCharacteristic(characteristic: Characteristic | {new (): Characteristic}): void { + public addOptionalCharacteristic(characteristic: Characteristic | { new (): Characteristic }): void { // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance // of Characteristic. Coerce if necessary. - if (typeof characteristic === "function") { - characteristic = new characteristic() as Characteristic; + if (typeof characteristic === 'function') { + characteristic = new characteristic() as Characteristic // eslint-disable-line new-cap } - this.optionalCharacteristics.push(characteristic); + this.optionalCharacteristics.push(characteristic) } - // noinspection JSUnusedGlobalSymbols /** * This method was created to copy all characteristics from another service to this. * It's only adopting is currently in homebridge to merge the AccessoryInformation service. So some things @@ -760,31 +765,31 @@ export class Service extends EventEmitter { * It will not remove characteristics which are present currently but not added on the other characteristic. * It will not replace the characteristic if the value is falsy (except of '0' or 'false') * @param service - * @private used by homebridge + * @private */ replaceCharacteristicsFromService(service: Service): void { if (this.UUID !== service.UUID) { - throw new Error(`Incompatible services. Tried replacing characteristics of ${this.UUID} with characteristics from ${service.UUID}`); + throw new Error(`Incompatible services. Tried replacing characteristics of ${this.UUID} with characteristics from ${service.UUID}`) } - const foreignCharacteristics: Record = {}; // index foreign characteristics by UUID - service.characteristics.forEach(characteristic => foreignCharacteristics[characteristic.UUID] = characteristic); + const foreignCharacteristics: Record = {} // index foreign characteristics by UUID + service.characteristics.forEach(characteristic => foreignCharacteristics[characteristic.UUID] = characteristic) - this.characteristics.forEach(characteristic => { - const foreignCharacteristic = foreignCharacteristics[characteristic.UUID]; + this.characteristics.forEach((characteristic) => { + const foreignCharacteristic = foreignCharacteristics[characteristic.UUID] if (foreignCharacteristic) { - delete foreignCharacteristics[characteristic.UUID]; + delete foreignCharacteristics[characteristic.UUID] if (!foreignCharacteristic.value && foreignCharacteristic.value !== 0 && foreignCharacteristic.value !== false) { - return; // ignore falsy values except if it's the number zero or literally false + return // ignore falsy values except if it's the number zero or literally false } - characteristic.replaceBy(foreignCharacteristic); + characteristic.replaceBy(foreignCharacteristic) } - }); + }) // add all additional characteristics which where not present already - Object.values(foreignCharacteristics).forEach(characteristic => this.addCharacteristic(characteristic)); + Object.values(foreignCharacteristics).forEach(characteristic => this.addCharacteristic(characteristic)) } /** @@ -793,7 +798,7 @@ export class Service extends EventEmitter { getCharacteristicByIID(iid: number): Characteristic | undefined { for (const characteristic of this.characteristics) { if (characteristic.iid === iid) { - return characteristic; + return characteristic } } } @@ -803,38 +808,38 @@ export class Service extends EventEmitter { */ _assignIDs(identifierCache: IdentifierCache, accessoryName: string, baseIID = 0): void { // the Accessory Information service must have a (reserved by IdentifierCache) ID of 1 - if (this.UUID === "0000003E-0000-1000-8000-0026BB765291") { - this.iid = 1; + if (this.UUID === '0000003E-0000-1000-8000-0026BB765291') { + this.iid = 1 } else { // assign our own ID based on our UUID - this.iid = baseIID + identifierCache.getIID(accessoryName, this.UUID, this.subtype); + this.iid = baseIID + identifierCache.getIID(accessoryName, this.UUID, this.subtype) } // assign IIDs to our Characteristics for (const characteristic of this.characteristics) { - characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype); + characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype) } } /** * Returns a JSON representation of this service suitable for delivering to HAP clients. - * @private used to generate response to /accessories query + * @private */ toHAP(connection: HAPConnection, contactGetHandlers = true): Promise { - return new Promise(resolve => { - assert(this.iid, "iid cannot be undefined for service '" + this.displayName + "'"); - assert(this.characteristics.length, "service '" + this.displayName + "' does not have any characteristics!"); + return new Promise((resolve) => { + assert(this.iid, `iid cannot be undefined for service '${this.displayName}'`) + assert(this.characteristics.length, `service '${this.displayName}' does not have any characteristics!`) const service: ServiceJsonObject = { type: toShortForm(this.UUID), iid: this.iid!, characteristics: [], - hidden: this.isHiddenService? true: undefined, - primary: this.isPrimaryService? true: undefined, - }; + hidden: this.isHiddenService ? true : undefined, + primary: this.isPrimaryService ? true : undefined, + } if (this.linkedServices.length) { - service.linked = []; + service.linked = [] for (const linked of this.linkedServices) { if (!linked.iid) { // we got a linked service which is not added to the accessory @@ -842,75 +847,73 @@ export class Service extends EventEmitter { // we have some (at least one) plugins on homebridge which link to the AccessoryInformation service. // homebridge always creates its own AccessoryInformation service and ignores the user supplied one // thus the link is automatically broken. - debug(`iid of linked service '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`); - continue; + debug(`iid of linked service '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`) + continue } - service.linked.push(linked.iid!); + service.linked.push(linked.iid!) } } - const missingCharacteristics: Set = new Set(); + const missingCharacteristics: Set = new Set() let timeout: NodeJS.Timeout | undefined = setTimeout(() => { for (const characteristic of missingCharacteristics) { - this.emitCharacteristicWarningEvent(characteristic, CharacteristicWarningType.SLOW_READ, - `The read handler for the characteristic '${characteristic.displayName}' was slow to respond!`); + this.emitCharacteristicWarningEvent(characteristic, CharacteristicWarningType.SLOW_READ, `The read handler for the characteristic '${characteristic.displayName}' was slow to respond!`) } timeout = setTimeout(() => { - timeout = undefined; + timeout = undefined for (const characteristic of missingCharacteristics) { - this.emitCharacteristicWarningEvent(characteristic, CharacteristicWarningType.TIMEOUT_READ, - "The read handler for the characteristic '" + characteristic?.displayName + - "' didn't respond at all!. Please check that you properly call the callback!"); - service.characteristics.push(characteristic.internalHAPRepresentation()); // value is set to null + this.emitCharacteristicWarningEvent(characteristic, CharacteristicWarningType.TIMEOUT_READ, `The read handler for the characteristic '${characteristic?.displayName + }' didn't respond at all!. Please check that you properly call the callback!`) + service.characteristics.push(characteristic.internalHAPRepresentation()) // value is set to null } - missingCharacteristics.clear(); - resolve(service); - }, 6000); - }, 3000); + missingCharacteristics.clear() + resolve(service) + }, 6000) + }, 3000) for (const characteristic of this.characteristics) { - missingCharacteristics.add(characteristic); - characteristic.toHAP(connection, contactGetHandlers).then(value => { + missingCharacteristics.add(characteristic) + characteristic.toHAP(connection, contactGetHandlers).then((value) => { if (!timeout) { - return; // if timeout is undefined, response was already sent out + return // if timeout is undefined, response was already sent out } - missingCharacteristics.delete(characteristic); - service.characteristics.push(value); + missingCharacteristics.delete(characteristic) + service.characteristics.push(value) if (missingCharacteristics.size === 0) { if (timeout) { - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout) + timeout = undefined } - resolve(service); + resolve(service) } - }); + }) } - }); + }) } /** * Returns a JSON representation of this service without characteristic values. - * @private used to generate the config hash + * @private */ internalHAPRepresentation(): ServiceJsonObject { - assert(this.iid, "iid cannot be undefined for service '" + this.displayName + "'"); - assert(this.characteristics.length, "service '" + this.displayName + "' does not have any characteristics!"); + assert(this.iid, `iid cannot be undefined for service '${this.displayName}'`) + assert(this.characteristics.length, `service '${this.displayName}' does not have any characteristics!`) const service: ServiceJsonObject = { type: toShortForm(this.UUID), iid: this.iid!, characteristics: this.characteristics.map(characteristic => characteristic.internalHAPRepresentation()), - hidden: this.isHiddenService? true: undefined, - primary: this.isPrimaryService? true: undefined, - }; + hidden: this.isHiddenService ? true : undefined, + primary: this.isPrimaryService ? true : undefined, + } if (this.linkedServices.length) { - service.linked = []; + service.linked = [] for (const linked of this.linkedServices) { if (!linked.iid) { // we got a linked service which is not added to the accessory @@ -918,14 +921,14 @@ export class Service extends EventEmitter { // we have some (at least one) plugins on homebridge which link to the AccessoryInformation service. // homebridge always creates its own AccessoryInformation service and ignores the user supplied one // thus the link is automatically broken. - debug(`iid of linked service '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`); - continue; + debug(`iid of linked service '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`) + continue } - service.linked.push(linked.iid!); + service.linked.push(linked.iid!) } } - return service; + return service } /** @@ -934,10 +937,10 @@ export class Service extends EventEmitter { private setupCharacteristicEventHandlers(characteristic: Characteristic): void { // listen for changes in characteristics and bubble them up characteristic.on(CharacteristicEventTypes.CHANGE, (change: CharacteristicChange) => { - this.emit(ServiceEventTypes.CHARACTERISTIC_CHANGE, { ...change, characteristic: characteristic }); - }); + this.emit(ServiceEventTypes.CHARACTERISTIC_CHANGE, { ...change, characteristic }) + }) - characteristic.on(CharacteristicEventTypes.CHARACTERISTIC_WARNING, this.emitCharacteristicWarningEvent.bind(this, characteristic)); + characteristic.on(CharacteristicEventTypes.CHARACTERISTIC_WARNING, this.emitCharacteristicWarningEvent.bind(this, characteristic)) } /** @@ -945,12 +948,12 @@ export class Service extends EventEmitter { */ private emitCharacteristicWarningEvent(characteristic: Characteristic, type: CharacteristicWarningType, message: string, stack?: string): void { this.emit(ServiceEventTypes.CHARACTERISTIC_WARNING, { - characteristic: characteristic, - type: type, - message: message, + characteristic, + type, + message, originatorChain: [this.displayName, characteristic.displayName], - stack: stack, - }); + stack, + }) } /** @@ -958,19 +961,19 @@ export class Service extends EventEmitter { */ private _sideloadCharacteristics(targetCharacteristics: Characteristic[]): void { for (const target of targetCharacteristics) { - this.setupCharacteristicEventHandlers(target); + this.setupCharacteristicEventHandlers(target) } - this.characteristics = targetCharacteristics.slice(); + this.characteristics = targetCharacteristics.slice() } /** * @private */ static serialize(service: Service): SerializedService { - let constructorName: string | undefined; - if (service.constructor.name !== "Service") { - constructorName = service.constructor.name; + let constructorName: string | undefined + if (service.constructor.name !== 'Service') { + constructorName = service.constructor.name } return { @@ -978,46 +981,47 @@ export class Service extends EventEmitter { UUID: service.UUID, subtype: service.subtype, - constructorName: constructorName, + constructorName, hiddenService: service.isHiddenService, primaryService: service.isPrimaryService, characteristics: service.characteristics.map(characteristic => Characteristic.serialize(characteristic)), optionalCharacteristics: service.optionalCharacteristics.map(characteristic => Characteristic.serialize(characteristic)), - }; + } } /** * @private */ static deserialize(json: SerializedService): Service { - let service: Service; + let service: Service if (json.constructorName && json.constructorName.charAt(0).toUpperCase() === json.constructorName.charAt(0) && Service[json.constructorName as keyof (typeof Service)]) { // MUST start with uppercase character and must exist on Service object - const constructor = Service[json.constructorName as keyof (typeof Service)] as { new(displayName?: string, subtype?: string): Service }; - service = new constructor(json.displayName, json.subtype); + const constructor = Service[json.constructorName as keyof (typeof Service)] as { new(displayName?: string, subtype?: string): Service } + service = new constructor(json.displayName, json.subtype) } else { - service = new Service(json.displayName, json.UUID, json.subtype); + service = new Service(json.displayName, json.UUID, json.subtype) } - service.isHiddenService = !!json.hiddenService; - service.isPrimaryService = !!json.primaryService; + service.isHiddenService = !!json.hiddenService + service.isPrimaryService = !!json.primaryService - const characteristics = json.characteristics.map(serialized => Characteristic.deserialize(serialized)); - service._sideloadCharacteristics(characteristics); + const characteristics = json.characteristics.map(serialized => Characteristic.deserialize(serialized)) + service._sideloadCharacteristics(characteristics) if (json.optionalCharacteristics) { - service.optionalCharacteristics = json.optionalCharacteristics.map(serialized => Characteristic.deserialize(serialized)); + service.optionalCharacteristics = json.optionalCharacteristics.map(serialized => Characteristic.deserialize(serialized)) } - return service; + return service } - } // We have a cyclic dependency problem. Within this file we have the definitions of "./definitions" as // type imports only (in order to define the static properties). Setting those properties is done outside // this file, within the definition files. Therefore, we import it at the end of this file. Seems weird, but is important. -import "./definitions/ServiceDefinitions"; +(async () => { + await import('./definitions/ServiceDefinitions.js') +})() diff --git a/src/lib/camera/RTPProxy.ts b/src/lib/camera/RTPProxy.ts index b7a5b1e77..a53b4c5d7 100644 --- a/src/lib/camera/RTPProxy.ts +++ b/src/lib/camera/RTPProxy.ts @@ -1,14 +1,17 @@ -import dgram, { Socket, SocketType } from "dgram"; +import type { Socket, SocketType } from 'node:dgram' + +import { Buffer } from 'node:buffer' +import { createSocket } from 'node:dgram' /** * @group Camera */ export interface RTPProxyOptions { - disabled: boolean; - isIPV6?: boolean; - outgoingAddress: string; - outgoingPort: number; - outgoingSSRC: number; + disabled: boolean + isIPV6?: boolean + outgoingAddress: string + outgoingPort: number + outgoingSSRC: number } /** @@ -21,318 +24,296 @@ export interface RTPProxyOptions { * @group Camera */ export default class RTPProxy { - startingPort = 10000; - type: SocketType; - outgoingAddress: string; - outgoingPort: number; - incomingPayloadType: number; - outgoingSSRC: number; - incomingSSRC: number | null; - outgoingPayloadType: number | null; - disabled: boolean; - - incomingRTPSocket!: Socket; - incomingRTCPSocket!: Socket; - outgoingSocket!: Socket; - serverAddress?: string; - serverRTPPort?: number; - serverRTCPPort?: number; + startingPort = 10000 + type: SocketType + outgoingAddress: string + outgoingPort: number + incomingPayloadType: number + outgoingSSRC: number + incomingSSRC: number | null + outgoingPayloadType: number | null + disabled: boolean + + incomingRTPSocket!: Socket + incomingRTCPSocket!: Socket + outgoingSocket!: Socket + serverAddress?: string + serverRTPPort?: number + serverRTCPPort?: number constructor(public options: RTPProxyOptions) { - this.type = options.isIPV6 ? "udp6" : "udp4"; + this.type = options.isIPV6 ? 'udp6' : 'udp4' - this.startingPort = 10000; + this.startingPort = 10000 - this.outgoingAddress = options.outgoingAddress; - this.outgoingPort = options.outgoingPort; - this.incomingPayloadType = 0; - this.outgoingSSRC = options.outgoingSSRC; - this.disabled = options.disabled; - this.incomingSSRC = null; - this.outgoingPayloadType = null; + this.outgoingAddress = options.outgoingAddress + this.outgoingPort = options.outgoingPort + this.incomingPayloadType = 0 + this.outgoingSSRC = options.outgoingSSRC + this.disabled = options.disabled + this.incomingSSRC = null + this.outgoingPayloadType = null } - setup(): Promise { - return this.createSocketPair(this.type) - .then((sockets) => { - this.incomingRTPSocket = sockets[0]; - this.incomingRTCPSocket = sockets[1]; - - return this.createSocket(this.type); - }).then((socket) => { - this.outgoingSocket = socket; - this.onBound(); - }); + async setup(): Promise { + const sockets = await this.createSocketPair(this.type) + this.incomingRTPSocket = sockets[0] + this.incomingRTCPSocket = sockets[1] + this.outgoingSocket = await this.createSocket(this.type) + this.onBound() } destroy(): void { if (this.incomingRTPSocket) { - this.incomingRTPSocket.close(); + this.incomingRTPSocket.close() } if (this.incomingRTCPSocket) { - this.incomingRTCPSocket.close(); + this.incomingRTCPSocket.close() } if (this.outgoingSocket) { - this.outgoingSocket.close(); + this.outgoingSocket.close() } } incomingRTPPort(): number { - const address = this.incomingRTPSocket.address(); - - if (typeof address !== "string") { - return address.port; - } - - throw new Error("Unsupported socket!"); + const address = this.incomingRTPSocket.address() + return address.port } incomingRTCPPort(): number { - const address = this.incomingRTCPSocket.address(); - - if (typeof address !== "string") { - return address.port; - } - - throw new Error("Unsupported socket!"); + const address = this.incomingRTCPSocket.address() + return address.port } outgoingLocalPort(): number { - const address = this.outgoingSocket.address(); - - if (typeof address !== "string") { - return address.port; - } - - throw new Error("Unsupported socket!"); + const address = this.outgoingSocket.address() + return address.port } setServerAddress(address: string): void { - this.serverAddress = address; + this.serverAddress = address } setServerRTPPort(port: number): void { - this.serverRTPPort = port; + this.serverRTPPort = port } setServerRTCPPort(port: number): void { - this.serverRTCPPort = port; + this.serverRTCPPort = port } setIncomingPayloadType(pt: number): void { - this.incomingPayloadType = pt; + this.incomingPayloadType = pt } setOutgoingPayloadType(pt: number): void { - this.outgoingPayloadType = pt; + this.outgoingPayloadType = pt } sendOut(msg: Buffer): void { // Just drop it if we're not setup yet, I guess. - if(!this.outgoingAddress || !this.outgoingPort) { - return; + if (!this.outgoingAddress || !this.outgoingPort) { + return } - this.outgoingSocket.send(msg, this.outgoingPort, this.outgoingAddress); + this.outgoingSocket.send(msg, this.outgoingPort, this.outgoingAddress) } sendBack(msg: Buffer): void { // Just drop it if we're not setup yet, I guess. - if(!this.serverAddress || !this.serverRTCPPort) { - return; + if (!this.serverAddress || !this.serverRTCPPort) { + return } - this.outgoingSocket.send(msg, this.serverRTCPPort, this.serverAddress); + this.outgoingSocket.send(msg, this.serverRTCPPort, this.serverAddress) } onBound(): void { - if(this.disabled) { - return; + if (this.disabled) { + return } - this.incomingRTPSocket.on("message", msg => { - this.rtpMessage(msg); - }); + this.incomingRTPSocket.on('message', (msg) => { + this.rtpMessage(msg) + }) - this.incomingRTCPSocket.on("message", msg => { - this.rtcpMessage(msg); - }); + this.incomingRTCPSocket.on('message', (msg) => { + this.rtcpMessage(msg) + }) - this.outgoingSocket.on("message", msg => { - this.rtcpReply(msg); - }); + this.outgoingSocket.on('message', (msg) => { + this.rtcpReply(msg) + }) } rtpMessage(msg: Buffer): void { - - if(msg.length < 12) { + if (msg.length < 12) { // Not a proper RTP packet. Just forward it. - this.sendOut(msg); - return; + this.sendOut(msg) + return } - let mpt = msg.readUInt8(1); - const pt = mpt & 0x7F; - if(pt === this.incomingPayloadType) { - mpt = (mpt & 0x80) | this.outgoingPayloadType!; - msg.writeUInt8(mpt, 1); + let mpt = msg.readUInt8(1) + const pt = mpt & 0x7F + if (pt === this.incomingPayloadType) { + mpt = (mpt & 0x80) | this.outgoingPayloadType! + msg.writeUInt8(mpt, 1) } - if(this.incomingSSRC === null) { - this.incomingSSRC = msg.readUInt32BE(4); + if (this.incomingSSRC === null) { + this.incomingSSRC = msg.readUInt32BE(4) } - msg.writeUInt32BE(this.outgoingSSRC, 8); - this.sendOut(msg); + msg.writeUInt32BE(this.outgoingSSRC, 8) + this.sendOut(msg) } processRTCPMessage(msg: Buffer, transform: (pt: number, packet: Buffer) => Buffer): Buffer | null { - const rtcpPackets = []; - let offset = 0; - while((offset + 4) <= msg.length) { - const pt = msg.readUInt8(offset + 1); - const len = msg.readUInt16BE(offset + 2) * 4; - if((offset + 4 + len) > msg.length) { - break; + const rtcpPackets = [] + let offset = 0 + while ((offset + 4) <= msg.length) { + const pt = msg.readUInt8(offset + 1) + const len = msg.readUInt16BE(offset + 2) * 4 + if ((offset + 4 + len) > msg.length) { + break } - let packet = msg.slice(offset, offset + 4 + len); + let packet = msg.subarray(offset, offset + 4 + len) - packet = transform(pt, packet); + packet = transform(pt, packet) - if(packet) { - rtcpPackets.push(packet); + if (packet) { + rtcpPackets.push(packet) } - offset += 4 + len; + offset += 4 + len } - if(rtcpPackets.length > 0) { - return Buffer.concat(rtcpPackets); + if (rtcpPackets.length > 0) { + return Buffer.concat(rtcpPackets) } - return null; + return null } rtcpMessage(msg: Buffer): void { const processed = this.processRTCPMessage(msg, (pt, packet) => { - if(pt !== 200 || packet.length < 8) { - return packet; + if (pt !== 200 || packet.length < 8) { + return packet } - if(this.incomingSSRC === null) { - this.incomingSSRC = packet.readUInt32BE(4); + if (this.incomingSSRC === null) { + this.incomingSSRC = packet.readUInt32BE(4) } - packet.writeUInt32BE(this.outgoingSSRC, 4); - return packet; - }); + packet.writeUInt32BE(this.outgoingSSRC, 4) + return packet + }) - if(processed) { - this.sendOut(processed); + if (processed) { + this.sendOut(processed) } } rtcpReply(msg: Buffer): void { const processed = this.processRTCPMessage(msg, (pt, packet) => { - if(pt !== 201 || packet.length < 12) { - return packet; + if (pt !== 201 || packet.length < 12) { + return packet } // Assume source 1 is the one we want to edit. - packet.writeUInt32BE(this.incomingSSRC!, 8); - return packet; - }); + packet.writeUInt32BE(this.incomingSSRC!, 8) + return packet + }) - - if(processed) { - this.sendOut(processed); + if (processed) { + this.sendOut(processed) } } createSocket(type: SocketType): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const retry = () => { - const socket = dgram.createSocket(type); + const socket = createSocket(type) const bindErrorHandler = () => { - if(this.startingPort === 65535) { - this.startingPort = 10000; + if (this.startingPort === 65535) { + this.startingPort = 10000 } else { - ++this.startingPort; + ++this.startingPort } - socket.close(); - retry(); - }; + socket.close() + retry() + } - socket.once("error", bindErrorHandler); + socket.once('error', bindErrorHandler) - socket.on("listening", () => { - resolve(socket); - }); + socket.on('listening', () => { + resolve(socket) + }) - socket.bind(this.startingPort); - }; + socket.bind(this.startingPort) + } - retry(); - }); + retry() + }) } createSocketPair(type: SocketType): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const retry = () => { - const socket1 = dgram.createSocket(type); - const socket2 = dgram.createSocket(type); - const state = { socket1: 0, socket2: 0 }; + const socket1 = createSocket(type) + const socket2 = createSocket(type) + const state = { socket1: 0, socket2: 0 } const recheck = () => { - if(state.socket1 === 0 || state.socket2 === 0) { - return; + if (state.socket1 === 0 || state.socket2 === 0) { + return } - if(state.socket1 === 2 && state.socket2 === 2) { - resolve([socket1, socket2]); - return; + if (state.socket1 === 2 && state.socket2 === 2) { + resolve([socket1, socket2]) + return } - if(this.startingPort === 65534) { - this.startingPort = 10000; + if (this.startingPort === 65534) { + this.startingPort = 10000 } else { - ++this.startingPort; + ++this.startingPort } - socket1.close(); - socket2.close(); + socket1.close() + socket2.close() - retry(); - }; + retry() + } - socket1.once("error", () => { - state.socket1 = 1; - recheck(); - }); + socket1.once('error', () => { + state.socket1 = 1 + recheck() + }) - socket2.once("error", () => { - state.socket2 = 1; - recheck(); - }); + socket2.once('error', () => { + state.socket2 = 1 + recheck() + }) - socket1.once("listening", () => { - state.socket1 = 2; - recheck(); - }); + socket1.once('listening', () => { + state.socket1 = 2 + recheck() + }) - socket2.once("listening", () => { - state.socket2 = 2; - recheck(); - }); + socket2.once('listening', () => { + state.socket2 = 2 + recheck() + }) - socket1.bind(this.startingPort); - socket2.bind(this.startingPort + 1); - }; + socket1.bind(this.startingPort) + socket2.bind(this.startingPort + 1) + } - retry(); - }); + retry() + }) } } diff --git a/src/lib/camera/RTPStreamManagement.ts b/src/lib/camera/RTPStreamManagement.ts index 274679aa3..cbb2c21c9 100644 --- a/src/lib/camera/RTPStreamManagement.ts +++ b/src/lib/camera/RTPStreamManagement.ts @@ -1,27 +1,37 @@ -import assert from "assert"; -import crypto from "crypto"; -import createDebug from "debug"; -import net from "net"; -import { Access, Characteristic, CharacteristicEventTypes, CharacteristicSetCallback } from "../Characteristic"; -import { CameraController, CameraStreamingDelegate, ResourceRequestReason, StateChangeDelegate } from "../controller"; -import type { CameraRTPStreamManagement } from "../definitions"; -import { CharacteristicValue } from "../../types"; -import { HAPStatus } from "../HAPServer"; -import { Service } from "../Service"; -import { HAPConnection, HAPConnectionEvent } from "../util/eventedhttp"; -import { HapStatusError } from "../util/hapStatusError"; -import { once } from "../util/once"; -import * as tlv from "../util/tlv"; -import * as uuid from "../util/uuid"; -import RTPProxy from "./RTPProxy"; - -const debug = createDebug("HAP-NodeJS:Camera:RTPStreamManagement"); +import type { CharacteristicValue } from '../../types' +import type { CharacteristicSetCallback } from '../Characteristic' +import type { CameraStreamingDelegate, ResourceRequestReason, StateChangeDelegate } from '../controller' +import type { CameraRTPStreamManagement } from '../definitions' +import type { HAPConnection } from '../util/eventedhttp' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' +import { isIPv4 } from 'node:net' + +import createDebug from 'debug' + +import { Access, Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { CameraController } from '../controller/index.js' +import { HAPStatus } from '../HAPServer.js' +import { Service } from '../Service.js' +import { HAPConnectionEvent } from '../util/eventedhttp.js' +import { HapStatusError } from '../util/hapStatusError.js' +import { once } from '../util/once.js' +import { decode, encode, writeUInt16, writeUInt32 } from '../util/tlv.js' +import { unparse, write } from '../util/uuid.js' +import RTPProxy from './RTPProxy.js' + +const debug = createDebug('HAP-NodeJS:Camera:RTPStreamManagement') + // ---------------------------------- TLV DEFINITIONS START ---------------------------------- +// eslint-disable-next-line no-restricted-syntax const enum StreamingStatusTypes { STATUS = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum StreamingStatus { AVAILABLE = 0x00, IN_USE = 0x01, // Session is marked IN_USE after the first setup request @@ -30,16 +40,19 @@ const enum StreamingStatus { // ---------- +// eslint-disable-next-line no-restricted-syntax const enum SupportedVideoStreamConfigurationTypes { VIDEO_CODEC_CONFIGURATION = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum VideoCodecConfigurationTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, ATTRIBUTES = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum VideoCodecParametersTypes { PROFILE_ID = 0x01, LEVEL = 0x02, @@ -48,15 +61,17 @@ const enum VideoCodecParametersTypes { CVO_ID = 0x05, // ID for CVO RTP extension, value in range from 1 to 14 } +// eslint-disable-next-line no-restricted-syntax const enum VideoAttributesTypes { IMAGE_WIDTH = 0x01, IMAGE_HEIGHT = 0x02, - FRAME_RATE = 0x03 + FRAME_RATE = 0x03, } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum VideoCodecType { H264 = 0x00, // while the namespace is already reserved for H265 it isn't currently supported. @@ -66,6 +81,7 @@ export const enum VideoCodecType { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum H264Profile { BASELINE = 0x00, MAIN = 0x01, @@ -75,6 +91,7 @@ export const enum H264Profile { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum H264Level { LEVEL3_1 = 0x00, LEVEL3_2 = 0x01, @@ -84,27 +101,32 @@ export const enum H264Level { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum VideoCodecPacketizationMode { - NON_INTERLEAVED = 0x00 + NON_INTERLEAVED = 0x00, } +// eslint-disable-next-line no-restricted-syntax const enum VideoCodecCVO { // Coordination of Video Orientation UNSUPPORTED = 0x00, - SUPPORTED = 0x01 + SUPPORTED = 0x01, } // ---------- +// eslint-disable-next-line no-restricted-syntax const enum SupportedAudioStreamConfigurationTypes { AUDIO_CODEC_CONFIGURATION = 0x01, COMFORT_NOISE_SUPPORT = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecConfigurationTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecTypes { // only really by HAP supported codecs are AAC-ELD and OPUS PCMU = 0x00, PCMA = 0x01, @@ -115,34 +137,38 @@ const enum AudioCodecTypes { // only really by HAP supported codecs are AAC-ELD AMR_WB = 0x06, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecParametersTypes { CHANNEL = 0x01, BIT_RATE = 0x02, SAMPLE_RATE = 0x03, - PACKET_TIME = 0x04 // only present in selected audio codec parameters tlv + PACKET_TIME = 0x04, // only present in selected audio codec parameters tlv } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioBitrate { VARIABLE = 0x00, - CONSTANT = 0x01 + CONSTANT = 0x01, } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioSamplerate { KHZ_8 = 0x00, KHZ_16 = 0x01, - KHZ_24 = 0x02 + KHZ_24 = 0x02, // 3, 4, 5 are theoretically defined, but no idea to what kHz value they correspond to // probably KHZ_32, KHZ_44_1, KHZ_48 (as supported by Secure Video recordings) } // ---------- +// eslint-disable-next-line no-restricted-syntax const enum SupportedRTPConfigurationTypes { SRTP_CRYPTO_SUITE = 0x02, } @@ -150,16 +176,16 @@ const enum SupportedRTPConfigurationTypes { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum SRTPCryptoSuites { // public API AES_CM_128_HMAC_SHA1_80 = 0x00, AES_CM_256_HMAC_SHA1_80 = 0x01, - NONE = 0x02 + NONE = 0x02, } - // ---------- - +// eslint-disable-next-line no-restricted-syntax const enum SetupEndpointsTypes { SESSION_ID = 0x01, CONTROLLER_ADDRESS = 0x03, @@ -167,6 +193,7 @@ const enum SetupEndpointsTypes { AUDIO_SRTP_PARAMETERS = 0x05, } +// eslint-disable-next-line no-restricted-syntax const enum AddressTypes { ADDRESS_VERSION = 0x01, ADDRESS = 0x02, @@ -174,18 +201,20 @@ const enum AddressTypes { AUDIO_RTP_PORT = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum IPAddressVersion { IPV4 = 0x00, - IPV6 = 0x01 + IPV6 = 0x01, } - +// eslint-disable-next-line no-restricted-syntax const enum SRTPParametersTypes { SRTP_CRYPTO_SUITE = 0x01, MASTER_KEY = 0x02, // 16 bytes for AES_CM_128_HMAC_SHA1_80; 32 bytes for AES_256_CM_HMAC_SHA1_80 - MASTER_SALT = 0x03 // 14 bytes + MASTER_SALT = 0x03, // 14 bytes } +// eslint-disable-next-line no-restricted-syntax const enum SetupEndpointsResponseTypes { SESSION_ID = 0x01, STATUS = 0x02, @@ -196,22 +225,23 @@ const enum SetupEndpointsResponseTypes { AUDIO_SSRC = 0x07, } +// eslint-disable-next-line no-restricted-syntax const enum SetupEndpointsStatus { SUCCESS = 0x00, BUSY = 0x01, - ERROR = 0x02 + ERROR = 0x02, } - // ---------- - +// eslint-disable-next-line no-restricted-syntax const enum SelectedRTPStreamConfigurationTypes { SESSION_CONTROL = 0x01, SELECTED_VIDEO_PARAMETERS = 0x02, - SELECTED_AUDIO_PARAMETERS = 0x03 + SELECTED_AUDIO_PARAMETERS = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum SessionControlTypes { SESSION_IDENTIFIER = 0x01, // uuid, 16 bytes COMMAND = 0x02, @@ -225,6 +255,7 @@ enum SessionControlCommand { RECONFIGURE_SESSION = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum SelectedVideoParametersTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, @@ -232,6 +263,7 @@ const enum SelectedVideoParametersTypes { RTP_PARAMETERS = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum VideoRTPParametersTypes { PAYLOAD_TYPE = 0x01, SYNCHRONIZATION_SOURCE = 0x02, @@ -240,6 +272,7 @@ const enum VideoRTPParametersTypes { MAX_MTU = 0x05, // only there if value is not default value; default values: ipv4 1378; ipv6 1228 bytes } +// eslint-disable-next-line no-restricted-syntax const enum SelectedAudioParametersTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, @@ -247,12 +280,13 @@ const enum SelectedAudioParametersTypes { COMFORT_NOISE = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum AudioRTPParametersTypes { PAYLOAD_TYPE = 0x01, SYNCHRONIZATION_SOURCE = 0x02, MAX_BIT_RATE = 0x03, MIN_RTCP_INTERVAL = 0x04, // minimum RTCP interval in seconds - COMFORT_NOISE_PAYLOAD_TYPE = 0x06 + COMFORT_NOISE_PAYLOAD_TYPE = 0x06, } // ---------------------------------- TLV DEFINITIONS END ------------------------------------ @@ -265,112 +299,112 @@ export type CameraStreamingOptions = CameraStreamingOptionsBase & (CameraStreami * @group Camera */ export interface CameraStreamingOptionsBase { - proxy?: boolean; // default false - disable_audio_proxy?: boolean; // default false; If proxy = true, you can opt out audio proxy via this + proxy?: boolean // default false + disable_audio_proxy?: boolean // default false; If proxy = true, you can opt out audio proxy via this - video: VideoStreamingOptions; + video: VideoStreamingOptions /** * "audio" is optional and only needs to be declared if audio streaming is supported. * If defined the Microphone service will be added and Microphone volume control will be made available. * If not defined hap-nodejs will expose a default codec in order for the video stream to work */ - audio?: AudioStreamingOptions; + audio?: AudioStreamingOptions } /** * @group Camera */ export interface CameraStreamingOptionsLegacySRTP { - srtp: boolean; // a value of true indicates support of AES_CM_128_HMAC_SHA1_80 + srtp: boolean // a value of true indicates support of AES_CM_128_HMAC_SHA1_80 } /** * @group Camera */ export interface CameraStreamingOptionsSupportedCryptoSuites { - supportedCryptoSuites: SRTPCryptoSuites[], // Suite NONE should only be used for testing and will probably be never selected by iOS! + supportedCryptoSuites: SRTPCryptoSuites[] // Suite NONE should only be used for testing and will probably be never selected by iOS! } -// eslint-disable-next-line @typescript-eslint/no-explicit-any function isLegacySRTPOptions(options: any): options is CameraStreamingOptionsLegacySRTP { - return "srtp" in options; + return 'srtp' in options } /** * @group Camera */ -export type VideoStreamingOptions = { - codec: H264CodecParameters, - resolutions: Resolution[], - cvoId?: number, +export interface VideoStreamingOptions { + codec: H264CodecParameters + resolutions: Resolution[] + cvoId?: number } /** * @group Camera */ export interface H264CodecParameters { - levels: H264Level[], - profiles: H264Profile[], + levels: H264Level[] + profiles: H264Profile[] } /** * @group Camera */ -export type Resolution = [number, number, number]; // width, height, framerate +export type Resolution = [number, number, number] // width, height, framerate /** * @group Camera */ -export type AudioStreamingOptions = { - codecs: AudioStreamingCodec[], - twoWayAudio?: boolean, // default false, indicates support of 2way audio (will add the Speaker service and Speaker volume control) - comfort_noise?: boolean, // default false +export interface AudioStreamingOptions { + codecs: AudioStreamingCodec[] + twoWayAudio?: boolean // default false, indicates support of 2way audio (will add the Speaker service and Speaker volume control) + comfort_noise?: boolean // default false } /** * @group Camera */ -export type AudioStreamingCodec = { - type: AudioStreamingCodecType | string, // string type for backwards compatibility - audioChannels?: number, // default 1 - bitrate?: AudioBitrate, // default VARIABLE, AAC-ELD or OPUS MUST support VARIABLE bitrate - samplerate: AudioStreamingSamplerate[] | AudioStreamingSamplerate, // OPUS or AAC-ELD must support samplerate at 16k and 25k +export interface AudioStreamingCodec { + type: AudioStreamingCodecType | string // string type for backwards compatibility + audioChannels?: number // default 1 + bitrate?: AudioBitrate // default VARIABLE, AAC-ELD or OPUS MUST support VARIABLE bitrate + samplerate: AudioStreamingSamplerate[] | AudioStreamingSamplerate // OPUS or AAC-ELD must support samplerate at 16k and 25k } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioStreamingCodecType { // codecs as defined by the HAP spec; only AAC-ELD and OPUS seem to work - PCMU = "PCMU", - PCMA = "PCMA", - AAC_ELD = "AAC-eld", - OPUS = "OPUS", - MSBC = "mSBC", - AMR = "AMR", - AMR_WB = "AMR-WB", + PCMU = 'PCMU', + PCMA = 'PCMA', + AAC_ELD = 'AAC-eld', + OPUS = 'OPUS', + MSBC = 'mSBC', + AMR = 'AMR', + AMR_WB = 'AMR-WB', } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioStreamingSamplerate { KHZ_8 = 8, KHZ_16 = 16, KHZ_24 = 24, } - /** * @group Camera */ -export type StreamSessionIdentifier = string; // uuid provided by HAP to identify a streaming session +export type StreamSessionIdentifier = string // uuid provided by HAP to identify a streaming session /** * @group Camera */ -export type SnapshotRequest = { - height: number; - width: number; +export interface SnapshotRequest { + height: number + width: number /** * An optional {@link ResourceRequestReason}. The client decides if it wants to send this value. It is typically * only sent in the context of HomeKit Secure Video Cameras. @@ -383,213 +417,214 @@ export type SnapshotRequest = { /** * @group Camera */ -export type PrepareStreamRequest = { - sessionID: StreamSessionIdentifier, - sourceAddress: string, - targetAddress: string, - addressVersion: "ipv4" | "ipv6", - audio: Source, - video: Source, +export interface PrepareStreamRequest { + sessionID: StreamSessionIdentifier + sourceAddress: string + targetAddress: string + addressVersion: 'ipv4' | 'ipv6' + audio: Source + video: Source } /** * @group Camera */ -export type Source = { - port: number, +export interface Source { + port: number - srtpCryptoSuite: SRTPCryptoSuites, // if cryptoSuite is NONE, key and salt are both zero-length - srtp_key: Buffer, - srtp_salt: Buffer, + srtpCryptoSuite: SRTPCryptoSuites // if cryptoSuite is NONE, key and salt are both zero-length + srtp_key: Buffer + srtp_salt: Buffer - proxy_rtp?: number, - proxy_rtcp?: number, -}; + proxy_rtp?: number + proxy_rtcp?: number +} /** * @group Camera */ -export type PrepareStreamResponse = { +export interface PrepareStreamResponse { /** * Any value set to this optional property will overwrite the automatically determined local address, * which is sent as RTP endpoint to the iOS device. */ - addressOverride?: string; + addressOverride?: string // video should be instanceOf ProxiedSourceResponse if proxy is required - video: SourceResponse | ProxiedSourceResponse; + video: SourceResponse | ProxiedSourceResponse // needs to be only supplied if audio is required; audio should be instanceOf ProxiedSourceResponse if proxy is required and audio proxy is not disabled - audio?: SourceResponse | ProxiedSourceResponse; + audio?: SourceResponse | ProxiedSourceResponse } /** * @group Camera */ export interface SourceResponse { - port: number, // RTP/RTCP port of streaming server - ssrc: number, // synchronization source of the stream + port: number // RTP/RTCP port of streaming server + ssrc: number // synchronization source of the stream - srtp_key?: Buffer, // SRTP Key. Required if SRTP is used for the current stream - srtp_salt?: Buffer, // SRTP Salt. Required if SRTP is used for the current stream + srtp_key?: Buffer // SRTP Key. Required if SRTP is used for the current stream + srtp_salt?: Buffer // SRTP Salt. Required if SRTP is used for the current stream } /** * @group Camera */ export interface ProxiedSourceResponse { - proxy_pt: number, // Payload Type of input stream - proxy_server_address: string, // IP address of RTP server - proxy_server_rtp: number, // RTP port - proxy_server_rtcp: number, // RTCP port + proxy_pt: number // Payload Type of input stream + proxy_server_address: string // IP address of RTP server + proxy_server_rtp: number // RTP port + proxy_server_rtcp: number // RTCP port } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum StreamRequestTypes { - RECONFIGURE = "reconfigure", - START = "start", - STOP = "stop", + RECONFIGURE = 'reconfigure', + START = 'start', + STOP = 'stop', } /** * @group Camera */ -export type StreamingRequest = StartStreamRequest | ReconfigureStreamRequest | StopStreamRequest; +export type StreamingRequest = StartStreamRequest | ReconfigureStreamRequest | StopStreamRequest /** * @group Camera */ -export type StartStreamRequest = { - sessionID: StreamSessionIdentifier, - type: StreamRequestTypes.START, - video: VideoInfo, - audio: AudioInfo, +export interface StartStreamRequest { + sessionID: StreamSessionIdentifier + type: StreamRequestTypes.START + video: VideoInfo + audio: AudioInfo } /** * @group Camera */ -export type ReconfigureStreamRequest = { - sessionID: StreamSessionIdentifier, - type: StreamRequestTypes.RECONFIGURE, - video: ReconfiguredVideoInfo, +export interface ReconfigureStreamRequest { + sessionID: StreamSessionIdentifier + type: StreamRequestTypes.RECONFIGURE + video: ReconfiguredVideoInfo } /** * @group Camera */ -export type StopStreamRequest = { - sessionID: StreamSessionIdentifier, - type: StreamRequestTypes.STOP, +export interface StopStreamRequest { + sessionID: StreamSessionIdentifier + type: StreamRequestTypes.STOP } /** * @group Camera */ -export type AudioInfo = { - codec: AudioStreamingCodecType, // block size for AAC-ELD must be 480 samples +export interface AudioInfo { + codec: AudioStreamingCodecType // block size for AAC-ELD must be 480 samples - channel: number, - bit_rate: number, - sample_rate: AudioStreamingSamplerate, // 8, 16, 24 - packet_time: number, // rtp packet time: length of time in ms represented by the media in a packet (20ms, 30ms, 40ms, 60ms) + channel: number + bit_rate: number + sample_rate: AudioStreamingSamplerate // 8, 16, 24 + packet_time: number // rtp packet time: length of time in ms represented by the media in a packet (20ms, 30ms, 40ms, 60ms) - pt: number, // payloadType, typically 110 - ssrc: number, // synchronisation source - max_bit_rate: number, - rtcp_interval: number, // minimum rtcp interval in seconds (floating point number), pretty much always 0.5 - comfort_pt: number, // comfortNoise payloadType, 13 + pt: number // payloadType, typically 110 + ssrc: number // synchronisation source + max_bit_rate: number + rtcp_interval: number // minimum rtcp interval in seconds (floating point number), pretty much always 0.5 + comfort_pt: number // comfortNoise payloadType, 13 - comfortNoiseEnabled: boolean, -}; + comfortNoiseEnabled: boolean +} /** * @group Camera */ -export type VideoInfo = { // minimum keyframe interval is about 5 seconds - codec: VideoCodecType; - profile: H264Profile, - level: H264Level, - packetizationMode: VideoCodecPacketizationMode, - cvoId?: number, // Coordination of Video Orientation, only supplied if enabled AND supported; ranges from 1 to 14 - - width: number, - height: number, - fps: number, - - pt: number, // payloadType, 99 for h264 - ssrc: number, // synchronisation source - max_bit_rate: number, - rtcp_interval: number, // minimum rtcp interval in seconds (floating point number), pretty much always 0.5 (standard says a rang from 0.5 to 1.5) - mtu: number, // maximum transmissions unit, default values: ipv4: 1378 bytes; ipv6: 1228 bytes -}; +export interface VideoInfo { // minimum keyframe interval is about 5 seconds + codec: VideoCodecType + profile: H264Profile + level: H264Level + packetizationMode: VideoCodecPacketizationMode + cvoId?: number // Coordination of Video Orientation, only supplied if enabled AND supported; ranges from 1 to 14 + + width: number + height: number + fps: number + + pt: number // payloadType, 99 for h264 + ssrc: number // synchronisation source + max_bit_rate: number + rtcp_interval: number // minimum rtcp interval in seconds (floating point number), pretty much always 0.5 (standard says a rang from 0.5 to 1.5) + mtu: number // maximum transmissions unit, default values: ipv4: 1378 bytes; ipv6: 1228 bytes +} /** * @group Camera */ -export type ReconfiguredVideoInfo = { - width: number, - height: number, - fps: number, +export interface ReconfiguredVideoInfo { + width: number + height: number + fps: number - max_bit_rate: number, - rtcp_interval: number, // minimum rtcp interval in seconds (floating point number) + max_bit_rate: number + rtcp_interval: number // minimum rtcp interval in seconds (floating point number) } /** * @group Camera */ export interface RTPStreamManagementState { - id: number; - active: boolean; + id: number + active: boolean } /** * @group Camera */ export class RTPStreamManagement { - private readonly id: number; - private readonly delegate: CameraStreamingDelegate; - readonly service: CameraRTPStreamManagement; + private readonly id: number + private readonly delegate: CameraStreamingDelegate + readonly service: CameraRTPStreamManagement - private stateChangeDelegate?: StateChangeDelegate; + private stateChangeDelegate?: StateChangeDelegate - requireProxy: boolean; - disableAudioProxy: boolean; - supportedCryptoSuites: SRTPCryptoSuites[]; - videoOnly = false; + requireProxy: boolean + disableAudioProxy: boolean + supportedCryptoSuites: SRTPCryptoSuites[] + videoOnly = false - readonly supportedRTPConfiguration: string; - readonly supportedVideoStreamConfiguration: string; - readonly supportedAudioStreamConfiguration: string; + readonly supportedRTPConfiguration: string + readonly supportedVideoStreamConfiguration: string + readonly supportedAudioStreamConfiguration: string - private activeConnection?: HAPConnection; - private readonly activeConnectionClosedListener: (callback?: CharacteristicSetCallback) => void; - sessionIdentifier?: StreamSessionIdentifier = undefined; + private activeConnection?: HAPConnection + private readonly activeConnectionClosedListener: (callback?: CharacteristicSetCallback) => void + sessionIdentifier?: StreamSessionIdentifier = undefined /** - * @private private API + * @private */ - streamStatus: StreamingStatus = StreamingStatus.AVAILABLE; // use _updateStreamStatus to update this property - private ipVersion?: "ipv4" | "ipv6"; // ip version for the current session + streamStatus: StreamingStatus = StreamingStatus.AVAILABLE // use _updateStreamStatus to update this property + private ipVersion?: 'ipv4' | 'ipv6' // ip version for the current session - selectedConfiguration = ""; // base64 representation of the currently selected configuration - setupEndpointsResponse = ""; // response of the SetupEndpoints Characteristic + selectedConfiguration = '' // base64 representation of the currently selected configuration + setupEndpointsResponse = '' // response of the SetupEndpoints Characteristic /** - * @private deprecated API + * @private */ - audioProxy?: RTPProxy; + audioProxy?: RTPProxy /** - * @private deprecated API + * @private */ - videoProxy?: RTPProxy; + videoProxy?: RTPProxy /** * A RTPStreamManagement is considered disabled if `HomeKitCameraActive` is set to false. * We use a closure based approach to retrieve the value of this characteristic. * The characteristic is managed by the RecordingManagement. */ - private readonly disabledThroughOperatingMode?: () => boolean; + private readonly disabledThroughOperatingMode?: () => boolean constructor( id: number, @@ -598,218 +633,220 @@ export class RTPStreamManagement { service?: CameraRTPStreamManagement, disabledThroughOperatingMode?: () => boolean, ) { - this.id = id; - this.delegate = delegate; + this.id = id + this.delegate = delegate - this.requireProxy = options.proxy || false; - this.disableAudioProxy = options.disable_audio_proxy || false; + this.requireProxy = options.proxy || false + this.disableAudioProxy = options.disable_audio_proxy || false if (isLegacySRTPOptions(options)) { - this.supportedCryptoSuites = [options.srtp? SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80: SRTPCryptoSuites.NONE]; + this.supportedCryptoSuites = [options.srtp ? SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 : SRTPCryptoSuites.NONE] } else { - this.supportedCryptoSuites = options.supportedCryptoSuites; + this.supportedCryptoSuites = options.supportedCryptoSuites } if (this.supportedCryptoSuites.length === 0) { - this.supportedCryptoSuites.push(SRTPCryptoSuites.NONE); + this.supportedCryptoSuites.push(SRTPCryptoSuites.NONE) } if (!options.video) { - throw new Error("Video parameters cannot be undefined in options"); + throw new Error('Video parameters cannot be undefined in options') } - this.supportedRTPConfiguration = RTPStreamManagement._supportedRTPConfiguration(this.supportedCryptoSuites); - this.supportedVideoStreamConfiguration = RTPStreamManagement._supportedVideoStreamConfiguration(options.video); - this.supportedAudioStreamConfiguration = this._supportedAudioStreamConfiguration(options.audio); + this.supportedRTPConfiguration = RTPStreamManagement._supportedRTPConfiguration(this.supportedCryptoSuites) + this.supportedVideoStreamConfiguration = RTPStreamManagement._supportedVideoStreamConfiguration(options.video) + this.supportedAudioStreamConfiguration = this._supportedAudioStreamConfiguration(options.audio) - this.activeConnectionClosedListener = this._handleStopStream.bind(this); + this.activeConnectionClosedListener = this._handleStopStream.bind(this) - this.service = service || this.constructService(id); - this.setupServiceHandlers(); + this.service = service || this.constructService(id) + this.setupServiceHandlers() - this.resetSetupEndpointsResponse(); - this.resetSelectedStreamConfiguration(); + this.resetSetupEndpointsResponse() + this.resetSelectedStreamConfiguration() - this.disabledThroughOperatingMode = disabledThroughOperatingMode; + this.disabledThroughOperatingMode = disabledThroughOperatingMode } public forceStop(): void { - this.handleSessionClosed(); + this.handleSessionClosed() } getService(): CameraRTPStreamManagement { - return this.service; + return this.service } handleFactoryReset(): void { - this.resetSelectedStreamConfiguration(); - this.resetSetupEndpointsResponse(); + this.resetSelectedStreamConfiguration() + this.resetSetupEndpointsResponse() - this.service.updateCharacteristic(Characteristic.Active, true); + this.service.updateCharacteristic(Characteristic.Active, true) // on a factory reset the assumption is that all connections were already terminated and thus "handleStopStream" was already called } public destroy(): void { if (this.activeConnection) { - this._handleStopStream(); + this._handleStopStream() } } private constructService(id: number): CameraRTPStreamManagement { - const managementService = new Service.CameraRTPStreamManagement("", id.toString()); + const managementService = new Service.CameraRTPStreamManagement('', id.toString()) // this service is required only when recording is enabled. We don't really have access to this info here, // so we just add the characteristic. Doesn't really hurt. - managementService.setCharacteristic(Characteristic.Active, true); + managementService.setCharacteristic(Characteristic.Active, true) - return managementService; + return managementService } private setupServiceHandlers() { if (!this.service.testCharacteristic(Characteristic.Active)) { // the active characteristic might not be present on some older configurations. - this.service.setCharacteristic(Characteristic.Active, true); + this.service.setCharacteristic(Characteristic.Active, true) } this.service.getCharacteristic(Characteristic.Active) .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) // ensure that configurations are up-to-date and reflected in the characteristic values - this.service.setCharacteristic(Characteristic.SupportedRTPConfiguration, this.supportedRTPConfiguration); - this.service.setCharacteristic(Characteristic.SupportedVideoStreamConfiguration, this.supportedVideoStreamConfiguration); - this.service.setCharacteristic(Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioStreamConfiguration); + this.service.setCharacteristic(Characteristic.SupportedRTPConfiguration, this.supportedRTPConfiguration) + this.service.setCharacteristic(Characteristic.SupportedVideoStreamConfiguration, this.supportedVideoStreamConfiguration) + this.service.setCharacteristic(Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioStreamConfiguration) - this._updateStreamStatus(StreamingStatus.AVAILABLE); // reset streaming status to available - this.service.setCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse); // reset SetupEndpoints to default + this._updateStreamStatus(StreamingStatus.AVAILABLE) // reset streaming status to available + this.service.setCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse) // reset SetupEndpoints to default this.service.getCharacteristic(Characteristic.SelectedRTPStreamConfiguration)! - .on(CharacteristicEventTypes.GET, callback => { + .on(CharacteristicEventTypes.GET, (callback) => { if (this.streamingIsDisabled()) { - callback(null, tlv.encode( - SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, tlv.encode( - SessionControlTypes.COMMAND, SessionControlCommand.SUSPEND_SESSION, + callback(null, encode( + SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, + encode( + SessionControlTypes.COMMAND, + SessionControlCommand.SUSPEND_SESSION, ), - ).toString("base64")); - return; + ).toString('base64')) + return } - callback(null, this.selectedConfiguration); + callback(null, this.selectedConfiguration) }) - .on(CharacteristicEventTypes.SET, this._handleSelectedStreamConfigurationWrite.bind(this)); + .on(CharacteristicEventTypes.SET, this._handleSelectedStreamConfigurationWrite.bind(this)) this.service.getCharacteristic(Characteristic.SetupEndpoints)! - .on(CharacteristicEventTypes.GET, callback => { + .on(CharacteristicEventTypes.GET, (callback) => { if (this.streamingIsDisabled()) { - callback(null, tlv.encode( - SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR, - ).toString("base64")); - return; + callback(null, encode( + SetupEndpointsResponseTypes.STATUS, + SetupEndpointsStatus.ERROR, + ).toString('base64')) + return } - callback(null, this.setupEndpointsResponse); + callback(null, this.setupEndpointsResponse) }) .on(CharacteristicEventTypes.SET, (value, callback, context, connection) => { if (!connection) { - debug("Set event handler for SetupEndpoints cannot be called from plugin. Connection undefined!"); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + debug('Set event handler for SetupEndpoints cannot be called from plugin. Connection undefined!') + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } - this.handleSetupEndpoints(value, callback, connection); - }); + this.handleSetupEndpoints(value, callback, connection) + }) } private handleSessionClosed(): void { // called when the streaming was ended or aborted and needs to be cleaned up - this.resetSelectedStreamConfiguration(); - this.resetSetupEndpointsResponse(); + this.resetSelectedStreamConfiguration() + this.resetSetupEndpointsResponse() if (this.activeConnection) { - this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener); - this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() - 1); - this.activeConnection = undefined; + this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener) + this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() - 1) + this.activeConnection = undefined } - this._updateStreamStatus(StreamingStatus.AVAILABLE); - this.sessionIdentifier = undefined; - this.ipVersion = undefined; + this._updateStreamStatus(StreamingStatus.AVAILABLE) + this.sessionIdentifier = undefined + this.ipVersion = undefined if (this.videoProxy) { - this.videoProxy.destroy(); - this.videoProxy = undefined; + this.videoProxy.destroy() + this.videoProxy = undefined } if (this.audioProxy) { - this.audioProxy.destroy(); - this.audioProxy = undefined; + this.audioProxy.destroy() + this.audioProxy = undefined } } private streamingIsDisabled(callback?: CharacteristicSetCallback): boolean { if (!this.service.getCharacteristic(Characteristic.Active).value) { - if (typeof callback === "function") { - callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE)); + if (typeof callback === 'function') { + callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE)) } - return true; + return true } if (this.disabledThroughOperatingMode?.()) { - if (typeof callback === "function") { - callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE)); + if (typeof callback === 'function') { + callback(new HapStatusError(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE)) } - return true; + return true } - return false; + return false } private _handleSelectedStreamConfigurationWrite(value: CharacteristicValue, callback: CharacteristicSetCallback): void { if (this.streamingIsDisabled(callback)) { - return; + return } - const data = Buffer.from(value as string, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value as string, 'base64') + const objects = decode(data) - const sessionControl = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SESSION_CONTROL]); - const sessionIdentifier = uuid.unparse(sessionControl[SessionControlTypes.SESSION_IDENTIFIER]); - const requestType: SessionControlCommand = sessionControl[SessionControlTypes.COMMAND][0]; + const sessionControl = decode(objects[SelectedRTPStreamConfigurationTypes.SESSION_CONTROL]) + const sessionIdentifier = unparse(sessionControl[SessionControlTypes.SESSION_IDENTIFIER]) + const requestType: SessionControlCommand = sessionControl[SessionControlTypes.COMMAND][0] if (sessionIdentifier !== this.sessionIdentifier) { - debug(`Received unknown session Identifier with request to ${SessionControlCommand[requestType]}`); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + debug(`Received unknown session Identifier with request to ${SessionControlCommand[requestType]}`) + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } - this.selectedConfiguration = value as string; + this.selectedConfiguration = value as string // intercept the callback chain to check if an error occurred. const streamCallback: CharacteristicSetCallback = (error, writeResponse) => { - callback(error, writeResponse); // does not support writeResponse, but how knows what comes in the future. + callback(error, writeResponse) // does not support writeResponse, but how knows what comes in the future. if (error) { - this.handleSessionClosed(); + this.handleSessionClosed() } - }; + } switch (requestType) { - case SessionControlCommand.START_SESSION: { - const selectedVideoParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]); - const selectedAudioParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_AUDIO_PARAMETERS]); + case SessionControlCommand.START_SESSION: { + const selectedVideoParameters = decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]) + const selectedAudioParameters = decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_AUDIO_PARAMETERS]) - this._handleStartStream(selectedVideoParameters, selectedAudioParameters, streamCallback); - break; - } - case SessionControlCommand.RECONFIGURE_SESSION: { - const reconfiguredVideoParameters = tlv.decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]); + this._handleStartStream(selectedVideoParameters, selectedAudioParameters, streamCallback) + break + } + case SessionControlCommand.RECONFIGURE_SESSION: { + const reconfiguredVideoParameters = decode(objects[SelectedRTPStreamConfigurationTypes.SELECTED_VIDEO_PARAMETERS]) - this.handleReconfigureStream(reconfiguredVideoParameters, streamCallback); - break; - } - case SessionControlCommand.END_SESSION: - this._handleStopStream(streamCallback); - break; - case SessionControlCommand.RESUME_SESSION: - case SessionControlCommand.SUSPEND_SESSION: - default: - debug(`Unhandled request type ${SessionControlCommand[requestType]}`); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + this.handleReconfigureStream(reconfiguredVideoParameters, streamCallback) + break + } + case SessionControlCommand.END_SESSION: + this._handleStopStream(streamCallback) + break + case SessionControlCommand.RESUME_SESSION: + case SessionControlCommand.SUSPEND_SESSION: + default: + debug(`Unhandled request type ${SessionControlCommand[requestType]}`) + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) } } @@ -819,79 +856,76 @@ export class RTPStreamManagement { callback: CharacteristicSetCallback, ): void { // selected video configuration - // noinspection JSUnusedLocalSymbols - const videoCodec = videoConfiguration[SelectedVideoParametersTypes.CODEC_TYPE]; // always 0x00 for h264 - const videoParametersTLV = videoConfiguration[SelectedVideoParametersTypes.CODEC_PARAMETERS]; - const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES]; - const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS]; + const videoCodec = videoConfiguration[SelectedVideoParametersTypes.CODEC_TYPE] // always 0x00 for h264 + const videoParametersTLV = videoConfiguration[SelectedVideoParametersTypes.CODEC_PARAMETERS] + const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES] + const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS] // video parameters - const videoParameters = tlv.decode(videoParametersTLV); - const h264Profile: H264Profile = videoParameters[VideoCodecParametersTypes.PROFILE_ID][0]; - const h264Level: H264Level = videoParameters[VideoCodecParametersTypes.LEVEL][0]; - const packetizationMode: VideoCodecPacketizationMode = videoParameters[VideoCodecParametersTypes.PACKETIZATION_MODE][0]; - const cvoEnabled = videoParameters[VideoCodecParametersTypes.CVO_ENABLED]; - let cvoId: number | undefined = undefined; + const videoParameters = decode(videoParametersTLV) + const h264Profile: H264Profile = videoParameters[VideoCodecParametersTypes.PROFILE_ID][0] + const h264Level: H264Level = videoParameters[VideoCodecParametersTypes.LEVEL][0] + const packetizationMode: VideoCodecPacketizationMode = videoParameters[VideoCodecParametersTypes.PACKETIZATION_MODE][0] + const cvoEnabled = videoParameters[VideoCodecParametersTypes.CVO_ENABLED] + let cvoId: number | undefined if (cvoEnabled && cvoEnabled[0] === VideoCodecCVO.SUPPORTED) { - cvoId = videoParameters[VideoCodecParametersTypes.CVO_ID].readUInt8(0); + cvoId = videoParameters[VideoCodecParametersTypes.CVO_ID].readUInt8(0) } // video attributes - const videoAttributes = tlv.decode(videoAttributesTLV); - const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0); - const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0); - const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0); + const videoAttributes = decode(videoAttributesTLV) + const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0) + const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0) + const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0) // video rtp parameters - const videoRTPParameters = tlv.decode(videoRTPParametersTLV); - const videoPayloadType = videoRTPParameters[VideoRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0); // 99 - const videoSSRC = videoRTPParameters[VideoRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0); - const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0); - const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0); - let maxMTU = this.ipVersion === "ipv6"? 1228: 1378; // default values ipv4: 1378 bytes; ipv6: 1228 bytes + const videoRTPParameters = decode(videoRTPParametersTLV) + const videoPayloadType = videoRTPParameters[VideoRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0) // 99 + const videoSSRC = videoRTPParameters[VideoRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0) + const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0) + const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0) + let maxMTU = this.ipVersion === 'ipv6' ? 1228 : 1378 // default values ipv4: 1378 bytes; ipv6: 1228 bytes if (videoRTPParameters[VideoRTPParametersTypes.MAX_MTU]) { - maxMTU = videoRTPParameters[VideoRTPParametersTypes.MAX_MTU].readUInt16LE(0); + maxMTU = videoRTPParameters[VideoRTPParametersTypes.MAX_MTU].readUInt16LE(0) } - // selected audio configuration - const audioCodec: AudioCodecTypes = audioConfiguration[SelectedAudioParametersTypes.CODEC_TYPE][0]; - const audioParametersTLV = audioConfiguration[SelectedAudioParametersTypes.CODEC_PARAMETERS]; - const audioRTPParametersTLV = audioConfiguration[SelectedAudioParametersTypes.RTP_PARAMETERS]; - const comfortNoise = !!audioConfiguration[SelectedAudioParametersTypes.COMFORT_NOISE].readUInt8(0); + const audioCodec: AudioCodecTypes = audioConfiguration[SelectedAudioParametersTypes.CODEC_TYPE][0] + const audioParametersTLV = audioConfiguration[SelectedAudioParametersTypes.CODEC_PARAMETERS] + const audioRTPParametersTLV = audioConfiguration[SelectedAudioParametersTypes.RTP_PARAMETERS] + const comfortNoise = !!audioConfiguration[SelectedAudioParametersTypes.COMFORT_NOISE].readUInt8(0) // audio parameters - const audioParameters = tlv.decode(audioParametersTLV); - const channels = audioParameters[AudioCodecParametersTypes.CHANNEL][0]; - const audioBitrate: AudioBitrate = audioParameters[AudioCodecParametersTypes.BIT_RATE][0]; - const samplerate: AudioSamplerate = audioParameters[AudioCodecParametersTypes.SAMPLE_RATE][0]; - const rtpPacketTime = audioParameters[AudioCodecParametersTypes.PACKET_TIME].readUInt8(0); + const audioParameters = decode(audioParametersTLV) + const channels = audioParameters[AudioCodecParametersTypes.CHANNEL][0] + const audioBitrate: AudioBitrate = audioParameters[AudioCodecParametersTypes.BIT_RATE][0] + const samplerate: AudioSamplerate = audioParameters[AudioCodecParametersTypes.SAMPLE_RATE][0] + const rtpPacketTime = audioParameters[AudioCodecParametersTypes.PACKET_TIME].readUInt8(0) // audio rtp parameters - const audioRTPParameters = tlv.decode(audioRTPParametersTLV); - const audioPayloadType = audioRTPParameters[AudioRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0); // 110 - const audioSSRC = audioRTPParameters[AudioRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0); - const audioMaximumBitrate = audioRTPParameters[AudioRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0); - const audioRTCPInterval = audioRTPParameters[AudioRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0); - const comfortNoisePayloadType = audioRTPParameters[AudioRTPParametersTypes.COMFORT_NOISE_PAYLOAD_TYPE].readUInt8(0); // 13 + const audioRTPParameters = decode(audioRTPParametersTLV) + const audioPayloadType = audioRTPParameters[AudioRTPParametersTypes.PAYLOAD_TYPE].readUInt8(0) // 110 + const audioSSRC = audioRTPParameters[AudioRTPParametersTypes.SYNCHRONIZATION_SOURCE].readUInt32LE(0) + const audioMaximumBitrate = audioRTPParameters[AudioRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0) + const audioRTCPInterval = audioRTPParameters[AudioRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0) + const comfortNoisePayloadType = audioRTPParameters[AudioRTPParametersTypes.COMFORT_NOISE_PAYLOAD_TYPE].readUInt8(0) // 13 if (this.requireProxy) { - this.videoProxy!.setOutgoingPayloadType(videoPayloadType); + this.videoProxy!.setOutgoingPayloadType(videoPayloadType) if (!this.disableAudioProxy) { - this.audioProxy!.setOutgoingPayloadType(audioPayloadType); + this.audioProxy!.setOutgoingPayloadType(audioPayloadType) } } - const videoInfo: VideoInfo = { codec: videoCodec.readUInt8(0), profile: h264Profile, level: h264Level, - packetizationMode: packetizationMode, - cvoId: cvoId, + packetizationMode, + cvoId, - width: width, - height: height, + width, + height, fps: frameRate, pt: videoPayloadType, @@ -899,49 +933,49 @@ export class RTPStreamManagement { max_bit_rate: videoMaximumBitrate, rtcp_interval: videoRTCPInterval, mtu: maxMTU, - }; + } - let audioCodecName: AudioStreamingCodecType; - let samplerateNum: AudioStreamingSamplerate; + let audioCodecName: AudioStreamingCodecType + let samplerateNum: AudioStreamingSamplerate switch (audioCodec) { - case AudioCodecTypes.PCMU: - audioCodecName = AudioStreamingCodecType.PCMU; - break; - case AudioCodecTypes.PCMA: - audioCodecName = AudioStreamingCodecType.PCMA; - break; - case AudioCodecTypes.AAC_ELD: - audioCodecName = AudioStreamingCodecType.AAC_ELD; - break; - case AudioCodecTypes.OPUS: - audioCodecName = AudioStreamingCodecType.OPUS; - break; - case AudioCodecTypes.MSBC: - audioCodecName = AudioStreamingCodecType.MSBC; - break; - case AudioCodecTypes.AMR: - audioCodecName = AudioStreamingCodecType.AMR; - break; - case AudioCodecTypes.AMR_WB: - audioCodecName = AudioStreamingCodecType.AMR_WB; - break; - default: - throw new Error(`Encountered unknown selected audio codec ${audioCodec}`); + case AudioCodecTypes.PCMU: + audioCodecName = AudioStreamingCodecType.PCMU + break + case AudioCodecTypes.PCMA: + audioCodecName = AudioStreamingCodecType.PCMA + break + case AudioCodecTypes.AAC_ELD: + audioCodecName = AudioStreamingCodecType.AAC_ELD + break + case AudioCodecTypes.OPUS: + audioCodecName = AudioStreamingCodecType.OPUS + break + case AudioCodecTypes.MSBC: + audioCodecName = AudioStreamingCodecType.MSBC + break + case AudioCodecTypes.AMR: + audioCodecName = AudioStreamingCodecType.AMR + break + case AudioCodecTypes.AMR_WB: + audioCodecName = AudioStreamingCodecType.AMR_WB + break + default: + throw new Error(`Encountered unknown selected audio codec ${audioCodec}`) } switch (samplerate) { - case AudioSamplerate.KHZ_8: - samplerateNum = 8; - break; - case AudioSamplerate.KHZ_16: - samplerateNum = 16; - break; - case AudioSamplerate.KHZ_24: - samplerateNum = 24; - break; - default: - throw new Error(`Encountered unknown selected audio samplerate ${samplerate}`); + case AudioSamplerate.KHZ_8: + samplerateNum = 8 + break + case AudioSamplerate.KHZ_16: + samplerateNum = 16 + break + case AudioSamplerate.KHZ_24: + samplerateNum = 24 + break + default: + throw new Error(`Encountered unknown selected audio samplerate ${samplerate}`) } const audioInfo: AudioInfo = { @@ -959,134 +993,144 @@ export class RTPStreamManagement { comfort_pt: comfortNoisePayloadType, comfortNoiseEnabled: comfortNoise, - }; + } const request: StartStreamRequest = { sessionID: this.sessionIdentifier!, type: StreamRequestTypes.START, video: videoInfo, audio: audioInfo, - }; + } - this.delegate.handleStreamRequest(request, error => callback(error)); + this.delegate.handleStreamRequest(request, error => callback(error)) } private handleReconfigureStream(videoConfiguration: Record, callback: CharacteristicSetCallback): void { // selected video configuration - const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES]; - const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS]; + const videoAttributesTLV = videoConfiguration[SelectedVideoParametersTypes.ATTRIBUTES] + const videoRTPParametersTLV = videoConfiguration[SelectedVideoParametersTypes.RTP_PARAMETERS] // video attributes - const videoAttributes = tlv.decode(videoAttributesTLV); - const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0); - const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0); - const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0); + const videoAttributes = decode(videoAttributesTLV) + const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readUInt16LE(0) + const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readUInt16LE(0) + const frameRate = videoAttributes[VideoAttributesTypes.FRAME_RATE].readUInt8(0) // video rtp parameters - const videoRTPParameters = tlv.decode(videoRTPParametersTLV); - const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0); + const videoRTPParameters = decode(videoRTPParametersTLV) + const videoMaximumBitrate = videoRTPParameters[VideoRTPParametersTypes.MAX_BIT_RATE].readUInt16LE(0) // seems to be always zero, use default of 0.5 - const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0) || 0.5; + const videoRTCPInterval = videoRTPParameters[VideoRTPParametersTypes.MIN_RTCP_INTERVAL].readFloatLE(0) || 0.5 const reconfiguredVideoInfo: ReconfiguredVideoInfo = { - width: width, - height: height, + width, + height, fps: frameRate, max_bit_rate: videoMaximumBitrate, rtcp_interval: videoRTCPInterval, - }; + } const request: ReconfigureStreamRequest = { sessionID: this.sessionIdentifier!, type: StreamRequestTypes.RECONFIGURE, video: reconfiguredVideoInfo, - }; + } - this.delegate.handleStreamRequest(request, error => callback(error)); + this.delegate.handleStreamRequest(request, error => callback(error)) } private _handleStopStream(callback?: CharacteristicSetCallback): void { const request: StopStreamRequest = { sessionID: this.sessionIdentifier!, // save sessionIdentifier before handleSessionClosed is called type: StreamRequestTypes.STOP, - }; + } - this.handleSessionClosed(); + this.handleSessionClosed() - this.delegate.handleStreamRequest(request, error => callback? callback(error): undefined); + this.delegate.handleStreamRequest(request, error => callback ? callback(error) : undefined) } private handleSetupEndpoints(value: CharacteristicValue, callback: CharacteristicSetCallback, connection: HAPConnection): void { if (this.streamingIsDisabled(callback)) { - return; + return } - const data = Buffer.from(value as string, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value as string, 'base64') + const objects = decode(data) - const sessionIdentifier = uuid.unparse(objects[SetupEndpointsTypes.SESSION_ID]); + const sessionIdentifier = unparse(objects[SetupEndpointsTypes.SESSION_ID]) if (this.streamStatus !== StreamingStatus.AVAILABLE) { - this.setupEndpointsResponse = tlv.encode( - SetupEndpointsResponseTypes.SESSION_ID, uuid.write(sessionIdentifier), - SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.BUSY, - ).toString("base64"); - callback(); - return; + this.setupEndpointsResponse = encode( + SetupEndpointsResponseTypes.SESSION_ID, + write(sessionIdentifier), + SetupEndpointsResponseTypes.STATUS, + SetupEndpointsStatus.BUSY, + ).toString('base64') + callback() + return } - assert(this.activeConnection == null, - "Found non-nil `activeConnection` when trying to setup streaming endpoints, even though streamStatus is reported to be AVAILABLE!"); + assert(this.activeConnection == null, 'Found non-nil `activeConnection` when trying to setup streaming endpoints, even though streamStatus is reported to be AVAILABLE!') - this.activeConnection = connection; - this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() + 1); - this.activeConnection.on(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener); + this.activeConnection = connection + this.activeConnection.setMaxListeners(this.activeConnection.getMaxListeners() + 1) + this.activeConnection.on(HAPConnectionEvent.CLOSED, this.activeConnectionClosedListener) - this.sessionIdentifier = sessionIdentifier; - this._updateStreamStatus(StreamingStatus.IN_USE); + this.sessionIdentifier = sessionIdentifier + this._updateStreamStatus(StreamingStatus.IN_USE) // Address - const targetAddressPayload = objects[SetupEndpointsTypes.CONTROLLER_ADDRESS]; - const processedAddressInfo = tlv.decode(targetAddressPayload); - const addressVersion = processedAddressInfo[AddressTypes.ADDRESS_VERSION][0]; - const controllerAddress = processedAddressInfo[AddressTypes.ADDRESS].toString("utf8"); - const targetVideoPort = processedAddressInfo[AddressTypes.VIDEO_RTP_PORT].readUInt16LE(0); - const targetAudioPort = processedAddressInfo[AddressTypes.AUDIO_RTP_PORT].readUInt16LE(0); + const targetAddressPayload = objects[SetupEndpointsTypes.CONTROLLER_ADDRESS] + const processedAddressInfo = decode(targetAddressPayload) + const addressVersion = processedAddressInfo[AddressTypes.ADDRESS_VERSION][0] + const controllerAddress = processedAddressInfo[AddressTypes.ADDRESS].toString('utf8') + const targetVideoPort = processedAddressInfo[AddressTypes.VIDEO_RTP_PORT].readUInt16LE(0) + const targetAudioPort = processedAddressInfo[AddressTypes.AUDIO_RTP_PORT].readUInt16LE(0) // Video SRTP Params - const videoSRTPPayload = objects[SetupEndpointsTypes.VIDEO_SRTP_PARAMETERS]; - const processedVideoInfo = tlv.decode(videoSRTPPayload); - const videoCryptoSuite = processedVideoInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0]; - const videoMasterKey = processedVideoInfo[SRTPParametersTypes.MASTER_KEY]; - const videoMasterSalt = processedVideoInfo[SRTPParametersTypes.MASTER_SALT]; + const videoSRTPPayload = objects[SetupEndpointsTypes.VIDEO_SRTP_PARAMETERS] + const processedVideoInfo = decode(videoSRTPPayload) + const videoCryptoSuite = processedVideoInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0] + const videoMasterKey = processedVideoInfo[SRTPParametersTypes.MASTER_KEY] + const videoMasterSalt = processedVideoInfo[SRTPParametersTypes.MASTER_SALT] // Audio SRTP Params - const audioSRTPPayload = objects[SetupEndpointsTypes.AUDIO_SRTP_PARAMETERS]; - const processedAudioInfo = tlv.decode(audioSRTPPayload); - const audioCryptoSuite = processedAudioInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0]; - const audioMasterKey = processedAudioInfo[SRTPParametersTypes.MASTER_KEY]; - const audioMasterSalt = processedAudioInfo[SRTPParametersTypes.MASTER_SALT]; + const audioSRTPPayload = objects[SetupEndpointsTypes.AUDIO_SRTP_PARAMETERS] + const processedAudioInfo = decode(audioSRTPPayload) + const audioCryptoSuite = processedAudioInfo[SRTPParametersTypes.SRTP_CRYPTO_SUITE][0] + const audioMasterKey = processedAudioInfo[SRTPParametersTypes.MASTER_KEY] + const audioMasterSalt = processedAudioInfo[SRTPParametersTypes.MASTER_SALT] debug( - "Session: ", sessionIdentifier, - "\nControllerAddress: ", controllerAddress, - "\nVideoPort: ", targetVideoPort, - "\nAudioPort: ", targetAudioPort, - "\nVideo Crypto: ", videoCryptoSuite, - "\nVideo Master Key: ", videoMasterKey, - "\nVideo Master Salt: ", videoMasterSalt, - "\nAudio Crypto: ", audioCryptoSuite, - "\nAudio Master Key: ", audioMasterKey, - "\nAudio Master Salt: ", audioMasterSalt, - ); - + 'Session: ', + sessionIdentifier, + '\nControllerAddress: ', + controllerAddress, + '\nVideoPort: ', + targetVideoPort, + '\nAudioPort: ', + targetAudioPort, + '\nVideo Crypto: ', + videoCryptoSuite, + '\nVideo Master Key: ', + videoMasterKey, + '\nVideo Master Salt: ', + videoMasterSalt, + '\nAudio Crypto: ', + audioCryptoSuite, + '\nAudio Master Key: ', + audioMasterKey, + '\nAudio Master Salt: ', + audioMasterSalt, + ) const prepareRequest: PrepareStreamRequest = { sessionID: sessionIdentifier, sourceAddress: connection.localAddress, targetAddress: controllerAddress, - addressVersion: addressVersion === IPAddressVersion.IPV6? "ipv6": "ipv4", + addressVersion: addressVersion === IPAddressVersion.IPV6 ? 'ipv6' : 'ipv4', video: { // if suite is NONE, keys and salts are zero-length port: targetVideoPort, @@ -1102,56 +1146,58 @@ export class RTPStreamManagement { srtp_key: audioMasterKey, srtp_salt: audioMasterSalt, }, - }; + } - const promises: Promise[] = []; + const promises: Promise[] = [] if (this.requireProxy) { - prepareRequest.targetAddress = connection.getLocalAddress(addressVersion === IPAddressVersion.IPV6? "ipv6": "ipv4"); // ip versions must be the same + prepareRequest.targetAddress = connection.getLocalAddress(addressVersion === IPAddressVersion.IPV6 ? 'ipv6' : 'ipv4') // ip versions must be the same this.videoProxy = new RTPProxy({ outgoingAddress: controllerAddress, outgoingPort: targetVideoPort, - outgoingSSRC: crypto.randomBytes(4).readUInt32LE(0), // videoSSRC + outgoingSSRC: randomBytes(4).readUInt32LE(0), // videoSSRC disabled: false, - }); + }) promises.push(this.videoProxy.setup().then(() => { - prepareRequest.video.proxy_rtp = this.videoProxy!.incomingRTPPort(); - prepareRequest.video.proxy_rtcp = this.videoProxy!.incomingRTCPPort(); - })); + prepareRequest.video.proxy_rtp = this.videoProxy!.incomingRTPPort() + prepareRequest.video.proxy_rtcp = this.videoProxy!.incomingRTCPPort() + })) if (!this.disableAudioProxy) { this.audioProxy = new RTPProxy({ outgoingAddress: controllerAddress, outgoingPort: targetAudioPort, - outgoingSSRC: crypto.randomBytes(4).readUInt32LE(0), // audioSSRC + outgoingSSRC: randomBytes(4).readUInt32LE(0), // audioSSRC disabled: this.videoOnly, - }); + }) promises.push(this.audioProxy.setup().then(() => { - prepareRequest.audio.proxy_rtp = this.audioProxy!.incomingRTPPort(); - prepareRequest.audio.proxy_rtcp = this.audioProxy!.incomingRTCPPort(); - })); + prepareRequest.audio.proxy_rtp = this.audioProxy!.incomingRTPPort() + prepareRequest.audio.proxy_rtcp = this.audioProxy!.incomingRTCPPort() + })) } } Promise.all(promises).then(() => { this.delegate.prepareStream(prepareRequest, once((error?: Error, response?: PrepareStreamResponse) => { if (error || !response) { - debug(`PrepareStream request encountered an error: ${error? error.message: undefined}`); - this.setupEndpointsResponse = tlv.encode( - SetupEndpointsResponseTypes.SESSION_ID, uuid.write(sessionIdentifier), - SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR, - ).toString("base64"); - - this.handleSessionClosed(); - callback(error); + debug(`PrepareStream request encountered an error: ${error ? error.message : undefined}`) + this.setupEndpointsResponse = encode( + SetupEndpointsResponseTypes.SESSION_ID, + write(sessionIdentifier), + SetupEndpointsResponseTypes.STATUS, + SetupEndpointsStatus.ERROR, + ).toString('base64') + + this.handleSessionClosed() + callback(error) } else { - this.generateSetupEndpointResponse(connection, sessionIdentifier, prepareRequest, response, callback); + this.generateSetupEndpointResponse(connection, sessionIdentifier, prepareRequest, response, callback) } - })); - }); + })) + }) } private generateSetupEndpointResponse( @@ -1161,24 +1207,24 @@ export class RTPStreamManagement { response: PrepareStreamResponse, callback: CharacteristicSetCallback, ): void { - let address: string; - let addressVersion = request.addressVersion; + let address: string + let addressVersion = request.addressVersion - let videoPort: number; - let audioPort: number; + let videoPort: number + let audioPort: number - let videoCryptoSuite: SRTPCryptoSuites; - let videoSRTPKey: Buffer; - let videoSRTPSalt: Buffer; - let audioCryptoSuite: SRTPCryptoSuites; - let audioSRTPKey: Buffer; - let audioSRTPSalt: Buffer; + let videoCryptoSuite: SRTPCryptoSuites + let videoSRTPKey: Buffer + let videoSRTPSalt: Buffer + let audioCryptoSuite: SRTPCryptoSuites + let audioSRTPKey: Buffer + let audioSRTPSalt: Buffer - let videoSSRC: number; - let audioSSRC: number; + let videoSSRC: number + let audioSSRC: number if (!this.videoOnly && !response.audio) { - throw new Error("Audio was enabled but not supplied in PrepareStreamResponse!"); + throw new Error('Audio was enabled but not supplied in PrepareStreamResponse!') } // Provide default values if audio was not supplied @@ -1187,344 +1233,380 @@ export class RTPStreamManagement { ssrc: CameraController.generateSynchronisationSource(), srtp_key: request.audio.srtp_key, srtp_salt: request.audio.srtp_salt, - }; + } if (!this.requireProxy) { - const videoInfo = response.video as SourceResponse; - const audioInfo = audio as SourceResponse; + const videoInfo = response.video as SourceResponse + const audioInfo = audio as SourceResponse if (response.addressOverride) { - addressVersion = net.isIPv4(response.addressOverride)? "ipv4": "ipv6"; - address = response.addressOverride; + addressVersion = isIPv4(response.addressOverride) ? 'ipv4' : 'ipv6' + address = response.addressOverride } else { - address = connection.getLocalAddress(addressVersion); + address = connection.getLocalAddress(addressVersion) } if (request.addressVersion !== addressVersion) { - throw new Error(`Incoming and outgoing ip address versions must match! Expected ${request.addressVersion} but got ${addressVersion}`); + throw new Error(`Incoming and outgoing ip address versions must match! Expected ${request.addressVersion} but got ${addressVersion}`) } - videoPort = videoInfo.port; - audioPort = audioInfo.port; - + videoPort = videoInfo.port + audioPort = audioInfo.port if (request.video.srtpCryptoSuite !== SRTPCryptoSuites.NONE - && (videoInfo.srtp_key === undefined || videoInfo.srtp_salt === undefined)) { - throw new Error("SRTP was selected for the prepared video stream, but no 'srtp_key' or 'srtp_salt' was specified!"); + && (videoInfo.srtp_key === undefined || videoInfo.srtp_salt === undefined)) { + throw new Error('SRTP was selected for the prepared video stream, but no \'srtp_key\' or \'srtp_salt\' was specified!') } if (request.audio.srtpCryptoSuite !== SRTPCryptoSuites.NONE - && (audioInfo.srtp_key === undefined || audioInfo.srtp_salt === undefined)) { - throw new Error("SRTP was selected for the prepared audio stream, but no 'srtp_key' or 'srtp_salt' was specified!"); + && (audioInfo.srtp_key === undefined || audioInfo.srtp_salt === undefined)) { + throw new Error('SRTP was selected for the prepared audio stream, but no \'srtp_key\' or \'srtp_salt\' was specified!') } - videoCryptoSuite = request.video.srtpCryptoSuite; - videoSRTPKey = videoInfo.srtp_key || Buffer.alloc(0); // key and salt are zero-length for cryptoSuite = NONE - videoSRTPSalt = videoInfo.srtp_salt || Buffer.alloc(0); + videoCryptoSuite = request.video.srtpCryptoSuite + videoSRTPKey = videoInfo.srtp_key || Buffer.alloc(0) // key and salt are zero-length for cryptoSuite = NONE + videoSRTPSalt = videoInfo.srtp_salt || Buffer.alloc(0) - audioCryptoSuite = request.audio.srtpCryptoSuite; - audioSRTPKey = audioInfo.srtp_key || Buffer.alloc(0); // key and salt are zero-length for cryptoSuite = NONE - audioSRTPSalt = audioInfo.srtp_salt || Buffer.alloc(0); + audioCryptoSuite = request.audio.srtpCryptoSuite + audioSRTPKey = audioInfo.srtp_key || Buffer.alloc(0) // key and salt are zero-length for cryptoSuite = NONE + audioSRTPSalt = audioInfo.srtp_salt || Buffer.alloc(0) - - videoSSRC = videoInfo.ssrc; - audioSSRC = audioInfo.ssrc; + videoSSRC = videoInfo.ssrc + audioSSRC = audioInfo.ssrc } else { - const videoInfo = response.video as ProxiedSourceResponse; - - address = connection.getLocalAddress(request.addressVersion); - + const videoInfo = response.video as ProxiedSourceResponse - videoCryptoSuite = SRTPCryptoSuites.NONE; - videoSRTPKey = Buffer.alloc(0); - videoSRTPSalt = Buffer.alloc(0); + address = connection.getLocalAddress(request.addressVersion) - audioCryptoSuite = SRTPCryptoSuites.NONE; - audioSRTPKey = Buffer.alloc(0); - audioSRTPSalt = Buffer.alloc(0); + videoCryptoSuite = SRTPCryptoSuites.NONE + videoSRTPKey = Buffer.alloc(0) + videoSRTPSalt = Buffer.alloc(0) + audioCryptoSuite = SRTPCryptoSuites.NONE + audioSRTPKey = Buffer.alloc(0) + audioSRTPSalt = Buffer.alloc(0) - this.videoProxy!.setIncomingPayloadType(videoInfo.proxy_pt); - this.videoProxy!.setServerAddress(videoInfo.proxy_server_address); - this.videoProxy!.setServerRTPPort(videoInfo.proxy_server_rtp); - this.videoProxy!.setServerRTCPPort(videoInfo.proxy_server_rtcp); + this.videoProxy!.setIncomingPayloadType(videoInfo.proxy_pt) + this.videoProxy!.setServerAddress(videoInfo.proxy_server_address) + this.videoProxy!.setServerRTPPort(videoInfo.proxy_server_rtp) + this.videoProxy!.setServerRTCPPort(videoInfo.proxy_server_rtcp) - videoPort = this.videoProxy!.outgoingLocalPort(); - videoSSRC = this.videoProxy!.outgoingSSRC; + videoPort = this.videoProxy!.outgoingLocalPort() + videoSSRC = this.videoProxy!.outgoingSSRC if (!this.disableAudioProxy) { - const audioInfo = response.audio as ProxiedSourceResponse; - this.audioProxy!.setIncomingPayloadType(audioInfo.proxy_pt); - this.audioProxy!.setServerAddress(audioInfo.proxy_server_address); - this.audioProxy!.setServerRTPPort(audioInfo.proxy_server_rtp); - this.audioProxy!.setServerRTCPPort(audioInfo.proxy_server_rtcp); - - audioPort = this.audioProxy!.outgoingLocalPort(); - audioSSRC = this.audioProxy!.outgoingSSRC; + const audioInfo = response.audio as ProxiedSourceResponse + this.audioProxy!.setIncomingPayloadType(audioInfo.proxy_pt) + this.audioProxy!.setServerAddress(audioInfo.proxy_server_address) + this.audioProxy!.setServerRTPPort(audioInfo.proxy_server_rtp) + this.audioProxy!.setServerRTCPPort(audioInfo.proxy_server_rtcp) + + audioPort = this.audioProxy!.outgoingLocalPort() + audioSSRC = this.audioProxy!.outgoingSSRC } else { - const audioInfo = response.audio as SourceResponse; + const audioInfo = response.audio as SourceResponse - audioPort = audioInfo.port; - audioSSRC = audioInfo.ssrc; + audioPort = audioInfo.port + audioSSRC = audioInfo.ssrc } } - this.ipVersion = addressVersion; // we need to save this in order to calculate some default mtu values later - - const accessoryAddress = tlv.encode( - AddressTypes.ADDRESS_VERSION, addressVersion === "ipv4"? IPAddressVersion.IPV4: IPAddressVersion.IPV6, - AddressTypes.ADDRESS, address, - AddressTypes.VIDEO_RTP_PORT, tlv.writeUInt16(videoPort), - AddressTypes.AUDIO_RTP_PORT, tlv.writeUInt16(audioPort), - ); - - const videoSRTPParameters = tlv.encode( - SRTPParametersTypes.SRTP_CRYPTO_SUITE, videoCryptoSuite, - SRTPParametersTypes.MASTER_KEY, videoSRTPKey, - SRTPParametersTypes.MASTER_SALT, videoSRTPSalt, - ); - - const audioSRTPParameters = tlv.encode( - SRTPParametersTypes.SRTP_CRYPTO_SUITE, audioCryptoSuite, - SRTPParametersTypes.MASTER_KEY, audioSRTPKey, - SRTPParametersTypes.MASTER_SALT, audioSRTPSalt, - ); - - this.setupEndpointsResponse = tlv.encode( - SetupEndpointsResponseTypes.SESSION_ID, uuid.write(identifier), - SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.SUCCESS, - SetupEndpointsResponseTypes.ACCESSORY_ADDRESS, accessoryAddress, - SetupEndpointsResponseTypes.VIDEO_SRTP_PARAMETERS, videoSRTPParameters, - SetupEndpointsResponseTypes.AUDIO_SRTP_PARAMETERS, audioSRTPParameters, - SetupEndpointsResponseTypes.VIDEO_SSRC, tlv.writeUInt32(videoSSRC), - SetupEndpointsResponseTypes.AUDIO_SSRC, tlv.writeUInt32(audioSSRC), - ).toString("base64"); - callback(); + this.ipVersion = addressVersion // we need to save this in order to calculate some default mtu values later + + const accessoryAddress = encode( + AddressTypes.ADDRESS_VERSION, + addressVersion === 'ipv4' ? IPAddressVersion.IPV4 : IPAddressVersion.IPV6, + AddressTypes.ADDRESS, + address, + AddressTypes.VIDEO_RTP_PORT, + writeUInt16(videoPort), + AddressTypes.AUDIO_RTP_PORT, + writeUInt16(audioPort), + ) + + const videoSRTPParameters = encode( + SRTPParametersTypes.SRTP_CRYPTO_SUITE, + videoCryptoSuite, + SRTPParametersTypes.MASTER_KEY, + videoSRTPKey, + SRTPParametersTypes.MASTER_SALT, + videoSRTPSalt, + ) + + const audioSRTPParameters = encode( + SRTPParametersTypes.SRTP_CRYPTO_SUITE, + audioCryptoSuite, + SRTPParametersTypes.MASTER_KEY, + audioSRTPKey, + SRTPParametersTypes.MASTER_SALT, + audioSRTPSalt, + ) + + this.setupEndpointsResponse = encode( + SetupEndpointsResponseTypes.SESSION_ID, + write(identifier), + SetupEndpointsResponseTypes.STATUS, + SetupEndpointsStatus.SUCCESS, + SetupEndpointsResponseTypes.ACCESSORY_ADDRESS, + accessoryAddress, + SetupEndpointsResponseTypes.VIDEO_SRTP_PARAMETERS, + videoSRTPParameters, + SetupEndpointsResponseTypes.AUDIO_SRTP_PARAMETERS, + audioSRTPParameters, + SetupEndpointsResponseTypes.VIDEO_SSRC, + writeUInt32(videoSSRC), + SetupEndpointsResponseTypes.AUDIO_SSRC, + writeUInt32(audioSSRC), + ).toString('base64') + callback() } private _updateStreamStatus(status: StreamingStatus): void { - this.streamStatus = status; + this.streamStatus = status - this.service.updateCharacteristic(Characteristic.StreamingStatus, tlv.encode( - StreamingStatusTypes.STATUS, this.streamStatus, - ).toString("base64")); + this.service.updateCharacteristic(Characteristic.StreamingStatus, encode( + StreamingStatusTypes.STATUS, + this.streamStatus, + ).toString('base64')) } private static _supportedRTPConfiguration(supportedCryptoSuites: SRTPCryptoSuites[]): string { if (supportedCryptoSuites.length === 1 && supportedCryptoSuites[0] === SRTPCryptoSuites.NONE) { - debug("Client claims it doesn't support SRTP. The stream may stops working with future iOS releases."); + debug('Client claims it doesn\'t support SRTP. The stream may stops working with future iOS releases.') } - return tlv.encode(SupportedRTPConfigurationTypes.SRTP_CRYPTO_SUITE, supportedCryptoSuites).toString("base64"); + return encode(SupportedRTPConfigurationTypes.SRTP_CRYPTO_SUITE, supportedCryptoSuites).toString('base64') } private static _supportedVideoStreamConfiguration(videoOptions: VideoStreamingOptions): string { if (!videoOptions.codec) { - throw new Error("Video codec cannot be undefined"); + throw new Error('Video codec cannot be undefined') } if (!videoOptions.resolutions) { - throw new Error("Video resolutions cannot be undefined"); + throw new Error('Video resolutions cannot be undefined') } - let codecParameters = tlv.encode( - VideoCodecParametersTypes.PROFILE_ID, videoOptions.codec.profiles, - VideoCodecParametersTypes.LEVEL, videoOptions.codec.levels, - VideoCodecParametersTypes.PACKETIZATION_MODE, VideoCodecPacketizationMode.NON_INTERLEAVED, - ); + let codecParameters = encode( + VideoCodecParametersTypes.PROFILE_ID, + videoOptions.codec.profiles, + VideoCodecParametersTypes.LEVEL, + videoOptions.codec.levels, + VideoCodecParametersTypes.PACKETIZATION_MODE, + VideoCodecPacketizationMode.NON_INTERLEAVED, + ) if (videoOptions.cvoId != null) { codecParameters = Buffer.concat([ codecParameters, - tlv.encode( - VideoCodecParametersTypes.CVO_ENABLED, VideoCodecCVO.SUPPORTED, - VideoCodecParametersTypes.CVO_ID, videoOptions.cvoId, + encode( + VideoCodecParametersTypes.CVO_ENABLED, + VideoCodecCVO.SUPPORTED, + VideoCodecParametersTypes.CVO_ID, + videoOptions.cvoId, ), - ]); + ]) } - const videoStreamConfiguration = tlv.encode( - VideoCodecConfigurationTypes.CODEC_TYPE, VideoCodecType.H264, - VideoCodecConfigurationTypes.CODEC_PARAMETERS, codecParameters, - VideoCodecConfigurationTypes.ATTRIBUTES, videoOptions.resolutions.map(resolution => { + const videoStreamConfiguration = encode( + VideoCodecConfigurationTypes.CODEC_TYPE, + VideoCodecType.H264, + VideoCodecConfigurationTypes.CODEC_PARAMETERS, + codecParameters, + VideoCodecConfigurationTypes.ATTRIBUTES, + videoOptions.resolutions.map((resolution) => { if (resolution.length !== 3) { - throw new Error("Unexpected video resolution"); + throw new Error('Unexpected video resolution') } - const width = Buffer.alloc(2); - const height = Buffer.alloc(2); - const frameRate = Buffer.alloc(1); - - width.writeUInt16LE(resolution[0], 0); - height.writeUInt16LE(resolution[1], 0); - frameRate.writeUInt8(resolution[2], 0); - - return tlv.encode( - VideoAttributesTypes.IMAGE_WIDTH, width, - VideoAttributesTypes.IMAGE_HEIGHT, height, - VideoAttributesTypes.FRAME_RATE, frameRate, - ); + const width = Buffer.alloc(2) + const height = Buffer.alloc(2) + const frameRate = Buffer.alloc(1) + + width.writeUInt16LE(resolution[0], 0) + height.writeUInt16LE(resolution[1], 0) + frameRate.writeUInt8(resolution[2], 0) + + return encode( + VideoAttributesTypes.IMAGE_WIDTH, + width, + VideoAttributesTypes.IMAGE_HEIGHT, + height, + VideoAttributesTypes.FRAME_RATE, + frameRate, + ) }), - ); + ) - return tlv.encode( - SupportedVideoStreamConfigurationTypes.VIDEO_CODEC_CONFIGURATION, videoStreamConfiguration, - ).toString("base64"); + return encode( + SupportedVideoStreamConfigurationTypes.VIDEO_CODEC_CONFIGURATION, + videoStreamConfiguration, + ).toString('base64') } private checkForLegacyAudioCodecRepresentation(codecs: AudioStreamingCodec[]) { // we basically merge the samplerates here - const codecMap: Record = {}; + const codecMap: Record = {} - codecs.slice().forEach(codec => { - const previous = codecMap[codec.type]; + codecs.slice().forEach((codec) => { + const previous = codecMap[codec.type] if (previous) { - if (typeof previous.samplerate === "number") { - previous.samplerate = [previous.samplerate]; + if (typeof previous.samplerate === 'number') { + previous.samplerate = [previous.samplerate] } - previous.samplerate = previous.samplerate.concat(codec.samplerate); + previous.samplerate = previous.samplerate.concat(codec.samplerate) - const index = codecs.indexOf(codec); + const index = codecs.indexOf(codec) if (index >= 0) { - codecs.splice(index, 1); + codecs.splice(index, 1) } } else { - codecMap[codec.type] = codec; + codecMap[codec.type] = codec } - }); + }) } private _supportedAudioStreamConfiguration(audioOptions?: AudioStreamingOptions): string { // Only AAC-ELD and OPUS are accepted by iOS currently, and we need to give it something it will accept // for it to start the video stream. - const comfortNoise = audioOptions && !!audioOptions.comfort_noise; - const supportedCodecs: AudioStreamingCodec[] = (audioOptions && audioOptions.codecs) || []; - this.checkForLegacyAudioCodecRepresentation(supportedCodecs); + const comfortNoise = audioOptions && !!audioOptions.comfort_noise + const supportedCodecs: AudioStreamingCodec[] = (audioOptions && audioOptions.codecs) || [] + this.checkForLegacyAudioCodecRepresentation(supportedCodecs) if (supportedCodecs.length === 0) { // Fake a Codec if we haven't got anything - debug("Client doesn't support any audio codec that HomeKit supports."); - this.videoOnly = true; + debug('Client doesn\'t support any audio codec that HomeKit supports.') + this.videoOnly = true supportedCodecs.push({ type: AudioStreamingCodecType.OPUS, // Opus @16K required by Apple Watch AFAIK samplerate: [AudioStreamingSamplerate.KHZ_16, AudioStreamingSamplerate.KHZ_24], // 16 and 24 must be supported - }); + }) } - const codecConfigurations: Buffer[] = supportedCodecs.map(codec => { - let type: AudioCodecTypes; + const codecConfigurations: Buffer[] = supportedCodecs.map((codec) => { + let type: AudioCodecTypes switch (codec.type) { - case AudioStreamingCodecType.OPUS: - type = AudioCodecTypes.OPUS; - break; - case AudioStreamingCodecType.AAC_ELD: - type = AudioCodecTypes.AAC_ELD; - break; - case AudioStreamingCodecType.PCMA: - type = AudioCodecTypes.PCMA; - break; - case AudioStreamingCodecType.PCMU: - type = AudioCodecTypes.PCMU; - break; - case AudioStreamingCodecType.MSBC: - type = AudioCodecTypes.MSBC; - break; - case AudioStreamingCodecType.AMR: - type = AudioCodecTypes.AMR; - break; - case AudioStreamingCodecType.AMR_WB: - type = AudioCodecTypes.AMR_WB; - break; - default: - throw new Error("Unsupported codec: " + codec.type); + case AudioStreamingCodecType.OPUS: + type = AudioCodecTypes.OPUS + break + case AudioStreamingCodecType.AAC_ELD: + type = AudioCodecTypes.AAC_ELD + break + case AudioStreamingCodecType.PCMA: + type = AudioCodecTypes.PCMA + break + case AudioStreamingCodecType.PCMU: + type = AudioCodecTypes.PCMU + break + case AudioStreamingCodecType.MSBC: + type = AudioCodecTypes.MSBC + break + case AudioStreamingCodecType.AMR: + type = AudioCodecTypes.AMR + break + case AudioStreamingCodecType.AMR_WB: + type = AudioCodecTypes.AMR_WB + break + default: + throw new Error(`Unsupported codec: ${codec.type}`) } - const providedSamplerates = (typeof codec.samplerate === "number"? [codec.samplerate]: codec.samplerate).map(rate => { - let samplerate; + const providedSamplerates = (typeof codec.samplerate === 'number' ? [codec.samplerate] : codec.samplerate).map((rate) => { + let samplerate switch (rate) { - case AudioStreamingSamplerate.KHZ_8: - samplerate = AudioSamplerate.KHZ_8; - break; - case AudioStreamingSamplerate.KHZ_16: - samplerate = AudioSamplerate.KHZ_16; - break; - case AudioStreamingSamplerate.KHZ_24: - samplerate = AudioSamplerate.KHZ_24; - break; - default: - console.log("Unsupported sample rate: ", codec.samplerate); - samplerate = -1; + case AudioStreamingSamplerate.KHZ_8: + samplerate = AudioSamplerate.KHZ_8 + break + case AudioStreamingSamplerate.KHZ_16: + samplerate = AudioSamplerate.KHZ_16 + break + case AudioStreamingSamplerate.KHZ_24: + samplerate = AudioSamplerate.KHZ_24 + break + default: + // eslint-disable-next-line no-console + console.log('Unsupported sample rate: ', codec.samplerate) + samplerate = -1 } - return samplerate; - }).filter(rate => rate !== -1); + return samplerate + }).filter(rate => rate !== -1) if (providedSamplerates.length === 0) { - throw new Error("Audio samplerate cannot be empty!"); + throw new Error('Audio samplerate cannot be empty!') } - const audioParameters = tlv.encode( - AudioCodecParametersTypes.CHANNEL, Math.max(1, codec.audioChannels || 1), - AudioCodecParametersTypes.BIT_RATE, codec.bitrate || AudioBitrate.VARIABLE, - AudioCodecParametersTypes.SAMPLE_RATE, providedSamplerates, - ); - - return tlv.encode( - AudioCodecConfigurationTypes.CODEC_TYPE, type, - AudioCodecConfigurationTypes.CODEC_PARAMETERS, audioParameters, - ); - }); - - return tlv.encode( - SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION, codecConfigurations, - SupportedAudioStreamConfigurationTypes.COMFORT_NOISE_SUPPORT, comfortNoise? 1: 0, - ).toString("base64"); + const audioParameters = encode( + AudioCodecParametersTypes.CHANNEL, + Math.max(1, codec.audioChannels || 1), + AudioCodecParametersTypes.BIT_RATE, + codec.bitrate || AudioBitrate.VARIABLE, + AudioCodecParametersTypes.SAMPLE_RATE, + providedSamplerates, + ) + + return encode( + AudioCodecConfigurationTypes.CODEC_TYPE, + type, + AudioCodecConfigurationTypes.CODEC_PARAMETERS, + audioParameters, + ) + }) + + return encode( + SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION, + codecConfigurations, + SupportedAudioStreamConfigurationTypes.COMFORT_NOISE_SUPPORT, + comfortNoise ? 1 : 0, + ).toString('base64') } private resetSetupEndpointsResponse(): void { - this.setupEndpointsResponse = tlv.encode( - SetupEndpointsResponseTypes.STATUS, SetupEndpointsStatus.ERROR, - ).toString("base64"); - this.service.updateCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse); + this.setupEndpointsResponse = encode( + SetupEndpointsResponseTypes.STATUS, + SetupEndpointsStatus.ERROR, + ).toString('base64') + this.service.updateCharacteristic(Characteristic.SetupEndpoints, this.setupEndpointsResponse) } private resetSelectedStreamConfiguration(): void { - this.selectedConfiguration = tlv.encode( - SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, tlv.encode( - SessionControlTypes.COMMAND, SessionControlCommand.SUSPEND_SESSION, + this.selectedConfiguration = encode( + SelectedRTPStreamConfigurationTypes.SESSION_CONTROL, + encode( + SessionControlTypes.COMMAND, + SessionControlCommand.SUSPEND_SESSION, ), - ).toString("base64"); - this.service.updateCharacteristic(Characteristic.SelectedRTPStreamConfiguration, this.selectedConfiguration); + ).toString('base64') + this.service.updateCharacteristic(Characteristic.SelectedRTPStreamConfiguration, this.selectedConfiguration) } /** * @private */ serialize(): RTPStreamManagementState | undefined { - const characteristicValue = this.service.getCharacteristic(Characteristic.Active).value; + const characteristicValue = this.service.getCharacteristic(Characteristic.Active).value if (characteristicValue === true) { - return undefined; + return undefined } return { id: this.id, active: !!characteristicValue, - }; + } } /** * @private */ deserialize(serialized: RTPStreamManagementState): void { - assert(serialized.id === this.id, `Tried to initialize RTPStreamManagement ${this.id} with data from management with id ${serialized.id}!`); + assert(serialized.id === this.id, `Tried to initialize RTPStreamManagement ${this.id} with data from management with id ${serialized.id}!`) - this.service.updateCharacteristic(Characteristic.Active, serialized.active); + this.service.updateCharacteristic(Characteristic.Active, serialized.active) } /** * @private */ setupStateChangeDelegate(delegate?: StateChangeDelegate): void { - this.stateChangeDelegate = delegate; + this.stateChangeDelegate = delegate } } - diff --git a/src/lib/camera/RecordingManagement.spec.ts b/src/lib/camera/RecordingManagement.spec.ts index 6155080ff..403cc05af 100644 --- a/src/lib/camera/RecordingManagement.spec.ts +++ b/src/lib/camera/RecordingManagement.spec.ts @@ -1,40 +1,43 @@ -import { Characteristic } from "../Characteristic"; -import { MockDelegate, mockRecordingOptions } from "../controller/CameraController.spec"; +import type { CameraRecordingConfiguration } from './RecordingManagement' + +import { describe, expect, it } from 'vitest' + +import { Characteristic } from '../Characteristic.js' +import { MockDelegate, mockRecordingOptions } from '../controller/CameraController.spec.js' import { AudioRecordingCodecType, AudioRecordingSamplerate, - CameraRecordingConfiguration, EventTriggerOption, MediaContainerType, RecordingManagement, -} from "./RecordingManagement"; -import { AudioBitrate, H264Level, H264Profile, VideoCodecType } from "./RTPStreamManagement"; +} from './RecordingManagement.js' +import { AudioBitrate, H264Level, H264Profile, VideoCodecType } from './RTPStreamManagement.js' -describe("RecordingManagement", () => { - test("handleSelectedCameraRecordingConfiguration", () => { - const base64Input = "AR0BBKAPAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECsAQCAkAGAwEYAxQBAQACDwEBAQIBAAMBAwQEQAAAAA=="; +describe('recordingManagement', () => { + it('handleSelectedCameraRecordingConfiguration', () => { + const base64Input = 'AR0BBKAPAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECsAQCAkAGAwEYAxQBAQACDwEBAQIBAAMBAwQEQAAAAA==' const management = new RecordingManagement( mockRecordingOptions, new MockDelegate(), new Set([EventTriggerOption.MOTION, EventTriggerOption.DOORBELL]), - ); + ) // @ts-expect-error: private access - management.handleSelectedCameraRecordingConfigurationWrite(base64Input); + management.handleSelectedCameraRecordingConfigurationWrite(base64Input) // @ts-expect-error: private access expect(management.handleSelectedCameraRecordingConfigurationRead()) - .toEqual(base64Input); + .toEqual(base64Input) // @ts-expect-error: private access - const configuration = management.selectedConfiguration; - expect(configuration).toBeDefined(); - expect(configuration!.base64).toEqual(base64Input); + const configuration = management.selectedConfiguration + expect(configuration).toBeDefined() + expect(configuration!.base64).toEqual(base64Input) const expected: CameraRecordingConfiguration = { prebufferLength: 4000, - eventTriggerTypes: [ EventTriggerOption.MOTION ], + eventTriggerTypes: [EventTriggerOption.MOTION], mediaContainerConfiguration: { type: MediaContainerType.FRAGMENTED_MP4, fragmentLength: 4000 }, videoCodec: { type: VideoCodecType.H264, @@ -44,7 +47,7 @@ describe("RecordingManagement", () => { bitRate: 2000, iFrameInterval: 4000, }, - resolution: [ 1200, 1600, 24 ], + resolution: [1200, 1600, 24], }, audioCodec: { type: AudioRecordingCodecType.AAC_LC, @@ -53,11 +56,11 @@ describe("RecordingManagement", () => { bitrateMode: AudioBitrate.VARIABLE, bitrate: 64, }, - }; - expect(configuration!.parsed).toEqual(expected); - }); + } + expect(configuration!.parsed).toEqual(expected) + }) - test("test supported configuration", () => { + it('test supported configuration', () => { const management = new RecordingManagement( { prebufferLength: 8000, @@ -101,19 +104,19 @@ describe("RecordingManagement", () => { }, new MockDelegate(), new Set([EventTriggerOption.MOTION, EventTriggerOption.DOORBELL]), - ); + ) const supportedRecording = management.recordingManagementService - .getCharacteristic(Characteristic.SupportedCameraRecordingConfiguration).value; + .getCharacteristic(Characteristic.SupportedCameraRecordingConfiguration).value const supportedVideo = management.recordingManagementService - .getCharacteristic(Characteristic.SupportedVideoRecordingConfiguration).value; + .getCharacteristic(Characteristic.SupportedVideoRecordingConfiguration).value const supportedAudio = management.recordingManagementService - .getCharacteristic(Characteristic.SupportedAudioRecordingConfiguration).value; + .getCharacteristic(Characteristic.SupportedAudioRecordingConfiguration).value - expect(supportedRecording).toEqual("AQRAHwAAAggDAAAAAAAAAAMLAQEAAgYBBKAPAAA="); - expect(supportedVideo).toEqual("Af4BAQACCwEBAQIBAAAAAgECAwsBAkAGAgKwBAMBGAAAAwsBAqAFAgI4BAMBGAAAAwsBAgAFAgLAAwMBGAAAAwsBAgAEAgIAAwMBGAAAAwsBAk" + - "AGAgKwBAMBDwAAAwsBAqAFAgI4BAMBDwAAAwsBAgAFAgLAAwMBDwAAAwsBAgAEAgIAAwMBDwAAAwsBArAEAgJABgMBGAAAAwsBAjgEAgKgBQMBGAAAAwsBAsADAgIABQMBGAAAAwsBAgADAgIABA" + - "MBGAAAAwsBArAEAgJABgMBDwAAAwsBAjgEAgKgBQMBDwAAAwsBAsADAgIABQMBDwAAAwsBAgADAgIABAMBDw=="); - expect(supportedAudio).toEqual("AQ4BAQACCQEBAQIBAAMBAw=="); - }); -}); + expect(supportedRecording).toEqual('AQRAHwAAAggDAAAAAAAAAAMLAQEAAgYBBKAPAAA=') + expect(supportedVideo).toEqual('Af4BAQACCwEBAQIBAAAAAgECAwsBAkAGAgKwBAMBGAAAAwsBAqAFAgI4BAMBGAAAAwsBAgAFAgLAAwMBGAAAAwsBAgAEAgIAAwMBGAAAAwsBAk' + + 'AGAgKwBAMBDwAAAwsBAqAFAgI4BAMBDwAAAwsBAgAFAgLAAwMBDwAAAwsBAgAEAgIAAwMBDwAAAwsBArAEAgJABgMBGAAAAwsBAjgEAgKgBQMBGAAAAwsBAsADAgIABQMBGAAAAwsBAgADAgIABA' + + 'MBGAAAAwsBArAEAgJABgMBDwAAAwsBAjgEAgKgBQMBDwAAAwsBAsADAgIABQMBDwAAAwsBAgADAgIABAMBDw==') + expect(supportedAudio).toEqual('AQ4BAQACCQEBAQIBAAMBAw==') + }) +}) diff --git a/src/lib/camera/RecordingManagement.ts b/src/lib/camera/RecordingManagement.ts index 09889745e..a2e651882 100644 --- a/src/lib/camera/RecordingManagement.ts +++ b/src/lib/camera/RecordingManagement.ts @@ -1,32 +1,40 @@ -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { AudioBitrate, VideoCodecType } from "."; -import { Access, Characteristic, CharacteristicEventTypes } from "../Characteristic"; -import { CameraRecordingDelegate, StateChangeDelegate } from "../controller"; -import { +/* global NodeJS */ +import type { VideoCodecType } from '.' +import type { CameraRecordingDelegate, StateChangeDelegate } from '../controller' +import type { DataStreamConnection, - DataStreamConnectionEvent, - DataStreamManagement, DataStreamProtocolHandler, EventHandler, + RequestHandler, +} from '../datastream' +import type { CameraOperatingMode, CameraRecordingManagement } from '../definitions' +import type { H264CodecParameters, H264Level, H264Profile, Resolution } from './RTPStreamManagement' + +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' +import { EventEmitter } from 'node:events' + +import createDebug from 'debug' + +import { Access, Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { + DataStreamConnectionEvent, + DataStreamManagement, HDSConnectionError, HDSConnectionErrorType, HDSProtocolError, HDSProtocolSpecificErrorReason, HDSStatus, Protocols, - RequestHandler, Topics, -} from "../datastream"; -import { CameraOperatingMode, CameraRecordingManagement } from "../definitions"; -import { HAPStatus } from "../HAPServer"; -import { Service } from "../Service"; -import { HapStatusError } from "../util/hapStatusError"; -import * as tlv from "../util/tlv"; -import { H264CodecParameters, H264Level, H264Profile, Resolution } from "./RTPStreamManagement"; +} from '../datastream/index.js' +import { HAPStatus } from '../HAPServer.js' +import { Service } from '../Service.js' +import { HapStatusError } from '../util/hapStatusError.js' +import { decode, encode } from '../util/tlv.js' +import { AudioBitrate } from './index.js' -const debug = createDebug("HAP-NodeJS:Camera:RecordingManagement"); +const debug = createDebug('HAP-NodeJS:Camera:RecordingManagement') /** * Describes options passed to the {@link RecordingManagement}. @@ -43,15 +51,15 @@ export interface CameraRecordingOptions { * This exactly is the prebuffer. A camera will constantly store the last * x seconds (the `prebufferLength`) to provide more context to a given event. */ - prebufferLength: number; + prebufferLength: number /** * This property can be used to override the automatic heuristic of the {@link CameraController} * which derives the {@link EventTriggerOption}s from application state. * * {@link EventTriggerOption}s are derived automatically as follows: - * * {@link EventTriggerOption.MOTION} is enabled when a {@link Service.MotionSensor} is configured (via {@link CameraControllerOptions.sensors}). - * * {@link EventTriggerOption.DOORBELL} is enabled when the {@link DoorbellController} is used. + * {@link EventTriggerOption.MOTION} is enabled when a {@link Service.MotionSensor} is configured (via {@link CameraControllerOptions.sensors}). + * {@link EventTriggerOption.DOORBELL} is enabled when the {@link DoorbellController} is used. * * Note: This property is **ADDITIVE**. Meaning if the {@link CameraController} decides to add * a certain {@link EventTriggerOption} it will still do so. This option can only be used to @@ -62,10 +70,10 @@ export interface CameraRecordingOptions { /** * List of supported media {@link MediaContainerConfiguration}s (or a single one). */ - mediaContainerConfiguration: MediaContainerConfiguration | MediaContainerConfiguration[]; + mediaContainerConfiguration: MediaContainerConfiguration | MediaContainerConfiguration[] - video: VideoRecordingOptions, - audio: AudioRecordingOptions, + video: VideoRecordingOptions + audio: AudioRecordingOptions } /** @@ -73,6 +81,7 @@ export interface CameraRecordingOptions { * * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum EventTriggerOption { /** * The Motion trigger. If enabled motion should trigger the start of a recording. @@ -92,8 +101,9 @@ export const enum EventTriggerOption { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum MediaContainerType { - FRAGMENTED_MP4 = 0x00 + FRAGMENTED_MP4 = 0x00, } /** @@ -103,57 +113,57 @@ export interface MediaContainerConfiguration { /** * The type of media container. */ - type: MediaContainerType; + type: MediaContainerType /** * The length in milliseconds of every individual recording fragment. * A typical value of HomeKit Secure Video cameras is 4000ms. */ - fragmentLength: number; + fragmentLength: number } /** * @group Camera */ export interface VideoRecordingOptions { - type: VideoCodecType; - parameters: H264CodecParameters; + type: VideoCodecType + parameters: H264CodecParameters /** * Required resolutions to be supported are: - * * 1920x1080 - * * 1280x720 + * 1920x1080 + * 1280x720 * * The following frame rates are required to be supported: - * * 15 fps - * * 24fps or 30fps + * 15 fps + * 24fps or 30fps */ - resolutions: Resolution[]; + resolutions: Resolution[] } /** * @group Camera */ -export type AudioRecordingOptions = { +export interface AudioRecordingOptions { /** * List (or single entry) of supported {@link AudioRecordingCodec}s. */ - codecs: AudioRecordingCodec | AudioRecordingCodec[], + codecs: AudioRecordingCodec | AudioRecordingCodec[] } /** * @group Camera */ -export type AudioRecordingCodec = { - type: AudioRecordingCodecType, +export interface AudioRecordingCodec { + type: AudioRecordingCodecType /** * The count of audio channels. Must be at least `1`. * Defaults to `1`. */ - audioChannels?: number, + audioChannels?: number /** * The supported bitrate mode. Defaults to {@link AudioBitrate.VARIABLE}. */ - bitrateMode?: AudioBitrate, - samplerate: AudioRecordingSamplerate[] | AudioRecordingSamplerate, + bitrateMode?: AudioBitrate + samplerate: AudioRecordingSamplerate[] | AudioRecordingSamplerate } /** @@ -166,54 +176,55 @@ export interface CameraRecordingConfiguration { * The size of the prebuffer in milliseconds. * This value is less or equal of the value advertised in the {@link Characteristic.SupportedCameraRecordingConfiguration}. */ - prebufferLength: number; + prebufferLength: number /** * List of the enabled {@link EventTriggerOption}s. */ - eventTriggerTypes: EventTriggerOption[]; + eventTriggerTypes: EventTriggerOption[] /** * The selected {@link MediaContainerConfiguration}. */ - mediaContainerConfiguration: MediaContainerConfiguration; + mediaContainerConfiguration: MediaContainerConfiguration /** * The selected video codec configuration. */ videoCodec: { - type: VideoCodecType.H264; - parameters: SelectedH264CodecParameters, - resolution: Resolution, - }, + type: VideoCodecType.H264 + parameters: SelectedH264CodecParameters + resolution: Resolution + } /** * The selected audio codec configuration. */ audioCodec: AudioRecordingCodec & { - bitrate: number, - samplerate: AudioRecordingSamplerate, - }, + bitrate: number + samplerate: AudioRecordingSamplerate + } } /** * @group Camera */ export interface SelectedH264CodecParameters { - profile: H264Profile, - level: H264Level, - bitRate: number, + profile: H264Profile + level: H264Level + bitRate: number /** * The selected i-frame interval in milliseconds. */ - iFrameInterval: number, + iFrameInterval: number } - +// eslint-disable-next-line no-restricted-syntax const enum VideoCodecConfigurationTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, ATTRIBUTES = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum VideoCodecParametersTypes { PROFILE_ID = 0x01, LEVEL = 0x02, @@ -221,12 +232,14 @@ const enum VideoCodecParametersTypes { IFRAME_INTERVAL = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum VideoAttributesTypes { IMAGE_WIDTH = 0x01, IMAGE_HEIGHT = 0x02, FRAME_RATE = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum SelectedCameraRecordingConfigurationTypes { SELECTED_RECORDING_CONFIGURATION = 0x01, SELECTED_VIDEO_CONFIGURATION = 0x02, @@ -236,6 +249,7 @@ const enum SelectedCameraRecordingConfigurationTypes { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioRecordingCodecType { AAC_LC = 0, AAC_ELD = 1, @@ -244,6 +258,7 @@ export const enum AudioRecordingCodecType { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioRecordingSamplerate { KHZ_8 = 0, KHZ_16 = 1, @@ -253,37 +268,44 @@ export const enum AudioRecordingSamplerate { KHZ_48 = 5, } +// eslint-disable-next-line no-restricted-syntax const enum SupportedVideoRecordingConfigurationTypes { VIDEO_CODEC_CONFIGURATION = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum SupportedCameraRecordingConfigurationTypes { PREBUFFER_LENGTH = 0x01, EVENT_TRIGGER_OPTIONS = 0x02, - MEDIA_CONTAINER_CONFIGURATIONS = 0x03 + MEDIA_CONTAINER_CONFIGURATIONS = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum MediaContainerConfigurationTypes { MEDIA_CONTAINER_TYPE = 0x01, MEDIA_CONTAINER_PARAMETERS = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum MediaContainerParameterTypes { FRAGMENT_LENGTH = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecParametersTypes { CHANNEL = 0x01, BIT_RATE = 0x02, SAMPLE_RATE = 0x03, - MAX_AUDIO_BITRATE = 0x04 // only present in selected audio codec parameters tlv + MAX_AUDIO_BITRATE = 0x04, // only present in selected audio codec parameters tlv } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecConfigurationTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum SupportedAudioRecordingConfigurationTypes { AUDIO_CODEC_CONFIGURATION = 0x01, } @@ -291,30 +313,31 @@ const enum SupportedAudioRecordingConfigurationTypes { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum PacketDataType { /** * mp4 moov box */ - MEDIA_INITIALIZATION = "mediaInitialization", + MEDIA_INITIALIZATION = 'mediaInitialization', /** * mp4 moof + mdat boxes */ - MEDIA_FRAGMENT = "mediaFragment", + MEDIA_FRAGMENT = 'mediaFragment', } interface DataSendDataEvent { - streamId: number; + streamId: number packets: { - data: Buffer; + data: Buffer metadata: { - dataType: PacketDataType, - dataSequenceNumber: number, - isLastDataChunk: boolean, - dataChunkSequenceNumber: number, - dataTotalSize?: number, + dataType: PacketDataType + dataSequenceNumber: number + isLastDataChunk: boolean + dataChunkSequenceNumber: number + dataTotalSize?: number } - }[]; - endOfStream?: boolean; + }[] + endOfStream?: boolean } /** @@ -324,22 +347,21 @@ export interface RecordingPacket { /** * The `Buffer` containing the data of the packet. */ - data: Buffer; + data: Buffer /** * Defines if this `RecordingPacket` is the last one in the recording stream. * If `true` this will signal an end of stream and closes the recording stream. */ - isLast: boolean; + isLast: boolean } - /** * @group Camera */ export interface RecordingManagementServices { - recordingManagement: CameraRecordingManagement; - operatingMode: CameraOperatingMode; - dataStreamManagement: DataStreamManagement; + recordingManagement: CameraRecordingManagement + operatingMode: CameraOperatingMode + dataStreamManagement: DataStreamManagement } /** @@ -353,76 +375,76 @@ export interface RecordingManagementState { * that they might reconsider their decision based on the updated configuration. */ configurationHash: { - algorithm: "sha256"; - hash: string; - }; + algorithm: 'sha256' + hash: string + } /** * The base64 encoded tlv of the {@link CameraRecordingConfiguration}. * This value MIGHT be `undefined` if no HomeKit controller has yet selected a configuration. */ - selectedConfiguration?: string; + selectedConfiguration?: string /** * Service `CameraRecordingManagement`; Characteristic `Active` */ - recordingActive: boolean; + recordingActive: boolean /** * Service `CameraRecordingManagement`; Characteristic `RecordingAudioActive` */ - recordingAudioActive: boolean; + recordingAudioActive: boolean /** * Service `CameraOperatingMode`; Characteristic `EventSnapshotsActive` */ - eventSnapshotsActive: boolean; + eventSnapshotsActive: boolean /** * Service `CameraOperatingMode`; Characteristic `HomeKitCameraActive` */ - homeKitCameraActive: boolean; + homeKitCameraActive: boolean /** * Service `CameraOperatingMode`; Characteristic `PeriodicSnapshotsActive` */ - periodicSnapshotsActive: boolean; + periodicSnapshotsActive: boolean } /** * @group Camera */ export class RecordingManagement { - readonly options: CameraRecordingOptions; - readonly delegate: CameraRecordingDelegate; + readonly options: CameraRecordingOptions + readonly delegate: CameraRecordingDelegate - private stateChangeDelegate?: StateChangeDelegate; + private stateChangeDelegate?: StateChangeDelegate - private readonly supportedCameraRecordingConfiguration: string; - private readonly supportedVideoRecordingConfiguration: string; - private readonly supportedAudioRecordingConfiguration: string; + private readonly supportedCameraRecordingConfiguration: string + private readonly supportedVideoRecordingConfiguration: string + private readonly supportedAudioRecordingConfiguration: string /** * 32 bit mask of enabled {@link EventTriggerOption}s. */ - private readonly eventTriggerOptions: number; + private readonly eventTriggerOptions: number - readonly recordingManagementService: CameraRecordingManagement; - readonly operatingModeService: CameraOperatingMode; - readonly dataStreamManagement: DataStreamManagement; + readonly recordingManagementService: CameraRecordingManagement + readonly operatingModeService: CameraOperatingMode + readonly dataStreamManagement: DataStreamManagement /** * The currently active recording stream. * Any camera only supports one stream at a time. */ - private recordingStream?: CameraRecordingStream; + private recordingStream?: CameraRecordingStream private selectedConfiguration?: { /** * The parsed configuration structure. */ - parsed: CameraRecordingConfiguration, + parsed: CameraRecordingConfiguration /** * The rawValue representation. TLV8 data encoded as base64 string. */ - base64: string, - }; + base64: string + } /** * Array of sensor services (e.g. {@link Service.MotionSensor} or {@link Service.OccupancySensor}). @@ -430,12 +452,12 @@ export class RecordingManagement { * The value of the {@link Characteristic.HomeKitCameraActive} is mirrored towards the {@link Characteristic.StatusActive} characteristic. * The array is initialized my the caller shortly after calling the constructor. */ - sensorServices: Service[] = []; + sensorServices: Service[] = [] /** * Defines if recording is enabled for this recording management. */ - private recordingActive = false; + private recordingActive = false constructor( options: CameraRecordingOptions, @@ -443,232 +465,230 @@ export class RecordingManagement { eventTriggerOptions: Set, services?: RecordingManagementServices, ) { - this.options = options; - this.delegate = delegate; + this.options = options + this.delegate = delegate - const recordingServices = services || this.constructService(); - this.recordingManagementService = recordingServices.recordingManagement; - this.operatingModeService = recordingServices.operatingMode; - this.dataStreamManagement = recordingServices.dataStreamManagement; + const recordingServices = services || this.constructService() + this.recordingManagementService = recordingServices.recordingManagement + this.operatingModeService = recordingServices.operatingMode + this.dataStreamManagement = recordingServices.dataStreamManagement - this.eventTriggerOptions = 0; + this.eventTriggerOptions = 0 for (const option of eventTriggerOptions) { - this.eventTriggerOptions |= option; // OR + this.eventTriggerOptions |= option // OR } - this.supportedCameraRecordingConfiguration = this._supportedCameraRecordingConfiguration(options); - this.supportedVideoRecordingConfiguration = this._supportedVideoRecordingConfiguration(options.video); - this.supportedAudioRecordingConfiguration = this._supportedAudioStreamConfiguration(options.audio); + this.supportedCameraRecordingConfiguration = this._supportedCameraRecordingConfiguration(options) + this.supportedVideoRecordingConfiguration = this._supportedVideoRecordingConfiguration(options.video) + this.supportedAudioRecordingConfiguration = this._supportedAudioStreamConfiguration(options.audio) - this.setupServiceHandlers(); + this.setupServiceHandlers() } private constructService(): RecordingManagementServices { - const recordingManagement = new Service.CameraRecordingManagement("", ""); - recordingManagement.setCharacteristic(Characteristic.Active, false); - recordingManagement.setCharacteristic(Characteristic.RecordingAudioActive, false); + const recordingManagement = new Service.CameraRecordingManagement('', '') + recordingManagement.setCharacteristic(Characteristic.Active, false) + recordingManagement.setCharacteristic(Characteristic.RecordingAudioActive, false) - const operatingMode = new Service.CameraOperatingMode("", ""); - operatingMode.setCharacteristic(Characteristic.EventSnapshotsActive, true); - operatingMode.setCharacteristic(Characteristic.HomeKitCameraActive, true); - operatingMode.setCharacteristic(Characteristic.PeriodicSnapshotsActive, true); + const operatingMode = new Service.CameraOperatingMode('', '') + operatingMode.setCharacteristic(Characteristic.EventSnapshotsActive, true) + operatingMode.setCharacteristic(Characteristic.HomeKitCameraActive, true) + operatingMode.setCharacteristic(Characteristic.PeriodicSnapshotsActive, true) - const dataStreamManagement = new DataStreamManagement(); - recordingManagement.addLinkedService(dataStreamManagement.getService()); + const dataStreamManagement = new DataStreamManagement() + recordingManagement.addLinkedService(dataStreamManagement.getService()) return { - recordingManagement: recordingManagement, - operatingMode: operatingMode, - dataStreamManagement: dataStreamManagement, - }; + recordingManagement, + operatingMode, + dataStreamManagement, + } } private setupServiceHandlers() { // update the current configuration values to the current state. - this.recordingManagementService.setCharacteristic(Characteristic.SupportedCameraRecordingConfiguration, this.supportedCameraRecordingConfiguration); - this.recordingManagementService.setCharacteristic(Characteristic.SupportedVideoRecordingConfiguration, this.supportedVideoRecordingConfiguration); - this.recordingManagementService.setCharacteristic(Characteristic.SupportedAudioRecordingConfiguration, this.supportedAudioRecordingConfiguration); + this.recordingManagementService.setCharacteristic(Characteristic.SupportedCameraRecordingConfiguration, this.supportedCameraRecordingConfiguration) + this.recordingManagementService.setCharacteristic(Characteristic.SupportedVideoRecordingConfiguration, this.supportedVideoRecordingConfiguration) + this.recordingManagementService.setCharacteristic(Characteristic.SupportedAudioRecordingConfiguration, this.supportedAudioRecordingConfiguration) this.recordingManagementService.getCharacteristic(Characteristic.SelectedCameraRecordingConfiguration) .onGet(this.handleSelectedCameraRecordingConfigurationRead.bind(this)) .onSet(this.handleSelectedCameraRecordingConfigurationWrite.bind(this)) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) this.recordingManagementService.getCharacteristic(Characteristic.Active) - .onSet(value => { + .onSet((value) => { if (!!value === this.recordingActive) { - return; // skip delegate call if state didn't change! + return // skip delegate call if state didn't change! } - this.recordingActive = !!value; - this.delegate.updateRecordingActive(this.recordingActive); + this.recordingActive = !!value + this.delegate.updateRecordingActive(this.recordingActive) }) .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) this.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive) - .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()); + .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()) this.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive) - .on(CharacteristicEventTypes.CHANGE, change => { + .on(CharacteristicEventTypes.CHANGE, (change) => { for (const service of this.sensorServices) { - service.setCharacteristic(Characteristic.StatusActive, !!change.newValue); + service.setCharacteristic(Characteristic.StatusActive, !!change.newValue) } if (!change.newValue && this.recordingStream) { - this.recordingStream.close(HDSProtocolSpecificErrorReason.NOT_ALLOWED); + this.recordingStream.close(HDSProtocolSpecificErrorReason.NOT_ALLOWED) } - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() }) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) this.operatingModeService.getCharacteristic(Characteristic.EventSnapshotsActive) .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) this.operatingModeService.getCharacteristic(Characteristic.PeriodicSnapshotsActive) .on(CharacteristicEventTypes.CHANGE, () => this.stateChangeDelegate?.()) - .setProps({ adminOnlyAccess: [Access.WRITE] }); + .setProps({ adminOnlyAccess: [Access.WRITE] }) this.dataStreamManagement - .onRequestMessage(Protocols.DATA_SEND, Topics.OPEN, this.handleDataSendOpen.bind(this)); + .onRequestMessage(Protocols.DATA_SEND, Topics.OPEN, this.handleDataSendOpen.bind(this)) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleDataSendOpen(connection: DataStreamConnection, id: number, message: Record) { // for message fields see https://github.com/Supereg/secure-video-specification#41-start - const streamId: number = message.streamId; - const type: string = message.type; - const target: string = message.target; - const reason: string = message.reason; - - if (target !== "controller" || type !== "ipcamera.recording") { - debug("[HDS %s] Received data send with unexpected target: %s or type: %d. Rejecting...", - connection.remoteAddress, target, type); + const streamId: number = message.streamId + const type: string = message.type + const target: string = message.target + const reason: string = message.reason + + if (target !== 'controller' || type !== 'ipcamera.recording') { + debug('[HDS %s] Received data send with unexpected target: %s or type: %d. Rejecting...', connection.remoteAddress, target, type) connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, id, HDSStatus.PROTOCOL_SPECIFIC_ERROR, { status: HDSProtocolSpecificErrorReason.UNEXPECTED_FAILURE, - }); - return; + }) + return } if (!this.recordingActive) { connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, id, HDSStatus.PROTOCOL_SPECIFIC_ERROR, { status: HDSProtocolSpecificErrorReason.NOT_ALLOWED, - }); - return; + }) + return } if (!this.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value) { connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, id, HDSStatus.PROTOCOL_SPECIFIC_ERROR, { status: HDSProtocolSpecificErrorReason.NOT_ALLOWED, - }); - return; + }) + return } if (this.recordingStream) { - debug("[HDS %s] Rejecting DATA_SEND OPEN as another stream (%s) is already recording with streamId %d!", - connection.remoteAddress, this.recordingStream.connection.remoteAddress, this.recordingStream.streamId); + debug('[HDS %s] Rejecting DATA_SEND OPEN as another stream (%s) is already recording with streamId %d!', connection.remoteAddress, this.recordingStream.connection.remoteAddress, this.recordingStream.streamId) // there is already a recording stream running. connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, id, HDSStatus.PROTOCOL_SPECIFIC_ERROR, { status: HDSProtocolSpecificErrorReason.BUSY, - }); - return; + }) + return } if (!this.selectedConfiguration) { connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, id, HDSStatus.PROTOCOL_SPECIFIC_ERROR, { status: HDSProtocolSpecificErrorReason.INVALID_CONFIGURATION, - }); - return; + }) + return } - debug("[HDS %s] HDS DATA_SEND Open with reason '%s'.", connection.remoteAddress, reason); + debug('[HDS %s] HDS DATA_SEND Open with reason \'%s\'.', connection.remoteAddress, reason) - // eslint-disable-next-line @typescript-eslint/no-use-before-define - this.recordingStream = new CameraRecordingStream(connection, this.delegate, id, streamId); + // eslint-disable-next-line ts/no-use-before-define + this.recordingStream = new CameraRecordingStream(connection, this.delegate, id, streamId) + + // eslint-disable-next-line ts/no-use-before-define this.recordingStream.on(CameraRecordingStreamEvents.CLOSED, () => { - debug("[HDS %s] Removing active recoding session from recording management!", connection.remoteAddress); - this.recordingStream = undefined; - }); + debug('[HDS %s] Removing active recoding session from recording management!', connection.remoteAddress) + this.recordingStream = undefined + }) - this.recordingStream.startStreaming(); + this.recordingStream.startStreaming() } private handleSelectedCameraRecordingConfigurationRead(): string { if (!this.selectedConfiguration) { - throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - return this.selectedConfiguration.base64; + return this.selectedConfiguration.base64 } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleSelectedCameraRecordingConfigurationWrite(value: any): void { - const configuration = this.parseSelectedConfiguration(value); + const configuration = this.parseSelectedConfiguration(value) - const changed = this.selectedConfiguration?.base64 !== value; + const changed = this.selectedConfiguration?.base64 !== value this.selectedConfiguration = { parsed: configuration, base64: value, - }; + } if (changed) { - this.delegate.updateRecordingConfiguration(this.selectedConfiguration.parsed); + this.delegate.updateRecordingConfiguration(this.selectedConfiguration.parsed) // notify controller storage about updated values! - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() } } private parseSelectedConfiguration(value: string): CameraRecordingConfiguration { - const decoded = tlv.decode(Buffer.from(value, "base64")); - - const recording = tlv.decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_RECORDING_CONFIGURATION]); - const video = tlv.decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_VIDEO_CONFIGURATION]); - const audio = tlv.decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_AUDIO_CONFIGURATION]); - - const prebufferLength = recording[SupportedCameraRecordingConfigurationTypes.PREBUFFER_LENGTH].readInt32LE(0); - let eventTriggerOptions = recording[SupportedCameraRecordingConfigurationTypes.EVENT_TRIGGER_OPTIONS].readInt32LE(0); - const mediaContainerConfiguration = tlv.decode(recording[SupportedCameraRecordingConfigurationTypes.MEDIA_CONTAINER_CONFIGURATIONS]); - const containerType = mediaContainerConfiguration[MediaContainerConfigurationTypes.MEDIA_CONTAINER_TYPE][0]; - const mediaContainerParameters = tlv.decode(mediaContainerConfiguration[MediaContainerConfigurationTypes.MEDIA_CONTAINER_PARAMETERS]); - const fragmentLength = mediaContainerParameters[MediaContainerParameterTypes.FRAGMENT_LENGTH].readInt32LE(0); - - const videoCodec = video[VideoCodecConfigurationTypes.CODEC_TYPE][0]; - const videoParameters = tlv.decode(video[VideoCodecConfigurationTypes.CODEC_PARAMETERS]); - const videoAttributes = tlv.decode(video[VideoCodecConfigurationTypes.ATTRIBUTES]); - - const profile = videoParameters[VideoCodecParametersTypes.PROFILE_ID][0]; - const level = videoParameters[VideoCodecParametersTypes.LEVEL][0]; - const videoBitrate = videoParameters[VideoCodecParametersTypes.BITRATE].readInt32LE(0); - const iFrameInterval = videoParameters[VideoCodecParametersTypes.IFRAME_INTERVAL].readInt32LE(0); - - const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readInt16LE(0); - const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readInt16LE(0); - const framerate = videoAttributes[VideoAttributesTypes.FRAME_RATE][0]; - - const audioCodec = audio[AudioCodecConfigurationTypes.CODEC_TYPE][0]; - const audioParameters = tlv.decode(audio[AudioCodecConfigurationTypes.CODEC_PARAMETERS]); - - const audioChannels = audioParameters[AudioCodecParametersTypes.CHANNEL][0]; - const samplerate = audioParameters[AudioCodecParametersTypes.SAMPLE_RATE][0]; - const audioBitrateMode = audioParameters[AudioCodecParametersTypes.BIT_RATE][0]; - const audioBitrate = audioParameters[AudioCodecParametersTypes.MAX_AUDIO_BITRATE].readUInt32LE(0); - - const typedEventTriggers: EventTriggerOption[] = []; - let bit_index = 0; + const decoded = decode(Buffer.from(value, 'base64')) + + const recording = decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_RECORDING_CONFIGURATION]) + const video = decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_VIDEO_CONFIGURATION]) + const audio = decode(decoded[SelectedCameraRecordingConfigurationTypes.SELECTED_AUDIO_CONFIGURATION]) + + const prebufferLength = recording[SupportedCameraRecordingConfigurationTypes.PREBUFFER_LENGTH].readInt32LE(0) + let eventTriggerOptions = recording[SupportedCameraRecordingConfigurationTypes.EVENT_TRIGGER_OPTIONS].readInt32LE(0) + const mediaContainerConfiguration = decode(recording[SupportedCameraRecordingConfigurationTypes.MEDIA_CONTAINER_CONFIGURATIONS]) + const containerType = mediaContainerConfiguration[MediaContainerConfigurationTypes.MEDIA_CONTAINER_TYPE][0] + const mediaContainerParameters = decode(mediaContainerConfiguration[MediaContainerConfigurationTypes.MEDIA_CONTAINER_PARAMETERS]) + const fragmentLength = mediaContainerParameters[MediaContainerParameterTypes.FRAGMENT_LENGTH].readInt32LE(0) + + const videoCodec = video[VideoCodecConfigurationTypes.CODEC_TYPE][0] + const videoParameters = decode(video[VideoCodecConfigurationTypes.CODEC_PARAMETERS]) + const videoAttributes = decode(video[VideoCodecConfigurationTypes.ATTRIBUTES]) + + const profile = videoParameters[VideoCodecParametersTypes.PROFILE_ID][0] + const level = videoParameters[VideoCodecParametersTypes.LEVEL][0] + const videoBitrate = videoParameters[VideoCodecParametersTypes.BITRATE].readInt32LE(0) + const iFrameInterval = videoParameters[VideoCodecParametersTypes.IFRAME_INTERVAL].readInt32LE(0) + + const width = videoAttributes[VideoAttributesTypes.IMAGE_WIDTH].readInt16LE(0) + const height = videoAttributes[VideoAttributesTypes.IMAGE_HEIGHT].readInt16LE(0) + const framerate = videoAttributes[VideoAttributesTypes.FRAME_RATE][0] + + const audioCodec = audio[AudioCodecConfigurationTypes.CODEC_TYPE][0] + const audioParameters = decode(audio[AudioCodecConfigurationTypes.CODEC_PARAMETERS]) + + const audioChannels = audioParameters[AudioCodecParametersTypes.CHANNEL][0] + const samplerate = audioParameters[AudioCodecParametersTypes.SAMPLE_RATE][0] + const audioBitrateMode = audioParameters[AudioCodecParametersTypes.BIT_RATE][0] + const audioBitrate = audioParameters[AudioCodecParametersTypes.MAX_AUDIO_BITRATE].readUInt32LE(0) + + const typedEventTriggers: EventTriggerOption[] = [] + let bitIndex = 0 while (eventTriggerOptions > 0) { - if (eventTriggerOptions & 0x01) { // of the lowest bit is set add the next event trigger option - typedEventTriggers.push(1 << bit_index); + if (eventTriggerOptions % 2 === 1) { // check if the lowest bit is set + typedEventTriggers.push(1 << bitIndex) } - eventTriggerOptions = eventTriggerOptions >> 1; // shift to right till we reach zero. - bit_index += 1; // count our current bit index + eventTriggerOptions = Math.floor(eventTriggerOptions / 2) // shift right by dividing by 2 + bitIndex += 1 } return { - prebufferLength: prebufferLength, + prebufferLength, eventTriggerTypes: typedEventTriggers, mediaContainerConfiguration: { type: containerType, @@ -677,10 +697,10 @@ export class RecordingManagement { videoCodec: { type: videoCodec, parameters: { - profile: profile, - level: level, + profile, + level, bitRate: videoBitrate, - iFrameInterval: iFrameInterval, + iFrameInterval, }, resolution: [width, height, framerate], }, @@ -691,121 +711,142 @@ export class RecordingManagement { bitrateMode: audioBitrateMode, bitrate: audioBitrate, }, - }; + } } private _supportedCameraRecordingConfiguration(options: CameraRecordingOptions): string { const mediaContainers = Array.isArray(options.mediaContainerConfiguration) ? options.mediaContainerConfiguration - : [options.mediaContainerConfiguration]; - - const prebufferLength = Buffer.alloc(4); - const eventTriggerOptions = Buffer.alloc(8); - - prebufferLength.writeInt32LE(options.prebufferLength, 0); - eventTriggerOptions.writeInt32LE(this.eventTriggerOptions, 0); - - return tlv.encode( - SupportedCameraRecordingConfigurationTypes.PREBUFFER_LENGTH, prebufferLength, - SupportedCameraRecordingConfigurationTypes.EVENT_TRIGGER_OPTIONS, eventTriggerOptions, - SupportedCameraRecordingConfigurationTypes.MEDIA_CONTAINER_CONFIGURATIONS, mediaContainers.map(config => { - const fragmentLength = Buffer.alloc(4); - - fragmentLength.writeInt32LE(config.fragmentLength, 0); - - return tlv.encode( - MediaContainerConfigurationTypes.MEDIA_CONTAINER_TYPE, config.type, - MediaContainerConfigurationTypes.MEDIA_CONTAINER_PARAMETERS, tlv.encode( - MediaContainerParameterTypes.FRAGMENT_LENGTH, fragmentLength, + : [options.mediaContainerConfiguration] + + const prebufferLength = Buffer.alloc(4) + const eventTriggerOptions = Buffer.alloc(8) + + prebufferLength.writeInt32LE(options.prebufferLength, 0) + eventTriggerOptions.writeInt32LE(this.eventTriggerOptions, 0) + + return encode( + SupportedCameraRecordingConfigurationTypes.PREBUFFER_LENGTH, + prebufferLength, + SupportedCameraRecordingConfigurationTypes.EVENT_TRIGGER_OPTIONS, + eventTriggerOptions, + SupportedCameraRecordingConfigurationTypes.MEDIA_CONTAINER_CONFIGURATIONS, + mediaContainers.map((config) => { + const fragmentLength = Buffer.alloc(4) + + fragmentLength.writeInt32LE(config.fragmentLength, 0) + + return encode( + MediaContainerConfigurationTypes.MEDIA_CONTAINER_TYPE, + config.type, + MediaContainerConfigurationTypes.MEDIA_CONTAINER_PARAMETERS, + encode( + MediaContainerParameterTypes.FRAGMENT_LENGTH, + fragmentLength, ), - ); + ) }), - ).toString("base64"); + ).toString('base64') } private _supportedVideoRecordingConfiguration(videoOptions: VideoRecordingOptions): string { if (!videoOptions.parameters) { - throw new Error("Video parameters cannot be undefined"); + throw new Error('Video parameters cannot be undefined') } if (!videoOptions.resolutions) { - throw new Error("Video resolutions cannot be undefined"); + throw new Error('Video resolutions cannot be undefined') } - const codecParameters = tlv.encode( - VideoCodecParametersTypes.PROFILE_ID, videoOptions.parameters.profiles, - VideoCodecParametersTypes.LEVEL, videoOptions.parameters.levels, - ); - - const videoStreamConfiguration = tlv.encode( - VideoCodecConfigurationTypes.CODEC_TYPE, videoOptions.type, - VideoCodecConfigurationTypes.CODEC_PARAMETERS, codecParameters, - VideoCodecConfigurationTypes.ATTRIBUTES, videoOptions.resolutions.map(resolution => { + const codecParameters = encode( + VideoCodecParametersTypes.PROFILE_ID, + videoOptions.parameters.profiles, + VideoCodecParametersTypes.LEVEL, + videoOptions.parameters.levels, + ) + + const videoStreamConfiguration = encode( + VideoCodecConfigurationTypes.CODEC_TYPE, + videoOptions.type, + VideoCodecConfigurationTypes.CODEC_PARAMETERS, + codecParameters, + VideoCodecConfigurationTypes.ATTRIBUTES, + videoOptions.resolutions.map((resolution) => { if (resolution.length !== 3) { - throw new Error("Unexpected video resolution"); + throw new Error('Unexpected video resolution') } - const width = Buffer.alloc(2); - const height = Buffer.alloc(2); - const frameRate = Buffer.alloc(1); - - width.writeUInt16LE(resolution[0], 0); - height.writeUInt16LE(resolution[1], 0); - frameRate.writeUInt8(resolution[2], 0); - - return tlv.encode( - VideoAttributesTypes.IMAGE_WIDTH, width, - VideoAttributesTypes.IMAGE_HEIGHT, height, - VideoAttributesTypes.FRAME_RATE, frameRate, - ); + const width = Buffer.alloc(2) + const height = Buffer.alloc(2) + const frameRate = Buffer.alloc(1) + + width.writeUInt16LE(resolution[0], 0) + height.writeUInt16LE(resolution[1], 0) + frameRate.writeUInt8(resolution[2], 0) + + return encode( + VideoAttributesTypes.IMAGE_WIDTH, + width, + VideoAttributesTypes.IMAGE_HEIGHT, + height, + VideoAttributesTypes.FRAME_RATE, + frameRate, + ) }), - ); + ) - return tlv.encode( - SupportedVideoRecordingConfigurationTypes.VIDEO_CODEC_CONFIGURATION, videoStreamConfiguration, - ).toString("base64"); + return encode( + SupportedVideoRecordingConfigurationTypes.VIDEO_CODEC_CONFIGURATION, + videoStreamConfiguration, + ).toString('base64') } private _supportedAudioStreamConfiguration(audioOptions: AudioRecordingOptions): string { const audioCodecs = Array.isArray(audioOptions.codecs) ? audioOptions.codecs - : [audioOptions.codecs]; + : [audioOptions.codecs] if (audioCodecs.length === 0) { - throw Error("CameraRecordingOptions.audio: At least one audio codec configuration must be specified!"); + throw new Error('CameraRecordingOptions.audio: At least one audio codec configuration must be specified!') } - const codecConfigurations: Buffer[] = audioCodecs.map(codec => { + const codecConfigurations: Buffer[] = audioCodecs.map((codec) => { const providedSamplerates = Array.isArray(codec.samplerate) ? codec.samplerate - : [codec.samplerate]; + : [codec.samplerate] if (providedSamplerates.length === 0) { - throw new Error("CameraRecordingOptions.audio.codecs: Audio samplerate cannot be empty!"); + throw new Error('CameraRecordingOptions.audio.codecs: Audio samplerate cannot be empty!') } - const audioParameters = tlv.encode( - AudioCodecParametersTypes.CHANNEL, Math.max(1, codec.audioChannels || 1), - AudioCodecParametersTypes.BIT_RATE, codec.bitrateMode || AudioBitrate.VARIABLE, - AudioCodecParametersTypes.SAMPLE_RATE, providedSamplerates, - ); - - return tlv.encode( - AudioCodecConfigurationTypes.CODEC_TYPE, codec.type, - AudioCodecConfigurationTypes.CODEC_PARAMETERS, audioParameters, - ); - }); - - return tlv.encode( - SupportedAudioRecordingConfigurationTypes.AUDIO_CODEC_CONFIGURATION, codecConfigurations, - ).toString("base64"); + const audioParameters = encode( + AudioCodecParametersTypes.CHANNEL, + Math.max(1, codec.audioChannels || 1), + AudioCodecParametersTypes.BIT_RATE, + codec.bitrateMode || AudioBitrate.VARIABLE, + AudioCodecParametersTypes.SAMPLE_RATE, + providedSamplerates, + ) + + return encode( + AudioCodecConfigurationTypes.CODEC_TYPE, + codec.type, + AudioCodecConfigurationTypes.CODEC_PARAMETERS, + audioParameters, + ) + }) + + return encode( + SupportedAudioRecordingConfigurationTypes.AUDIO_CODEC_CONFIGURATION, + codecConfigurations, + ).toString('base64') } - private computeConfigurationHash(algorithm = "sha256"): string { - const configurationHash = crypto.createHash(algorithm); - configurationHash.update(this.supportedCameraRecordingConfiguration); - configurationHash.update(this.supportedVideoRecordingConfiguration); - configurationHash.update(this.supportedAudioRecordingConfiguration); - return configurationHash.digest().toString("hex"); + private computeConfigurationHash(algorithm = 'sha256'): string { + const configurationHash = createHash(algorithm) + configurationHash.update(this.supportedCameraRecordingConfiguration) + configurationHash.update(this.supportedVideoRecordingConfiguration) + configurationHash.update(this.supportedAudioRecordingConfiguration) + return configurationHash.digest().toString('hex') } /** @@ -814,8 +855,8 @@ export class RecordingManagement { serialize(): RecordingManagementState | undefined { return { configurationHash: { - algorithm: "sha256", - hash: this.computeConfigurationHash("sha256"), + algorithm: 'sha256', + hash: this.computeConfigurationHash('sha256'), }, selectedConfiguration: this.selectedConfiguration?.base64, @@ -825,53 +866,53 @@ export class RecordingManagement { eventSnapshotsActive: !!this.operatingModeService.getCharacteristic(Characteristic.EventSnapshotsActive).value, homeKitCameraActive: !!this.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value, periodicSnapshotsActive: !!this.operatingModeService.getCharacteristic(Characteristic.PeriodicSnapshotsActive).value, - }; + } } /** * @private */ deserialize(serialized: RecordingManagementState): void { - let changedState = false; + let changedState = false // we only restore the `selectedConfiguration` if our supported configuration hasn't changed. - const currentConfigurationHash = this.computeConfigurationHash(serialized.configurationHash.algorithm); + const currentConfigurationHash = this.computeConfigurationHash(serialized.configurationHash.algorithm) if (serialized.selectedConfiguration) { if (currentConfigurationHash === serialized.configurationHash.hash) { this.selectedConfiguration = { base64: serialized.selectedConfiguration, parsed: this.parseSelectedConfiguration(serialized.selectedConfiguration), - }; + } } else { - changedState = true; + changedState = true } } - this.recordingActive = serialized.recordingActive; - this.recordingManagementService.updateCharacteristic(Characteristic.Active, serialized.recordingActive); - this.recordingManagementService.updateCharacteristic(Characteristic.RecordingAudioActive, serialized.recordingAudioActive); + this.recordingActive = serialized.recordingActive + this.recordingManagementService.updateCharacteristic(Characteristic.Active, serialized.recordingActive) + this.recordingManagementService.updateCharacteristic(Characteristic.RecordingAudioActive, serialized.recordingAudioActive) - this.operatingModeService.updateCharacteristic(Characteristic.EventSnapshotsActive, serialized.eventSnapshotsActive); - this.operatingModeService.updateCharacteristic(Characteristic.PeriodicSnapshotsActive, serialized.periodicSnapshotsActive); + this.operatingModeService.updateCharacteristic(Characteristic.EventSnapshotsActive, serialized.eventSnapshotsActive) + this.operatingModeService.updateCharacteristic(Characteristic.PeriodicSnapshotsActive, serialized.periodicSnapshotsActive) - this.operatingModeService.updateCharacteristic(Characteristic.HomeKitCameraActive, serialized.homeKitCameraActive); + this.operatingModeService.updateCharacteristic(Characteristic.HomeKitCameraActive, serialized.homeKitCameraActive) for (const service of this.sensorServices) { - service.setCharacteristic(Characteristic.StatusActive, serialized.homeKitCameraActive); + service.setCharacteristic(Characteristic.StatusActive, serialized.homeKitCameraActive) } try { if (this.selectedConfiguration) { - this.delegate.updateRecordingConfiguration(this.selectedConfiguration.parsed); + this.delegate.updateRecordingConfiguration(this.selectedConfiguration.parsed) } if (serialized.recordingActive) { - this.delegate.updateRecordingActive(serialized.recordingActive); + this.delegate.updateRecordingActive(serialized.recordingActive) } } catch (error) { - console.error("Failed to properly initialize CameraRecordingDelegate from persistent storage: " + error.stack); + console.error(`Failed to properly initialize CameraRecordingDelegate from persistent storage: ${error.stack}`) } if (changedState) { - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() } } @@ -879,57 +920,58 @@ export class RecordingManagement { * @private */ setupStateChangeDelegate(delegate?: StateChangeDelegate): void { - this.stateChangeDelegate = delegate; + this.stateChangeDelegate = delegate } destroy(): void { - this.dataStreamManagement.destroy(); + this.dataStreamManagement.destroy() } handleFactoryReset(): void { - this.selectedConfiguration = undefined; - this.recordingManagementService.updateCharacteristic(Characteristic.Active, false); - this.recordingManagementService.updateCharacteristic(Characteristic.RecordingAudioActive, false); + this.selectedConfiguration = undefined + this.recordingManagementService.updateCharacteristic(Characteristic.Active, false) + this.recordingManagementService.updateCharacteristic(Characteristic.RecordingAudioActive, false) - this.operatingModeService.updateCharacteristic(Characteristic.EventSnapshotsActive, true); - this.operatingModeService.updateCharacteristic(Characteristic.PeriodicSnapshotsActive, true); + this.operatingModeService.updateCharacteristic(Characteristic.EventSnapshotsActive, true) + this.operatingModeService.updateCharacteristic(Characteristic.PeriodicSnapshotsActive, true) - this.operatingModeService.updateCharacteristic(Characteristic.HomeKitCameraActive, true); + this.operatingModeService.updateCharacteristic(Characteristic.HomeKitCameraActive, true) for (const service of this.sensorServices) { - service.setCharacteristic(Characteristic.StatusActive, true); + service.setCharacteristic(Characteristic.StatusActive, true) } try { // notifying the delegate about the updated state - this.delegate.updateRecordingActive(false); - this.delegate.updateRecordingConfiguration(undefined); + this.delegate.updateRecordingActive(false) + this.delegate.updateRecordingConfiguration(undefined) } catch (error) { - console.error("CameraRecordingDelegate failed to update state after handleFactoryReset: " + error.stack); + console.error(`CameraRecordingDelegate failed to update state after handleFactoryReset: ${error.stack}`) } } } - /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax const enum CameraRecordingStreamEvents { /** * This event is fired when the recording stream is closed. * Either due to a normal exit (e.g. the HomeKit Controller acknowledging the stream) * or due to an erroneous exit (e.g. HDS connection getting closed). */ - CLOSED = "closed", + CLOSED = 'closed', } /** * @group Camera */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging declare interface CameraRecordingStream { - on(event: "closed", listener: () => void): this; - - emit(event: "closed"): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'closed', listener: () => void): this + emit(event: 'closed'): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -938,234 +980,231 @@ declare interface CameraRecordingStream { * * @group Camera */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging class CameraRecordingStream extends EventEmitter implements DataStreamProtocolHandler { - readonly connection: DataStreamConnection; - readonly delegate: CameraRecordingDelegate; - readonly hdsRequestId: number; - readonly streamId: number; - private closed = false; + readonly connection: DataStreamConnection + readonly delegate: CameraRecordingDelegate + readonly hdsRequestId: number + readonly streamId: number + private closed = false eventHandler?: Record = { [Topics.CLOSE]: this.handleDataSendClose.bind(this), [Topics.ACK]: this.handleDataSendAck.bind(this), - }; - requestHandler?: Record = undefined; + } + + requestHandler?: Record = undefined - private readonly closeListener: () => void; + private readonly closeListener: () => void - private generator?: AsyncGenerator; + private generator?: AsyncGenerator /** * This timeout is used to detect non-returning generators. * When we signal the delegate that it is being closed its generator must return withing 10s. */ - private generatorTimeout?: NodeJS.Timeout; + private generatorTimeout?: NodeJS.Timeout /** * This timer is used to check if the stream is properly closed when we expect it to do so. * When we expect a close signal from the remote, we wait 12s for it. Otherwise, we abort and close it ourselves. * This ensures memory is freed, and that we recover fast from erroneous states. */ - private closingTimeout?: NodeJS.Timeout; + private closingTimeout?: NodeJS.Timeout constructor(connection: DataStreamConnection, delegate: CameraRecordingDelegate, requestId: number, streamId: number) { - super(); - this.connection = connection; - this.delegate = delegate; - this.hdsRequestId = requestId; - this.streamId = streamId; - - this.connection.on(DataStreamConnectionEvent.CLOSED, this.closeListener = this.handleDataStreamConnectionClosed.bind(this)); - this.connection.addProtocolHandler(Protocols.DATA_SEND, this); + super() + this.connection = connection + this.delegate = delegate + this.hdsRequestId = requestId + this.streamId = streamId + + this.connection.on(DataStreamConnectionEvent.CLOSED, this.closeListener = this.handleDataStreamConnectionClosed.bind(this)) + this.connection.addProtocolHandler(Protocols.DATA_SEND, this) } startStreaming() { - // noinspection JSIgnoredPromiseFromCall - this._startStreaming(); + this._startStreaming() } private async _startStreaming() { - debug("[HDS %s] Sending DATA_SEND OPEN response for streamId %d", this.connection.remoteAddress, this.streamId); + debug('[HDS %s] Sending DATA_SEND OPEN response for streamId %d', this.connection.remoteAddress, this.streamId) this.connection.sendResponse(Protocols.DATA_SEND, Topics.OPEN, this.hdsRequestId, HDSStatus.SUCCESS, { status: HDSStatus.SUCCESS, - }); + }) // 256 KiB (1KiB to 900 KiB) - const maxChunk = 0x40000; + const maxChunk = 0x40000 // The first buffer which we receive from the generator is always the `mediaInitialization` packet (mp4 `moov` box). - let initialization = true; - let dataSequenceNumber = 1; + let initialization = true + let dataSequenceNumber = 1 // tracks if the last received RecordingPacket was yielded with `isLast=true`. - let lastFragmentWasMarkedLast = false; + let lastFragmentWasMarkedLast = false try { - this.generator = this.delegate.handleRecordingStreamRequest(this.streamId); + this.generator = this.delegate.handleRecordingStreamRequest(this.streamId) for await (const packet of this.generator) { if (this.closed) { - console.error(`[HDS ${this.connection.remoteAddress}] Delegate yielded fragment after stream ${this.streamId} was already closed!`); - break; + console.error(`[HDS ${this.connection.remoteAddress}] Delegate yielded fragment after stream ${this.streamId} was already closed!`) + break } if (lastFragmentWasMarkedLast) { - console.error(`[HDS ${this.connection.remoteAddress}] Delegate yielded fragment for stream ${this.streamId} after already signaling end of stream!`); - break; + console.error(`[HDS ${this.connection.remoteAddress}] Delegate yielded fragment for stream ${this.streamId} after already signaling end of stream!`) + break } - const fragment = packet.data; + const fragment = packet.data - let offset = 0; - let dataChunkSequenceNumber = 1; + let offset = 0 + let dataChunkSequenceNumber = 1 while (offset < fragment.length) { if (this.closed) { - break; + break } - const data = fragment.slice(offset, offset + maxChunk); - offset += data.length; + const data = fragment.subarray(offset, offset + maxChunk) + offset += data.length // see https://github.com/Supereg/secure-video-specification#42-binary-data const event: DataSendDataEvent = { streamId: this.streamId, packets: [{ - data: data, + data, metadata: { dataType: initialization ? PacketDataType.MEDIA_INITIALIZATION : PacketDataType.MEDIA_FRAGMENT, - dataSequenceNumber: dataSequenceNumber, - dataChunkSequenceNumber: dataChunkSequenceNumber, + dataSequenceNumber, + dataChunkSequenceNumber, isLastDataChunk: offset >= fragment.length, dataTotalSize: dataChunkSequenceNumber === 1 ? fragment.length : undefined, }, }], endOfStream: offset >= fragment.length ? Boolean(packet.isLast).valueOf() : undefined, - }; + } - debug("[HDS %s] Sending DATA_SEND DATA for stream %d with metadata: %o and length %d; EoS: %s", - this.connection.remoteAddress, this.streamId, event.packets[0].metadata, data.length, event.endOfStream); - this.connection.sendEvent(Protocols.DATA_SEND, Topics.DATA, event); + debug('[HDS %s] Sending DATA_SEND DATA for stream %d with metadata: %o and length %d; EoS: %s', this.connection.remoteAddress, this.streamId, event.packets[0].metadata, data.length, event.endOfStream) + this.connection.sendEvent(Protocols.DATA_SEND, Topics.DATA, event) - dataChunkSequenceNumber++; - initialization = false; + dataChunkSequenceNumber++ + initialization = false } - lastFragmentWasMarkedLast = packet.isLast; + lastFragmentWasMarkedLast = packet.isLast if (packet.isLast) { - break; + break } - dataSequenceNumber++; + dataSequenceNumber++ } if (!lastFragmentWasMarkedLast && !this.closed) { // Delegate violates the contract. Exited normally on a non-closed stream without properly setting `isLast`. - console.warn(`[HDS ${this.connection.remoteAddress}] Delegate finished streaming for ${this.streamId} without setting RecordingPacket.isLast. ` + - "Can't notify Controller about endOfStream!"); + console.warn(`[HDS ${this.connection.remoteAddress}] Delegate finished streaming for ${this.streamId} without setting RecordingPacket.isLast. ` + + 'Can\'t notify Controller about endOfStream!') } } catch (error) { if (this.closed) { - console.warn(`[HDS ${this.connection.remoteAddress}] Encountered unexpected error on already closed recording stream ${this.streamId}: ${error.stack}`); + console.warn(`[HDS ${this.connection.remoteAddress}] Encountered unexpected error on already closed recording stream ${this.streamId}: ${error.stack}`) } else { - let closeReason = HDSProtocolSpecificErrorReason.UNEXPECTED_FAILURE; + let closeReason = HDSProtocolSpecificErrorReason.UNEXPECTED_FAILURE if (error instanceof HDSProtocolError) { - closeReason = error.reason; - debug("[HDS %s] Delegate signaled to close the recording stream %d.", this.connection.remoteAddress, this.streamId); + closeReason = error.reason + debug('[HDS %s] Delegate signaled to close the recording stream %d.', this.connection.remoteAddress, this.streamId) } else if (error instanceof HDSConnectionError && error.type === HDSConnectionErrorType.CLOSED_SOCKET) { // we are probably on a shutdown or just late. Connection is dead. End the stream! - debug("[HDS %s] Exited recording stream due to closed HDS socket: stream id %d.", this.connection.remoteAddress, this.streamId); - return; // execute finally and then exit (we want to skip the `sendEvent` below) + debug('[HDS %s] Exited recording stream due to closed HDS socket: stream id %d.', this.connection.remoteAddress, this.streamId) + return // execute finally and then exit (we want to skip the `sendEvent` below) } else { - console.error(`[HDS ${this.connection.remoteAddress}] Encountered unexpected error for recording stream ${this.streamId}: ${error.stack}`); + console.error(`[HDS ${this.connection.remoteAddress}] Encountered unexpected error for recording stream ${this.streamId}: ${error.stack}`) } // call close to go through standard close routine! - this.close(closeReason); + this.close(closeReason) } - return; + return } finally { - this.generator = undefined; + this.generator = undefined if (this.generatorTimeout) { - clearTimeout(this.generatorTimeout); + clearTimeout(this.generatorTimeout) } if (!this.closed) { // e.g. when returning with `endOfStream` we rely on the HomeHub to send an ACK event to close the recording. // With this timer we ensure that the HomeHub has the chance to close the stream gracefully but at the same time // ensure that if something fails the recording stream is freed nonetheless. - this.kickOffCloseTimeout(); + this.kickOffCloseTimeout() } } - debug("[HDS %s] Finished DATA_SEND transmission for stream %d!", this.connection.remoteAddress, this.streamId); + debug('[HDS %s] Finished DATA_SEND transmission for stream %d!', this.connection.remoteAddress, this.streamId) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleDataSendAck(message: Record) { - const streamId: string = message.streamId; - const endOfStream: boolean = message.endOfStream; + const streamId: string = message.streamId + const endOfStream: boolean = message.endOfStream // The HomeKit Controller will send a DATA_SEND ACK if we set the `endOfStream` flag in the last packet // of our DATA_SEND DATA packet. // To my testing the session is then considered complete and the HomeKit controller will close the HDS Connection after 5 seconds. - debug("[HDS %s] Received DATA_SEND ACK packet for streamId %s. Acknowledged %s.", this.connection.remoteAddress, streamId, endOfStream); + debug('[HDS %s] Received DATA_SEND ACK packet for streamId %s. Acknowledged %s.', this.connection.remoteAddress, streamId, endOfStream) - this.handleClosed(() => this.delegate.acknowledgeStream?.(this.streamId)); + this.handleClosed(() => this.delegate.acknowledgeStream?.(this.streamId)) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleDataSendClose(message: Record) { // see https://github.com/Supereg/secure-video-specification#43-close - const streamId: number = message.streamId; - const reason: HDSProtocolSpecificErrorReason = message.reason; + const streamId: number = message.streamId + const reason: HDSProtocolSpecificErrorReason = message.reason if (streamId !== this.streamId) { - return; + return } - debug("[HDS %s] Received DATA_SEND CLOSE for streamId %d with reason %s", + debug('[HDS %s] Received DATA_SEND CLOSE for streamId %d with reason %s', // @ts-expect-error: forceConsistentCasingInFileNames compiler option - this.connection.remoteAddress, streamId, HDSProtocolSpecificErrorReason[reason]); + this.connection.remoteAddress, streamId, HDSProtocolSpecificErrorReason[reason]) - this.handleClosed(() => this.delegate.closeRecordingStream(streamId, reason)); + this.handleClosed(() => this.delegate.closeRecordingStream(streamId, reason)) } private handleDataStreamConnectionClosed() { - debug("[HDS %s] The HDS connection of the stream %d closed.", this.connection.remoteAddress, this.streamId); + debug('[HDS %s] The HDS connection of the stream %d closed.', this.connection.remoteAddress, this.streamId) - this.handleClosed(() => this.delegate.closeRecordingStream(this.streamId, undefined)); + this.handleClosed(() => this.delegate.closeRecordingStream(this.streamId, undefined)) } private handleClosed(closure: () => void): void { - this.closed = true; + this.closed = true if (this.closingTimeout) { - clearTimeout(this.closingTimeout); - this.closingTimeout = undefined; + clearTimeout(this.closingTimeout) + this.closingTimeout = undefined } - this.connection.removeProtocolHandler(Protocols.DATA_SEND, this); - this.connection.removeListener(DataStreamConnectionEvent.CLOSED, this.closeListener); + this.connection.removeProtocolHandler(Protocols.DATA_SEND, this) + this.connection.removeListener(DataStreamConnectionEvent.CLOSED, this.closeListener) if (this.generator) { // when this variable is defined, the generator hasn't returned yet. // we start a timeout to uncover potential programming mistakes where we await forever and can't free resources. this.generatorTimeout = setTimeout(() => { - console.error("[HDS %s] Recording download stream %d is still awaiting generator although stream was closed 10s ago! " + - "This is a programming mistake by the camera implementation which prevents freeing up resources.", this.connection.remoteAddress, this.streamId); - }, 10000); + console.error('[HDS %s] Recording download stream %d is still awaiting generator although stream was closed 10s ago! ' + + 'This is a programming mistake by the camera implementation which prevents freeing up resources.', this.connection.remoteAddress, this.streamId) + }, 10000) } try { - closure(); + closure() } catch (error) { - console.error(`[HDS ${this.connection.remoteAddress}] CameraRecordingDelegated failed to handle closing the stream ${this.streamId}: ${error.stack}`); + console.error(`[HDS ${this.connection.remoteAddress}] CameraRecordingDelegated failed to handle closing the stream ${this.streamId}: ${error.stack}`) } - this.emit(CameraRecordingStreamEvents.CLOSED); + this.emit(CameraRecordingStreamEvents.CLOSED) } /** @@ -1174,36 +1213,36 @@ class CameraRecordingStream extends EventEmitter implements DataStreamProtocolHa */ close(reason: HDSProtocolSpecificErrorReason): void { if (this.closed) { - return; + return } - debug("[HDS %s] Recording stream %d was closed manually with reason %s.", + debug('[HDS %s] Recording stream %d was closed manually with reason %s.', // @ts-expect-error: forceConsistentCasingInFileNames compiler option - this.connection.remoteAddress, this.streamId, reason ? HDSProtocolSpecificErrorReason[reason] : "CLOSED"); + this.connection.remoteAddress, this.streamId, reason ? HDSProtocolSpecificErrorReason[reason] : 'CLOSED') // the `isConsideredClosed` check just ensures that the won't ever throw here and that `handledClosed` is always executed. if (!this.connection.isConsideredClosed()) { this.connection.sendEvent(Protocols.DATA_SEND, Topics.CLOSE, { streamId: this.streamId, - reason: reason, - }); + reason, + }) } - this.handleClosed(() => this.delegate.closeRecordingStream(this.streamId, reason)); + this.handleClosed(() => this.delegate.closeRecordingStream(this.streamId, reason)) } private kickOffCloseTimeout(): void { if (this.closingTimeout) { - clearTimeout(this.closingTimeout); + clearTimeout(this.closingTimeout) } this.closingTimeout = setTimeout(() => { if (this.closed) { - return; + return } - debug("[HDS %s] Recording stream %d took longer than expected to fully close. Force closing now!", this.connection.remoteAddress, this.streamId); - this.close(HDSProtocolSpecificErrorReason.CANCELLED); - }, 12000); + debug('[HDS %s] Recording stream %d took longer than expected to fully close. Force closing now!', this.connection.remoteAddress, this.streamId) + this.close(HDSProtocolSpecificErrorReason.CANCELLED) + }, 12000) } } diff --git a/src/lib/camera/index.ts b/src/lib/camera/index.ts index 341a0ce87..320a11f5b 100644 --- a/src/lib/camera/index.ts +++ b/src/lib/camera/index.ts @@ -1,3 +1,3 @@ -export * from "./RTPProxy"; -export * from "./RTPStreamManagement"; -export * from "./RecordingManagement"; +export * from './RecordingManagement.js' +export * from './RTPProxy.js' +export * from './RTPStreamManagement.js' diff --git a/src/lib/controller/AdaptiveLightingController.ts b/src/lib/controller/AdaptiveLightingController.ts index a9354ee9f..0227139c0 100644 --- a/src/lib/controller/AdaptiveLightingController.ts +++ b/src/lib/controller/AdaptiveLightingController.ts @@ -1,20 +1,7 @@ -import assert from "assert"; -import { HAPStatus } from "../HAPServer"; -import { ColorUtils } from "../util/color-utils"; -import { HapStatusError } from "../util/hapStatusError"; -import { epochMillisFromMillisSince2001_01_01Buffer } from "../util/time"; -import * as uuid from "../util/uuid"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { CharacteristicValue } from "../../types"; -import { - ChangeReason, - Characteristic, - CharacteristicChange, - CharacteristicEventTypes, - CharacteristicOperationContext, -} from "../Characteristic"; -import { +/* global NodeJS */ +import type { CharacteristicValue } from '../../types' +import type { CharacteristicChange, CharacteristicOperationContext } from '../Characteristic' +import type { Brightness, CharacteristicValueActiveTransitionCount, CharacteristicValueTransitionControl, @@ -23,48 +10,74 @@ import { Lightbulb, Saturation, SupportedCharacteristicValueTransitionConfiguration, -} from "../definitions"; -import * as tlv from "../util/tlv"; -import { +} from '../definitions' +import type { ControllerIdentifier, ControllerServiceMap, - DefaultControllerType, SerializableController, StateChangeDelegate, -} from "./Controller"; +} from './Controller' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { EventEmitter } from 'node:events' -const debug = createDebug("HAP-NodeJS:Controller:TransitionControl"); +import createDebug from 'debug' +import { ChangeReason, Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { HAPStatus } from '../HAPServer.js' +import { ColorUtils } from '../util/color-utils.js' +import { HapStatusError } from '../util/hapStatusError.js' +import { epochMillisFromMillisSince2001_01_01Buffer } from '../util/time.js' +import { + decode, + decodeWithLists, + encode, + readVariableUIntLE, + writeFloat32LE, + writeUInt32, + writeVariableUIntLE, +} from '../util/tlv.js' +import { unparse, write } from '../util/uuid.js' +import { DefaultControllerType } from './Controller.js' + +const debug = createDebug('HAP-NodeJS:Controller:TransitionControl') + +// eslint-disable-next-line no-restricted-syntax const enum SupportedCharacteristicValueTransitionConfigurationsTypes { SUPPORTED_TRANSITION_CONFIGURATION = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum SupportedValueTransitionConfigurationTypes { CHARACTERISTIC_IID = 0x01, TRANSITION_TYPE = 0x02, // assumption } +// eslint-disable-next-line no-restricted-syntax const enum TransitionType { BRIGHTNESS = 0x01, // uncertain COLOR_TEMPERATURE = 0x02, } - +// eslint-disable-next-line no-restricted-syntax const enum TransitionControlTypes { READ_CURRENT_VALUE_TRANSITION_CONFIGURATION = 0x01, // could probably a list of ValueTransitionConfigurationTypes UPDATE_VALUE_TRANSITION_CONFIGURATION = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum ReadValueTransitionConfiguration { CHARACTERISTIC_IID = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum UpdateValueTransitionConfigurationsTypes { VALUE_TRANSITION_CONFIGURATION = 0x01, // this type could be a tlv8 list } +// eslint-disable-next-line no-restricted-syntax const enum ValueTransitionConfigurationTypes { - // noinspection JSUnusedGlobalSymbols CHARACTERISTIC_IID = 0x01, // 1 byte TRANSITION_PARAMETERS = 0x02, UNKNOWN_3 = 0x03, // sent with value = 1 (1 byte) @@ -75,18 +88,21 @@ const enum ValueTransitionConfigurationTypes { NOTIFY_INTERVAL_THRESHOLD = 0x08, // 32 bit uint } +// eslint-disable-next-line no-restricted-syntax const enum ValueTransitionParametersTypes { TRANSITION_ID = 0x01, // 16 bytes START_TIME = 0x02, // 8 bytes the start time for the provided schedule, millis since 2001/01/01 00:00:000 UNKNOWN_3 = 0x03, // 8 bytes, id or something (same for multiple writes) } +// eslint-disable-next-line no-restricted-syntax const enum TransitionCurveConfigurationTypes { TRANSITION_ENTRY = 0x01, ADJUSTMENT_CHARACTERISTIC_IID = 0x02, ADJUSTMENT_MULTIPLIER_RANGE = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum TransitionEntryTypes { ADJUSTMENT_FACTOR = 0x01, VALUE = 0x02, @@ -94,15 +110,18 @@ const enum TransitionEntryTypes { DURATION = 0x04, // optional, default 0, sets how long the previous value will stay the same (non interpolation time section) } +// eslint-disable-next-line no-restricted-syntax const enum TransitionAdjustmentMultiplierRange { MINIMUM_ADJUSTMENT_MULTIPLIER = 0x01, // brightness 10 MAXIMUM_ADJUSTMENT_MULTIPLIER = 0x02, // brightness 100 } +// eslint-disable-next-line no-restricted-syntax const enum ValueTransitionConfigurationResponseTypes { // read format for control point VALUE_CONFIGURATION_STATUS = 0x01, } +// eslint-disable-next-line no-restricted-syntax const enum ValueTransitionConfigurationStatusTypes { CHARACTERISTIC_IID = 0x01, TRANSITION_PARAMETERS = 0x02, @@ -110,17 +129,16 @@ const enum ValueTransitionConfigurationStatusTypes { } interface AdaptiveLightingCharacteristicContext extends CharacteristicOperationContext { - controller: AdaptiveLightingController; + controller: AdaptiveLightingController } -// eslint-disable-next-line @typescript-eslint/no-explicit-any function isAdaptiveLightingContext(context: any): context is AdaptiveLightingCharacteristicContext { - return context && "controller" in context; + return context && 'controller' in context } interface SavedLastTransitionPointInfo { - curveIndex: number; - lowerBoundTimeOffset: number; + curveIndex: number + lowerBoundTimeOffset: number } /** @@ -130,56 +148,56 @@ export interface ActiveAdaptiveLightingTransition { /** * The instance id for the characteristic for which this transition applies to (aka the ColorTemperature characteristic). */ - iid: number; + iid: number /** * Start of the transition in epoch time millis (as sent from the HomeKit controller). - * Additionally see {@link timeMillisOffset}. + * Additionally, see {@link timeMillisOffset}. */ - transitionStartMillis: number; + transitionStartMillis: number /** * It is not necessarily given, that we have the same time (or rather the correct time) as the HomeKit controller * who set up the transition schedule. - * Thus we record the delta between our current time and the the time sent with the setup request. + * Thus, we record the delta between our current time and the time sent with the setup request. * timeMillisOffset is defined as Date.now() - transitionStartMillis;. * So in the case were we actually have a correct local time, it most likely will be positive (due to network latency). * But of course it can also be negative. */ - timeMillisOffset: number; + timeMillisOffset: number /** * Value is the same for ALL control write requests I have seen (even on other homes). * @private */ - transitionId: string; + transitionId: string /** * Start of transition in milliseconds from 2001-01-01 00:00:00; unsigned 64 bit LE integer - * @private as it is a 64 bit integer, we just store the buffer to not have the struggle to encode/decode 64 bit int in JavaScript + * @private */ - transitionStartBuffer: string; + transitionStartBuffer: string /** * Hex string of 8 bytes. Some kind of id (?). Sometimes it isn't supplied. Don't know the use for that. * @private */ - id3?: string; + id3?: string - transitionCurve: AdaptiveLightingTransitionCurveEntry[]; + transitionCurve: AdaptiveLightingTransitionCurveEntry[] - brightnessCharacteristicIID: number; - brightnessAdjustmentRange: BrightnessAdjustmentMultiplierRange; + brightnessCharacteristicIID: number + brightnessAdjustmentRange: BrightnessAdjustmentMultiplierRange /** * Interval in milliseconds specifies how often the accessory should update the color temperature (internally). - * Typically this is 60000 aka 60 seconds aka 1 minute. + * Typically, this is 60000 aka 60 seconds aka 1 minute. * Note {@link notifyIntervalThreshold} */ - updateInterval: number, + updateInterval: number /** * Defines the interval in milliseconds on how often the accessory may send even notifications - * to subscribed HomeKit controllers (aka call {@link Characteristic.updateValue}. - * Typically this is 600000 aka 600 seconds aka 10 minutes or 300000 aka 300 seconds aka 5 minutes. + * to subscribed HomeKit controllers (aka call {@link Characteristic.updateValue}). + * Typically, this is 600000 aka 600 seconds aka 10 minutes or 300000 aka 300 seconds aka 5 minutes. */ - notifyIntervalThreshold: number; + notifyIntervalThreshold: number } /** @@ -189,13 +207,12 @@ export interface AdaptiveLightingTransitionPoint { /** * This is the time offset from the transition start to the {@link lowerBound}. */ - lowerBoundTimeOffset: number; + lowerBoundTimeOffset: number + transitionOffset: number - transitionOffset: number; - - lowerBound: AdaptiveLightingTransitionCurveEntry; - upperBound: AdaptiveLightingTransitionCurveEntry; + lowerBound: AdaptiveLightingTransitionCurveEntry + upperBound: AdaptiveLightingTransitionCurveEntry } /** @@ -205,13 +222,13 @@ export interface AdaptiveLightingTransitionCurveEntry { /** * The color temperature in mired. */ - temperature: number, + temperature: number /** * The color temperature actually set to the color temperature characteristic is dependent * on the current brightness value of the lightbulb. * This means you will always need to query the current brightness when updating the color temperature * for the next transition step. - * Additionally you will also need to correct the color temperature when the end user changes the + * Additionally, you will also need to correct the color temperature when the end user changes the * brightness of the Lightbulb. * * The brightnessAdjustmentFactor is always a negative floating point value. @@ -223,21 +240,21 @@ export interface AdaptiveLightingTransitionCurveEntry { * Complete example: * ```js * const temperature = ...; // next transition value, the above property - * // below query the current brightness while staying the the min/max brightness range (typically between 10-100 percent) + * // below query the current brightness while staying the min/max brightness range (typically between 10-100 percent) * const currentBrightness = Math.max(minBrightnessValue, Math.min(maxBrightnessValue, CHARACTERISTIC_BRIGHTNESS_VALUE)); * * // as both temperature and brightnessAdjustmentFactor are floating point values it is advised to round to the next integer * const resultTemperature = Math.round(temperature + brightnessAdjustmentFactor * currentBrightness); * ``` */ - brightnessAdjustmentFactor: number; + brightnessAdjustmentFactor: number /** * The duration in milliseconds this exact temperature value stays the same. - * When we transition to to the temperature value represented by this entry, it stays for the specified + * When we transition to the temperature value represented by this entry, it stays for the specified * duration on the exact same value (with respect to brightness adjustment) until we transition * to the next entry (see {@link transitionTime}). */ - duration?: number; + duration?: number /** * The time in milliseconds the color temperature should transition from the previous * entry to this one. @@ -247,15 +264,15 @@ export interface AdaptiveLightingTransitionCurveEntry { * If this is the first entry in the Curve (this value is probably zero) and is the offset to the transitionStartMillis * (the Date/Time were this transition curve was set up). */ - transitionTime: number; + transitionTime: number } /** * @group Adaptive Lighting */ export interface BrightnessAdjustmentMultiplierRange { - minBrightnessValue: number; - maxBrightnessValue: number; + minBrightnessValue: number + maxBrightnessValue: number } /** @@ -267,7 +284,7 @@ export interface AdaptiveLightingOptions { * You can choose between automatic and manual mode. * See {@link AdaptiveLightingControllerMode}. */ - controllerMode?: AdaptiveLightingControllerMode, + controllerMode?: AdaptiveLightingControllerMode /** * Defines a custom temperature adjustment factor. * @@ -277,13 +294,14 @@ export interface AdaptiveLightingOptions { * For example supplying a value of `-10` will reduce the ColorTemperature, which is * calculated from the transition schedule, by 10 mired for every change. */ - customTemperatureAdjustment?: number, + customTemperatureAdjustment?: number } /** * Defines in which mode the {@link AdaptiveLightingController} will operate in. * @group Adaptive Lighting */ +// eslint-disable-next-line no-restricted-syntax export const enum AdaptiveLightingControllerMode { /** * In automatic mode pretty much everything from setup to transition scheduling is done by the controller. @@ -299,19 +317,20 @@ export const enum AdaptiveLightingControllerMode { /** * @group Adaptive Lighting */ +// eslint-disable-next-line no-restricted-syntax export const enum AdaptiveLightingControllerEvents { /** * This event is called once a HomeKit controller enables Adaptive Lighting * or a HomeHub sends an updated transition schedule for the next 24 hours. * This is also called on startup when AdaptiveLighting was previously enabled. */ - UPDATE = "update", + UPDATE = 'update', /** * In yet unknown circumstances HomeKit may also send a dedicated disable command * via the control point characteristic. You may want to handle that in manual mode as well. * The current transition will still be associated with the controller object when this event is called. */ - DISABLED = "disable", + DISABLED = 'disable', } /** @@ -319,19 +338,20 @@ export const enum AdaptiveLightingControllerEvents { * see {@link ActiveAdaptiveLightingTransition}. */ export interface AdaptiveLightingControllerUpdate { - transitionStartMillis: number; - timeMillisOffset: number; - transitionCurve: AdaptiveLightingTransitionCurveEntry[]; - brightnessAdjustmentRange: BrightnessAdjustmentMultiplierRange; - updateInterval: number, - notifyIntervalThreshold: number; + transitionStartMillis: number + timeMillisOffset: number + transitionCurve: AdaptiveLightingTransitionCurveEntry[] + brightnessAdjustmentRange: BrightnessAdjustmentMultiplierRange + updateInterval: number + notifyIntervalThreshold: number } /** * @group Adaptive Lighting */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface AdaptiveLightingController { + /* eslint-disable ts/method-signature-style */ /** * See {@link AdaptiveLightingControllerEvents.UPDATE} * Also see {@link AdaptiveLightingControllerUpdate} @@ -339,27 +359,29 @@ export declare interface AdaptiveLightingController { * @param event * @param listener */ - on(event: "update", listener: (update: AdaptiveLightingControllerUpdate) => void): this; + on(event: 'update', listener: (update: AdaptiveLightingControllerUpdate) => void): this + /** * See {@link AdaptiveLightingControllerEvents.DISABLED} * * @param event * @param listener */ - on(event: "disable", listener: () => void): this; + on(event: 'disable', listener: () => void): this /** * See {@link AdaptiveLightingControllerUpdate} */ - emit(event: "update", update: AdaptiveLightingControllerUpdate): boolean; - emit(event: "disable"): boolean; + emit(event: 'update', update: AdaptiveLightingControllerUpdate): boolean + emit(event: 'disable'): boolean + /* eslint-enable ts/method-signature-style */ } /** * @group Adaptive Lighting */ export interface SerializedAdaptiveLightingControllerState { - activeTransition: ActiveAdaptiveLightingTransition; + activeTransition: ActiveAdaptiveLightingTransition } /** @@ -375,7 +397,7 @@ export interface SerializedAdaptiveLightingControllerState { * (updating the schedule according to your current day/night situation). * Once enabled the lightbulb will execute the provided transitions. The color temperature value set is always * dependent on the current brightness value. Meaning brighter light will be colder and darker light will be warmer. - * HomeKit considers Adaptive Lighting to be disabled as soon a write happens to either the + * HomeKit considers Adaptive Lighting to be disabled as soon a 'write' happens to either the * Hue/Saturation or the ColorTemperature characteristics. * The AdaptiveLighting state must persist across reboots. * @@ -392,10 +414,10 @@ export interface SerializedAdaptiveLightingControllerState { * * AUTOMATIC (Default mode): * - * This is the easiest mode to setup and needs less to no work form your side for AdaptiveLighting to work. + * This is the easiest mode to set up and needs less to no work form your side for AdaptiveLighting to work. * The AdaptiveLightingController will go through setup procedure with HomeKit and automatically update * the color temperature characteristic base on the current transition schedule. - * It is also adjusting the color temperature when a write to the brightness characteristic happens. + * It is also adjusting the color temperature when a 'write' to the brightness characteristic happens. * Additionally, it will also handle turning off AdaptiveLighting, when it detects a write happening to the * ColorTemperature, Hue or Saturation characteristic (though it can only detect writes coming from HomeKit and * can't detect changes done to the physical devices directly! See below). @@ -417,12 +439,12 @@ export interface SerializedAdaptiveLightingControllerState { * - When using Hue/Saturation: * When using Hue/Saturation in combination with the ColorTemperature characteristic you need to update the * respective other in a particular way depending on if being in "color mode" or "color temperature mode". - * When a write happens to Hue/Saturation characteristic in is advised to set the internal value of the + * When a 'write' happens to Hue/Saturation characteristic in is advised to set the internal value of the * ColorTemperature to the minimal (NOT RAISING an event). - * When a write happens to the ColorTemperature characteristic just MUST convert to a proper representation + * When a 'write' happens to the ColorTemperature characteristic just MUST convert to a proper representation * in hue and saturation values, with RAISING an event. * As noted above you MUST NOT call the {@link Characteristic.setValue} method for this, as this will be considered - * a write to the characteristic and will turn off AdaptiveLighting. Instead, you should use + * a 'write' to the characteristic and will turn off AdaptiveLighting. Instead, you should use * {@link Characteristic.updateValue} for this. * You can and SHOULD use the supplied utility method {@link ColorUtils.colorTemperatureToHueAndSaturation} * for converting mired to hue and saturation values. @@ -440,7 +462,7 @@ export interface SerializedAdaptiveLightingControllerState { * are only sent in the defined interval threshold, adjust the color temperature when brightness is changed * and signal that Adaptive Lighting should be disabled if ColorTemperature, Hue or Saturation is changed manually. * - * First step is to setup up an event handler for the {@link AdaptiveLightingControllerEvents.UPDATE}, which is called + * First step is to set up an event handler for the {@link AdaptiveLightingControllerEvents.UPDATE}, which is called * when AdaptiveLighting is enabled, the HomeHub updates the schedule for the next 24 hours or AdaptiveLighting * is restored from disk on startup. * In the event handler you can get the current schedule via {@link AdaptiveLightingController.getAdaptiveLightingTransitionCurve}, @@ -454,44 +476,43 @@ export interface SerializedAdaptiveLightingControllerState { * the color temperature when the brightness of the lightbulb changes (see {@link AdaptiveLightingTransitionCurveEntry.brightnessAdjustmentFactor}), * and signal when AdaptiveLighting got disabled by calling {@link AdaptiveLightingController.disableAdaptiveLighting} * when ColorTemperature, Hue or Saturation where changed manually. - * Lastly you should set up a event handler for the {@link AdaptiveLightingControllerEvents.DISABLED} event. + * Lastly you should set up an event handler for the {@link AdaptiveLightingControllerEvents.DISABLED} event. * In yet unknown circumstances HomeKit may also send a dedicated disable command via the control point characteristic. * Be prepared to handle that. * * @group Adaptive Lighting */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class AdaptiveLightingController extends EventEmitter implements SerializableController { + private stateChangeDelegate?: StateChangeDelegate - private stateChangeDelegate?: StateChangeDelegate; - - private readonly lightbulb: Lightbulb; - private readonly mode: AdaptiveLightingControllerMode; - private readonly customTemperatureAdjustment: number; + private readonly lightbulb: Lightbulb + private readonly mode: AdaptiveLightingControllerMode + private readonly customTemperatureAdjustment: number - private readonly adjustmentFactorChangedListener: (change: CharacteristicChange) => void; - private readonly characteristicManualWrittenChangeListener: (change: CharacteristicChange) => void; + private readonly adjustmentFactorChangedListener: (change: CharacteristicChange) => void + private readonly characteristicManualWrittenChangeListener: (change: CharacteristicChange) => void - private supportedTransitionConfiguration?: SupportedCharacteristicValueTransitionConfiguration; - private transitionControl?: CharacteristicValueTransitionControl; - private activeTransitionCount?: CharacteristicValueActiveTransitionCount; + private supportedTransitionConfiguration?: SupportedCharacteristicValueTransitionConfiguration + private transitionControl?: CharacteristicValueTransitionControl + private activeTransitionCount?: CharacteristicValueActiveTransitionCount - private colorTemperatureCharacteristic?: ColorTemperature; - private brightnessCharacteristic?: Brightness; - private saturationCharacteristic?: Saturation; - private hueCharacteristic?: Hue; + private colorTemperatureCharacteristic?: ColorTemperature + private brightnessCharacteristic?: Brightness + private saturationCharacteristic?: Saturation + private hueCharacteristic?: Hue - private activeTransition?: ActiveAdaptiveLightingTransition; - private didRunFirstInitializationStep = false; - private updateTimeout?: NodeJS.Timeout; + private activeTransition?: ActiveAdaptiveLightingTransition + private didRunFirstInitializationStep = false + private updateTimeout?: NodeJS.Timeout - private lastTransitionPointInfo?: SavedLastTransitionPointInfo; - private lastEventNotificationSent = 0; - private lastNotifiedTemperatureValue = 0; - private lastNotifiedSaturationValue = 0; - private lastNotifiedHueValue = 0; + private lastTransitionPointInfo?: SavedLastTransitionPointInfo + private lastEventNotificationSent = 0 + private lastNotifiedTemperatureValue = 0 + private lastNotifiedSaturationValue = 0 + private lastNotifiedHueValue = 0 /** * Creates a new instance of the AdaptiveLightingController. @@ -501,32 +522,32 @@ export class AdaptiveLightingController * @param options - Optional options to define the operating mode (automatic vs manual). */ constructor(service: Lightbulb, options?: AdaptiveLightingOptions) { - super(); - this.lightbulb = service; - this.mode = options?.controllerMode ?? AdaptiveLightingControllerMode.AUTOMATIC; - this.customTemperatureAdjustment = options?.customTemperatureAdjustment ?? 0; + super() + this.lightbulb = service + this.mode = options?.controllerMode ?? AdaptiveLightingControllerMode.AUTOMATIC + this.customTemperatureAdjustment = options?.customTemperatureAdjustment ?? 0 - assert(this.lightbulb.testCharacteristic(Characteristic.ColorTemperature), "Lightbulb must have the ColorTemperature characteristic added!"); - assert(this.lightbulb.testCharacteristic(Characteristic.Brightness), "Lightbulb must have the Brightness characteristic added!"); + assert(this.lightbulb.testCharacteristic(Characteristic.ColorTemperature), 'Lightbulb must have the ColorTemperature characteristic added!') + assert(this.lightbulb.testCharacteristic(Characteristic.Brightness), 'Lightbulb must have the Brightness characteristic added!') - this.adjustmentFactorChangedListener = this.handleAdjustmentFactorChanged.bind(this); - this.characteristicManualWrittenChangeListener = this.handleCharacteristicManualWritten.bind(this); + this.adjustmentFactorChangedListener = this.handleAdjustmentFactorChanged.bind(this) + this.characteristicManualWrittenChangeListener = this.handleCharacteristicManualWritten.bind(this) } /** * @private */ controllerId(): ControllerIdentifier { - return DefaultControllerType.CHARACTERISTIC_TRANSITION + "-" + this.lightbulb.getServiceId(); + return `${DefaultControllerType.CHARACTERISTIC_TRANSITION}-${this.lightbulb.getServiceId()}` } // ----------- PUBLIC API START ----------- /** - * Returns if a Adaptive Lighting transition is currently active. + * Returns if an Adaptive Lighting transition is currently active. */ public isAdaptiveLightingActive(): boolean { - return !!this.activeTransition; + return !!this.activeTransition } /** @@ -538,106 +559,106 @@ export class AdaptiveLightingController */ public disableAdaptiveLighting(): void { if (this.updateTimeout) { - clearTimeout(this.updateTimeout); - this.updateTimeout = undefined; + clearTimeout(this.updateTimeout) + this.updateTimeout = undefined } if (this.activeTransition) { - this.colorTemperatureCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); - this.brightnessCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.adjustmentFactorChangedListener); - this.hueCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); - this.saturationCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); + this.colorTemperatureCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) + this.brightnessCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.adjustmentFactorChangedListener) + this.hueCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) + this.saturationCharacteristic?.removeListener(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) - this.activeTransition = undefined; + this.activeTransition = undefined - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() } - this.colorTemperatureCharacteristic = undefined; - this.brightnessCharacteristic = undefined; - this.hueCharacteristic = undefined; - this.saturationCharacteristic = undefined; + this.colorTemperatureCharacteristic = undefined + this.brightnessCharacteristic = undefined + this.hueCharacteristic = undefined + this.saturationCharacteristic = undefined - this.lastTransitionPointInfo = undefined; - this.lastEventNotificationSent = 0; - this.lastNotifiedTemperatureValue = 0; - this.lastNotifiedSaturationValue = 0; - this.lastNotifiedHueValue = 0; + this.lastTransitionPointInfo = undefined + this.lastEventNotificationSent = 0 + this.lastNotifiedTemperatureValue = 0 + this.lastNotifiedSaturationValue = 0 + this.lastNotifiedHueValue = 0 - this.didRunFirstInitializationStep = false; + this.didRunFirstInitializationStep = false - this.activeTransitionCount?.sendEventNotification(0); + this.activeTransitionCount?.sendEventNotification(0) - debug("[%s] Disabling adaptive lighting", this.lightbulb.displayName); + debug('[%s] Disabling adaptive lighting', this.lightbulb.displayName) } /** - * Returns the time where the current transition curve was started in epoch time millis. + * Returns the time when the current transition curve was started in epoch time millis. * A transition curves is active for 24 hours typically and is renewed every 24 hours by a HomeHub. - * Additionally see {@link getAdaptiveLightingTimeOffset}. + * Additionally, see {@link getAdaptiveLightingTimeOffset}. */ public getAdaptiveLightingStartTimeOfTransition(): number { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.transitionStartMillis; + return this.activeTransition.transitionStartMillis } /** * It is not necessarily given, that we have the same time (or rather the correct time) as the HomeKit controller * who set up the transition schedule. - * Thus we record the delta between our current time and the the time send with the setup request. + * Thus, we record the delta between our current time and the time send with the setup request. * timeOffset is defined as Date.now() - getAdaptiveLightingStartTimeOfTransition();. * So in the case were we actually have a correct local time, it most likely will be positive (due to network latency). * But of course it can also be negative. */ public getAdaptiveLightingTimeOffset(): number { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.timeMillisOffset; + return this.activeTransition.timeMillisOffset } public getAdaptiveLightingTransitionCurve(): AdaptiveLightingTransitionCurveEntry[] { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.transitionCurve; + return this.activeTransition.transitionCurve } public getAdaptiveLightingBrightnessMultiplierRange(): BrightnessAdjustmentMultiplierRange { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.brightnessAdjustmentRange; + return this.activeTransition.brightnessAdjustmentRange } /** * This method returns the interval (in milliseconds) in which the light should update its internal color temperature * (aka changes it physical color). - * A lightbulb should ideally change this also when turned of in oder to have a smooth transition when turning the light on. + * A lightbulb should ideally change this also when turned off in oder to have a smooth transition when turning the light on. * - * Typically this evaluates to 60000 milliseconds (60 seconds). + * Typically, this evaluates to 60000 milliseconds (60 seconds). */ public getAdaptiveLightingUpdateInterval(): number { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.updateInterval; + return this.activeTransition.updateInterval } /** - * Returns the minimum interval threshold (in milliseconds) a accessory may notify HomeKit controllers about a new + * Returns the minimum interval threshold (in milliseconds) an accessory may notify HomeKit controllers about a new * color temperature value via event notifications (what happens when you call {@link Characteristic.updateValue}). * Meaning the accessory should only send event notifications to subscribed HomeKit controllers at the specified interval. * - * Typically this evaluates to 600000 milliseconds (10 minutes). + * Typically, this evaluates to 600000 milliseconds (10 minutes). */ public getAdaptiveLightingNotifyIntervalThreshold(): number { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - return this.activeTransition.notifyIntervalThreshold; + return this.activeTransition.notifyIntervalThreshold } // ----------- PUBLIC API END ----------- @@ -645,17 +666,17 @@ export class AdaptiveLightingController private handleActiveTransitionUpdated(calledFromDeserializer = false): void { if (this.activeTransitionCount) { if (!calledFromDeserializer) { - this.activeTransitionCount.sendEventNotification(1); + this.activeTransitionCount.sendEventNotification(1) } else { - this.activeTransitionCount.value = 1; + this.activeTransitionCount.value = 1 } } if (this.mode === AdaptiveLightingControllerMode.AUTOMATIC) { - this.scheduleNextUpdate(); + this.scheduleNextUpdate() } else if (this.mode === AdaptiveLightingControllerMode.MANUAL) { if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } const update: AdaptiveLightingControllerUpdate = { @@ -665,56 +686,56 @@ export class AdaptiveLightingController brightnessAdjustmentRange: this.activeTransition.brightnessAdjustmentRange, updateInterval: this.activeTransition.updateInterval, notifyIntervalThreshold: this.activeTransition.notifyIntervalThreshold, - }; + } - this.emit(AdaptiveLightingControllerEvents.UPDATE, update); + this.emit(AdaptiveLightingControllerEvents.UPDATE, update) } else { - throw new Error("Unsupported adaptive lighting controller mode: " + this.mode); + throw new Error(`Unsupported adaptive lighting controller mode: ${this.mode}`) } if (!calledFromDeserializer) { - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() } } private handleAdaptiveLightingEnabled(): void { // this method is run when the initial curve was sent if (!this.activeTransition) { - throw new Error("There is no active transition!"); + throw new Error('There is no active transition!') } - this.colorTemperatureCharacteristic = this.lightbulb.getCharacteristic(Characteristic.ColorTemperature); - this.brightnessCharacteristic = this.lightbulb.getCharacteristic(Characteristic.Brightness); + this.colorTemperatureCharacteristic = this.lightbulb.getCharacteristic(Characteristic.ColorTemperature) + this.brightnessCharacteristic = this.lightbulb.getCharacteristic(Characteristic.Brightness) - this.colorTemperatureCharacteristic.on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); - this.brightnessCharacteristic.on(CharacteristicEventTypes.CHANGE, this.adjustmentFactorChangedListener); + this.colorTemperatureCharacteristic.on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) + this.brightnessCharacteristic.on(CharacteristicEventTypes.CHANGE, this.adjustmentFactorChangedListener) if (this.lightbulb.testCharacteristic(Characteristic.Hue)) { this.hueCharacteristic = this.lightbulb.getCharacteristic(Characteristic.Hue) - .on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); + .on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) } if (this.lightbulb.testCharacteristic(Characteristic.Saturation)) { this.saturationCharacteristic = this.lightbulb.getCharacteristic(Characteristic.Saturation) - .on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener); + .on(CharacteristicEventTypes.CHANGE, this.characteristicManualWrittenChangeListener) } } private handleAdaptiveLightingDisabled(): void { if (this.mode === AdaptiveLightingControllerMode.MANUAL && this.activeTransition) { // only emit the event if a transition is actually enabled - this.emit(AdaptiveLightingControllerEvents.DISABLED); + this.emit(AdaptiveLightingControllerEvents.DISABLED) } - this.disableAdaptiveLighting(); + this.disableAdaptiveLighting() } private handleAdjustmentFactorChanged(change: CharacteristicChange): void { if (change.newValue === change.oldValue) { - return; + return } // consider the following scenario: // a HomeKit controller queries the light (meaning e.g. Brightness, Hue and Saturation characteristics). // As of the implementation of the light the brightness characteristic get handler returns first - // (and returns a value different than the cached value). - // This change handler gets called and we will update the color temperature accordingly + // (and returns a value different from the cached value). + // This change handler gets called, and we will update the color temperature accordingly // (which also adjusts the internal cached values for Hue and Saturation). // After some short time the Hue or Saturation get handler return with the last known value to the plugin. // As those values now differ from the cached values (we already updated) we get a call to handleCharacteristicManualWritten @@ -726,18 +747,18 @@ export class AdaptiveLightingController // It doesn't ensure that those race conditions do not happen anymore, but with a 1s delay it reduces the possibility by a bit setTimeout(() => { if (!this.activeTransition) { - return; // was disabled in the mean time + return // was disabled in the meantime } - this.scheduleNextUpdate(true); - }, 1000).unref(); + this.scheduleNextUpdate(true) + }, 1000).unref() } else { - this.scheduleNextUpdate(true); // run a dry scheduleNextUpdate to adjust the colorTemperature using the new brightness value + this.scheduleNextUpdate(true) // run a dry scheduleNextUpdate to adjust the colorTemperature using the new brightness value } } /** * This method is called when a change happens to the Hue/Saturation or ColorTemperature characteristic. - * When such a write happens (caused by the user changing the color/temperature) Adaptive Lighting must be disabled. + * When such a 'write' happens (caused by the user changing the color/temperature) Adaptive Lighting must be disabled. * * @param change */ @@ -747,9 +768,8 @@ export class AdaptiveLightingController // or the result of a changed value returned by a read handler // or the change was done by the controller itself - debug("[%s] Received a manual write to an characteristic (newValue: %d, oldValue: %d, reason: %s). Thus disabling adaptive lighting!", - this.lightbulb.displayName, change.newValue, change.oldValue, change.reason); - this.disableAdaptiveLighting(); + debug('[%s] Received a manual write to an characteristic (newValue: %d, oldValue: %d, reason: %s). Thus disabling adaptive lighting!', this.lightbulb.displayName, change.newValue, change.oldValue, change.reason) + this.disableAdaptiveLighting() } } @@ -759,101 +779,101 @@ export class AdaptiveLightingController */ public getCurrentAdaptiveLightingTransitionPoint(): AdaptiveLightingTransitionPoint | undefined { if (!this.activeTransition) { - throw new Error("Cannot calculate current transition point if no transition is active!"); + throw new Error('Cannot calculate current transition point if no transition is active!') } // adjustedNow is the now() date corrected to the time of the initiating controller - const adjustedNow = Date.now() - this.activeTransition.timeMillisOffset; + const adjustedNow = Date.now() - this.activeTransition.timeMillisOffset // "offset" since the start of the transition schedule - const offset = adjustedNow - this.activeTransition.transitionStartMillis; + const offset = adjustedNow - this.activeTransition.transitionStartMillis - let i = this.lastTransitionPointInfo?.curveIndex ?? 0; - let lowerBoundTimeOffset = this.lastTransitionPointInfo?.lowerBoundTimeOffset ?? 0; // time offset to the lowerBound transition entry - let lowerBound: AdaptiveLightingTransitionCurveEntry | undefined = undefined; - let upperBound: AdaptiveLightingTransitionCurveEntry | undefined = undefined; + let i = this.lastTransitionPointInfo?.curveIndex ?? 0 + let lowerBoundTimeOffset = this.lastTransitionPointInfo?.lowerBoundTimeOffset ?? 0 // time offset to the lowerBound transition entry + let lowerBound: AdaptiveLightingTransitionCurveEntry | undefined + let upperBound: AdaptiveLightingTransitionCurveEntry | undefined for (; i + 1 < this.activeTransition.transitionCurve.length; i++) { - const lowerBound0 = this.activeTransition.transitionCurve[i]; - const upperBound0 = this.activeTransition.transitionCurve[i + 1]; + const lowerBound0 = this.activeTransition.transitionCurve[i] + const upperBound0 = this.activeTransition.transitionCurve[i + 1] - const lowerBoundDuration = lowerBound0.duration ?? 0; - lowerBoundTimeOffset += lowerBound0.transitionTime; + const lowerBoundDuration = lowerBound0.duration ?? 0 + lowerBoundTimeOffset += lowerBound0.transitionTime if (offset >= lowerBoundTimeOffset) { if (offset <= lowerBoundTimeOffset + lowerBoundDuration + upperBound0.transitionTime) { - lowerBound = lowerBound0; - upperBound = upperBound0; - break; + lowerBound = lowerBound0 + upperBound = upperBound0 + break } } else if (this.lastTransitionPointInfo) { // if we reached here the entry in the transitionCurve we are searching for is somewhere before current i. // This can only happen when we have a faulty lastTransitionPointInfo (otherwise we would start from i=0). - // Thus we try again by searching from i=0 - this.lastTransitionPointInfo = undefined; - return this.getCurrentAdaptiveLightingTransitionPoint(); + // Thus, we try again by searching from i=0 + this.lastTransitionPointInfo = undefined + return this.getCurrentAdaptiveLightingTransitionPoint() } - lowerBoundTimeOffset += lowerBoundDuration; + lowerBoundTimeOffset += lowerBoundDuration } if (!lowerBound || !upperBound) { - this.lastTransitionPointInfo = undefined; - return undefined; + this.lastTransitionPointInfo = undefined + return undefined } this.lastTransitionPointInfo = { curveIndex: i, // we need to subtract lowerBound.transitionTime. When we start the loop above // with a saved transition point, we will always add lowerBound.transitionTime as first step. - // Otherwise our calculations are simply wrong. + // Otherwise, our calculations are simply wrong. lowerBoundTimeOffset: lowerBoundTimeOffset - lowerBound.transitionTime, - }; + } return { - lowerBoundTimeOffset: lowerBoundTimeOffset, + lowerBoundTimeOffset, transitionOffset: offset - lowerBoundTimeOffset, - lowerBound: lowerBound, - upperBound: upperBound, - }; + lowerBound, + upperBound, + } } private scheduleNextUpdate(dryRun = false): void { if (!this.activeTransition) { - throw new Error("tried scheduling transition when no transition was active!"); + throw new Error('tried scheduling transition when no transition was active!') } if (!dryRun) { - this.updateTimeout = undefined; + this.updateTimeout = undefined } if (!this.didRunFirstInitializationStep) { - this.didRunFirstInitializationStep = true; - this.handleAdaptiveLightingEnabled(); + this.didRunFirstInitializationStep = true + this.handleAdaptiveLightingEnabled() } - const transitionPoint = this.getCurrentAdaptiveLightingTransitionPoint(); + const transitionPoint = this.getCurrentAdaptiveLightingTransitionPoint() if (!transitionPoint) { - debug("[%s] Reached end of transition curve!", this.lightbulb.displayName); + debug('[%s] Reached end of transition curve!', this.lightbulb.displayName) if (!dryRun) { // the transition schedule is only for 24 hours, we reached the end? - this.disableAdaptiveLighting(); + this.disableAdaptiveLighting() } - return; + return } - const lowerBound = transitionPoint.lowerBound; - const upperBound = transitionPoint.upperBound; + const lowerBound = transitionPoint.lowerBound + const upperBound = transitionPoint.upperBound - let interpolatedTemperature: number; - let interpolatedAdjustmentFactor: number; - if (lowerBound.duration && transitionPoint.transitionOffset <= lowerBound.duration) { - interpolatedTemperature = lowerBound.temperature; - interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor; + let interpolatedTemperature: number + let interpolatedAdjustmentFactor: number + if (lowerBound.duration && transitionPoint.transitionOffset <= lowerBound.duration) { + interpolatedTemperature = lowerBound.temperature + interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor } else { - const timePercentage = (transitionPoint.transitionOffset - (lowerBound.duration ?? 0)) / upperBound.transitionTime; - interpolatedTemperature = lowerBound.temperature + (upperBound.temperature - lowerBound.temperature) * timePercentage; + const timePercentage = (transitionPoint.transitionOffset - (lowerBound.duration ?? 0)) / upperBound.transitionTime + interpolatedTemperature = lowerBound.temperature + (upperBound.temperature - lowerBound.temperature) * timePercentage interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor - + (upperBound.brightnessAdjustmentFactor - lowerBound.brightnessAdjustmentFactor) * timePercentage; + + (upperBound.brightnessAdjustmentFactor - lowerBound.brightnessAdjustmentFactor) * timePercentage } const adjustmentMultiplier = Math.max( @@ -862,31 +882,30 @@ export class AdaptiveLightingController this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue, this.brightnessCharacteristic?.value as number, // get handler is not called for optimal performance ), - ); + ) - let temperature = Math.round(interpolatedTemperature + interpolatedAdjustmentFactor * adjustmentMultiplier); + let temperature = Math.round(interpolatedTemperature + interpolatedAdjustmentFactor * adjustmentMultiplier) // apply any manually applied temperature adjustments - temperature += this.customTemperatureAdjustment; + temperature += this.customTemperatureAdjustment - const min = this.colorTemperatureCharacteristic?.props.minValue ?? 140; - const max = this.colorTemperatureCharacteristic?.props.maxValue ?? 500; - temperature = Math.max(min, Math.min(max, temperature)); - const color = ColorUtils.colorTemperatureToHueAndSaturation(temperature); + const min = this.colorTemperatureCharacteristic?.props.minValue ?? 140 + const max = this.colorTemperatureCharacteristic?.props.maxValue ?? 500 + temperature = Math.max(min, Math.min(max, temperature)) + const color = ColorUtils.colorTemperatureToHueAndSaturation(temperature) - debug("[%s] Next temperature value is %d (for brightness %d adj: %s)", - this.lightbulb.displayName, temperature, adjustmentMultiplier, this.customTemperatureAdjustment); + debug('[%s] Next temperature value is %d (for brightness %d adj: %s)', this.lightbulb.displayName, temperature, adjustmentMultiplier, this.customTemperatureAdjustment) const context: AdaptiveLightingCharacteristicContext = { controller: this, omitEventUpdate: true, - }; + } /* * We set saturation and hue values BEFORE we call the ColorTemperature SET handler (via setValue). * First thought was so the API user could get the values in the SET handler of the color temperature characteristic. - * Do this is probably not really elegant cause this would only work when Adaptive Lighting is turned on - * an the accessory MUST in any case update the Hue/Saturation values on a ColorTemperature write + * Do this is probably not really elegant because this would only work when Adaptive Lighting is turned on + * and the accessory MUST in any case update the Hue/Saturation values on a ColorTemperature write * (obviously only if Hue/Saturation characteristics are added to the service). * * The clever thing about this though is that, that it prevents notifications from being sent for Hue and Saturation @@ -898,48 +917,48 @@ export class AdaptiveLightingController * value to the hue and saturation representation. */ if (this.saturationCharacteristic) { - this.saturationCharacteristic.value = color.saturation; + this.saturationCharacteristic.value = color.saturation } if (this.hueCharacteristic) { - this.hueCharacteristic.value = color.hue; + this.hueCharacteristic.value = color.hue } - this.colorTemperatureCharacteristic?.handleSetRequest(temperature, undefined, context).catch(reason => { // reason is HAPStatus code - debug("[%s] Failed to next adaptive lighting transition point: %d", this.lightbulb.displayName, reason); - }); + this.colorTemperatureCharacteristic?.handleSetRequest(temperature, undefined, context).catch((reason) => { // reason is HAPStatus code + debug('[%s] Failed to next adaptive lighting transition point: %d', this.lightbulb.displayName, reason) + }) if (!this.activeTransition) { - console.warn("[" + this.lightbulb.displayName + "] Adaptive Lighting was probably disable my mistake by some call in " + - "the SET handler of the ColorTemperature characteristic! " + - "Please check that you don't call setValue/setCharacteristic on the Hue, Saturation or ColorTemperature characteristic!"); - return; + console.warn(`[${this.lightbulb.displayName}] Adaptive Lighting was probably disable my mistake by some call in ` + + `the SET handler of the ColorTemperature characteristic! ` + + `Please check that you don't call setValue/setCharacteristic on the Hue, Saturation or ColorTemperature characteristic!`) + return } - const now = Date.now(); + const now = Date.now() if (!dryRun && now - this.lastEventNotificationSent >= this.activeTransition.notifyIntervalThreshold) { - debug("[%s] Sending event notifications for current transition!", this.lightbulb.displayName); - this.lastEventNotificationSent = now; + debug('[%s] Sending event notifications for current transition!', this.lightbulb.displayName) + this.lastEventNotificationSent = now const eventContext: AdaptiveLightingCharacteristicContext = { controller: this, - }; + } if (this.lastNotifiedTemperatureValue !== temperature) { - this.colorTemperatureCharacteristic?.sendEventNotification(temperature, eventContext); - this.lastNotifiedTemperatureValue = temperature; + this.colorTemperatureCharacteristic?.sendEventNotification(temperature, eventContext) + this.lastNotifiedTemperatureValue = temperature } if (this.saturationCharacteristic && this.lastNotifiedSaturationValue !== color.saturation) { - this.saturationCharacteristic.sendEventNotification(color.saturation, eventContext); - this.lastNotifiedSaturationValue = color.saturation; + this.saturationCharacteristic.sendEventNotification(color.saturation, eventContext) + this.lastNotifiedSaturationValue = color.saturation } if (this.hueCharacteristic && this.lastNotifiedHueValue !== color.hue) { - this.hueCharacteristic.sendEventNotification(color.hue, eventContext); - this.lastNotifiedHueValue = color.hue; + this.hueCharacteristic.sendEventNotification(color.hue, eventContext) + this.lastNotifiedHueValue = color.hue } } if (!dryRun) { - this.updateTimeout = setTimeout(this.scheduleNextUpdate.bind(this), this.activeTransition.updateInterval); + this.updateTimeout = setTimeout(this.scheduleNextUpdate.bind(this), this.activeTransition.updateInterval) } } @@ -947,13 +966,13 @@ export class AdaptiveLightingController * @private */ constructServices(): ControllerServiceMap { - return {}; + return {} } /** * @private */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars initWithServices(serviceMap: ControllerServiceMap): void | ControllerServiceMap { // do nothing } @@ -962,50 +981,50 @@ export class AdaptiveLightingController * @private */ configureServices(): void { - this.supportedTransitionConfiguration = this.lightbulb.getCharacteristic(Characteristic.SupportedCharacteristicValueTransitionConfiguration); + this.supportedTransitionConfiguration = this.lightbulb.getCharacteristic(Characteristic.SupportedCharacteristicValueTransitionConfiguration) this.transitionControl = this.lightbulb.getCharacteristic(Characteristic.CharacteristicValueTransitionControl) - .updateValue(""); + .updateValue('') this.activeTransitionCount = this.lightbulb.getCharacteristic(Characteristic.CharacteristicValueActiveTransitionCount) - .updateValue(0); + .updateValue(0) this.supportedTransitionConfiguration - .onGet(this.handleSupportedTransitionConfigurationRead.bind(this)); + .onGet(this.handleSupportedTransitionConfigurationRead.bind(this)) this.transitionControl .onGet(() => { - return this.buildTransitionControlResponseBuffer().toString("base64"); + return this.buildTransitionControlResponseBuffer().toString('base64') }) - .onSet(value => { + .onSet((value) => { try { - return this.handleTransitionControlWrite(value); + return this.handleTransitionControlWrite(value) } catch (error) { - console.warn(`[%s] DEBUG: '${value}'`); - console.warn("[%s] Encountered error on CharacteristicValueTransitionControl characteristic: " + error.stack); - this.disableAdaptiveLighting(); - throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + console.warn(`[%s] DEBUG: '${value}'`) + console.warn(`[%s] Encountered error on CharacteristicValueTransitionControl characteristic: ${error.stack}`) + this.disableAdaptiveLighting() + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - }); + }) } /** * @private */ handleControllerRemoved(): void { - this.lightbulb.removeCharacteristic(this.supportedTransitionConfiguration!); - this.lightbulb.removeCharacteristic(this.transitionControl!); - this.lightbulb.removeCharacteristic(this.activeTransitionCount!); + this.lightbulb.removeCharacteristic(this.supportedTransitionConfiguration!) + this.lightbulb.removeCharacteristic(this.transitionControl!) + this.lightbulb.removeCharacteristic(this.activeTransitionCount!) - this.supportedTransitionConfiguration = undefined; - this.transitionControl = undefined; - this.activeTransitionCount = undefined; + this.supportedTransitionConfiguration = undefined + this.transitionControl = undefined + this.activeTransitionCount = undefined - this.removeAllListeners(); + this.removeAllListeners() } /** * @private */ handleFactoryReset(): void { - this.handleAdaptiveLightingDisabled(); + this.handleAdaptiveLightingDisabled() } /** @@ -1013,241 +1032,269 @@ export class AdaptiveLightingController */ serialize(): SerializedAdaptiveLightingControllerState | undefined { if (!this.activeTransition) { - return undefined; + return undefined } return { activeTransition: this.activeTransition, - }; + } } /** * @private */ deserialize(serialized: SerializedAdaptiveLightingControllerState): void { - this.activeTransition = serialized.activeTransition; + this.activeTransition = serialized.activeTransition // Data migrations from beta builds if (!this.activeTransition.transitionId) { // @ts-expect-error: data migration from beta builds - this.activeTransition.transitionId = this.activeTransition.id1; + this.activeTransition.transitionId = this.activeTransition.id1 // @ts-expect-error: data migration from beta builds - delete this.activeTransition.id1; + delete this.activeTransition.id1 } if (!this.activeTransition.timeMillisOffset) { // compatibility to data produced by early betas - this.activeTransition.timeMillisOffset = 0; + this.activeTransition.timeMillisOffset = 0 } - this.handleActiveTransitionUpdated(true); + this.handleActiveTransitionUpdated(true) } /** * @private */ setupStateChangeDelegate(delegate?: StateChangeDelegate): void { - this.stateChangeDelegate = delegate; + this.stateChangeDelegate = delegate } private handleSupportedTransitionConfigurationRead(): string { - const brightnessIID = this.lightbulb?.getCharacteristic(Characteristic.Brightness).iid; - const temperatureIID = this.lightbulb?.getCharacteristic(Characteristic.ColorTemperature).iid; - assert(brightnessIID, "iid for brightness characteristic is undefined"); - assert(temperatureIID, "iid for temperature characteristic is undefined"); - - return tlv.encode(SupportedCharacteristicValueTransitionConfigurationsTypes.SUPPORTED_TRANSITION_CONFIGURATION, [ - tlv.encode( - SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID, tlv.writeVariableUIntLE(brightnessIID!), - SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE, TransitionType.BRIGHTNESS, + const brightnessIID = this.lightbulb?.getCharacteristic(Characteristic.Brightness).iid + const temperatureIID = this.lightbulb?.getCharacteristic(Characteristic.ColorTemperature).iid + assert(brightnessIID, 'iid for brightness characteristic is undefined') + assert(temperatureIID, 'iid for temperature characteristic is undefined') + + return encode(SupportedCharacteristicValueTransitionConfigurationsTypes.SUPPORTED_TRANSITION_CONFIGURATION, [ + encode( + SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID, + writeVariableUIntLE(brightnessIID!), + SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE, + TransitionType.BRIGHTNESS, ), - tlv.encode( - SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID, tlv.writeVariableUIntLE(temperatureIID!), - SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE, TransitionType.COLOR_TEMPERATURE, + encode( + SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID, + writeVariableUIntLE(temperatureIID!), + SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE, + TransitionType.COLOR_TEMPERATURE, ), - ]).toString("base64"); + ]).toString('base64') } private buildTransitionControlResponseBuffer(time?: number): Buffer { if (!this.activeTransition) { - return Buffer.alloc(0); + return Buffer.alloc(0) } - const active = this.activeTransition; + const active = this.activeTransition - const timeSinceStart = time ?? (Date.now() - active.timeMillisOffset - active.transitionStartMillis); - const timeSinceStartBuffer = tlv.writeVariableUIntLE(timeSinceStart); + const timeSinceStart = time ?? (Date.now() - active.timeMillisOffset - active.transitionStartMillis) + const timeSinceStartBuffer = writeVariableUIntLE(timeSinceStart) - let parameters = tlv.encode( - ValueTransitionParametersTypes.TRANSITION_ID, uuid.write(active.transitionId), - ValueTransitionParametersTypes.START_TIME, Buffer.from(active.transitionStartBuffer, "hex"), - ); + let parameters = encode( + ValueTransitionParametersTypes.TRANSITION_ID, + write(active.transitionId), + ValueTransitionParametersTypes.START_TIME, + Buffer.from(active.transitionStartBuffer, 'hex'), + ) if (active.id3) { parameters = Buffer.concat([ parameters, - tlv.encode(ValueTransitionParametersTypes.UNKNOWN_3, Buffer.from(active.id3, "hex")), - ]); + encode(ValueTransitionParametersTypes.UNKNOWN_3, Buffer.from(active.id3, 'hex')), + ]) } - const status = tlv.encode( - ValueTransitionConfigurationStatusTypes.CHARACTERISTIC_IID, tlv.writeVariableUIntLE(active.iid!), - ValueTransitionConfigurationStatusTypes.TRANSITION_PARAMETERS, parameters, - ValueTransitionConfigurationStatusTypes.TIME_SINCE_START, timeSinceStartBuffer, - ); - - return tlv.encode( - ValueTransitionConfigurationResponseTypes.VALUE_CONFIGURATION_STATUS, status, - ); + const status = encode( + ValueTransitionConfigurationStatusTypes.CHARACTERISTIC_IID, + writeVariableUIntLE(active.iid!), + ValueTransitionConfigurationStatusTypes.TRANSITION_PARAMETERS, + parameters, + ValueTransitionConfigurationStatusTypes.TIME_SINCE_START, + timeSinceStartBuffer, + ) + + return encode( + ValueTransitionConfigurationResponseTypes.VALUE_CONFIGURATION_STATUS, + status, + ) } private handleTransitionControlWrite(value: CharacteristicValue): string { - if (typeof value !== "string") { - throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST); + if (typeof value !== 'string') { + throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST) } - const tlvData = tlv.decode(Buffer.from(value, "base64")); - const responseBuffers: Buffer[] = []; + const tlvData = decode(Buffer.from(value, 'base64')) + const responseBuffers: Buffer[] = [] - const readTransition = tlvData[TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION]; + const readTransition = tlvData[TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION] if (readTransition) { - const readTransitionResponse = this.handleTransitionControlReadTransition(readTransition); + const readTransitionResponse = this.handleTransitionControlReadTransition(readTransition) if (readTransitionResponse) { - responseBuffers.push(readTransitionResponse); + responseBuffers.push(readTransitionResponse) } } - const updateTransition = tlvData[TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION]; + const updateTransition = tlvData[TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION] if (updateTransition) { - const updateTransitionResponse = this.handleTransitionControlUpdateTransition(updateTransition); + const updateTransitionResponse = this.handleTransitionControlUpdateTransition(updateTransition) if (updateTransitionResponse) { - responseBuffers.push(updateTransitionResponse); + responseBuffers.push(updateTransitionResponse) } } - return Buffer.concat(responseBuffers).toString("base64"); + return Buffer.concat(responseBuffers).toString('base64') } private handleTransitionControlReadTransition(buffer: Buffer): Buffer | undefined { - const readTransition = tlv.decode(buffer); + const readTransition = decode(buffer) - const iid = tlv.readVariableUIntLE(readTransition[ReadValueTransitionConfiguration.CHARACTERISTIC_IID]); + const iid = readVariableUIntLE(readTransition[ReadValueTransitionConfiguration.CHARACTERISTIC_IID]) if (this.activeTransition) { if (this.activeTransition.iid !== iid) { - console.warn("[" + this.lightbulb.displayName + "] iid of current adaptive lighting transition (" + this.activeTransition.iid - + ") doesn't match the requested one " + iid); - throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST); + console.warn(`[${this.lightbulb.displayName}] iid of current adaptive lighting transition (${this.activeTransition.iid + }) doesn't match the requested one ${iid}`) + throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST) } - let parameters = tlv.encode( - ValueTransitionParametersTypes.TRANSITION_ID, uuid.write(this.activeTransition.transitionId), - ValueTransitionParametersTypes.START_TIME, Buffer.from(this.activeTransition.transitionStartBuffer, "hex"), - ); + let parameters = encode( + ValueTransitionParametersTypes.TRANSITION_ID, + write(this.activeTransition.transitionId), + ValueTransitionParametersTypes.START_TIME, + Buffer.from(this.activeTransition.transitionStartBuffer, 'hex'), + ) if (this.activeTransition.id3) { parameters = Buffer.concat([ parameters, - tlv.encode(ValueTransitionParametersTypes.UNKNOWN_3, Buffer.from(this.activeTransition.id3, "hex")), - ]); + encode(ValueTransitionParametersTypes.UNKNOWN_3, Buffer.from(this.activeTransition.id3, 'hex')), + ]) } - return tlv.encode( - TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION, tlv.encode( - ValueTransitionConfigurationTypes.CHARACTERISTIC_IID, tlv.writeVariableUIntLE(this.activeTransition.iid), - ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS, parameters, - ValueTransitionConfigurationTypes.UNKNOWN_3, 1, - ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION, tlv.encode( - TransitionCurveConfigurationTypes.TRANSITION_ENTRY, this.activeTransition.transitionCurve.map((entry, index, array) => { - const duration = array[index - 1]?.duration ?? 0; // we store stuff differently :sweat_smile: - - return tlv.encode( - TransitionEntryTypes.ADJUSTMENT_FACTOR, tlv.writeFloat32LE(entry.brightnessAdjustmentFactor), - TransitionEntryTypes.VALUE, tlv.writeFloat32LE(entry.temperature), - TransitionEntryTypes.TRANSITION_OFFSET, tlv.writeVariableUIntLE(entry.transitionTime), - TransitionEntryTypes.DURATION, tlv.writeVariableUIntLE(duration), - ); + return encode( + TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION, + encode( + ValueTransitionConfigurationTypes.CHARACTERISTIC_IID, + writeVariableUIntLE(this.activeTransition.iid), + ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS, + parameters, + ValueTransitionConfigurationTypes.UNKNOWN_3, + 1, + ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION, + encode( + TransitionCurveConfigurationTypes.TRANSITION_ENTRY, + this.activeTransition.transitionCurve.map((entry, index, array) => { + const duration = array[index - 1]?.duration ?? 0 // we store stuff differently :sweat_smile: + + return encode( + TransitionEntryTypes.ADJUSTMENT_FACTOR, + writeFloat32LE(entry.brightnessAdjustmentFactor), + TransitionEntryTypes.VALUE, + writeFloat32LE(entry.temperature), + TransitionEntryTypes.TRANSITION_OFFSET, + writeVariableUIntLE(entry.transitionTime), + TransitionEntryTypes.DURATION, + writeVariableUIntLE(duration), + ) }), - TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID, tlv.writeVariableUIntLE(this.activeTransition.brightnessCharacteristicIID), - TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE, tlv.encode( - // eslint-disable-next-line max-len - TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue), - // eslint-disable-next-line max-len - TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue), + TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID, + writeVariableUIntLE(this.activeTransition.brightnessCharacteristicIID), + TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE, + encode( + + TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER, + writeUInt32(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue), + + TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER, + writeUInt32(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue), ), ), - ValueTransitionConfigurationTypes.UPDATE_INTERVAL, tlv.writeVariableUIntLE(this.activeTransition.updateInterval), - ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD, tlv.writeVariableUIntLE(this.activeTransition.notifyIntervalThreshold), + ValueTransitionConfigurationTypes.UPDATE_INTERVAL, + writeVariableUIntLE(this.activeTransition.updateInterval), + ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD, + writeVariableUIntLE(this.activeTransition.notifyIntervalThreshold), ), - ); + ) } else { - return undefined; // returns empty string + return undefined // returns empty string } } private handleTransitionControlUpdateTransition(buffer: Buffer): Buffer { - const updateTransition = tlv.decode(buffer); - const transitionConfiguration = tlv.decode(updateTransition[UpdateValueTransitionConfigurationsTypes.VALUE_TRANSITION_CONFIGURATION]); + const updateTransition = decode(buffer) + const transitionConfiguration = decode(updateTransition[UpdateValueTransitionConfigurationsTypes.VALUE_TRANSITION_CONFIGURATION]) - const iid = tlv.readVariableUIntLE(transitionConfiguration[ValueTransitionConfigurationTypes.CHARACTERISTIC_IID]); + const iid = readVariableUIntLE(transitionConfiguration[ValueTransitionConfigurationTypes.CHARACTERISTIC_IID]) if (!this.lightbulb.getCharacteristicByIID(iid)) { - throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST); + throw new HapStatusError(HAPStatus.INVALID_VALUE_IN_REQUEST) } - const param3 = transitionConfiguration[ValueTransitionConfigurationTypes.UNKNOWN_3]?.readUInt8(0); // when present it is always 1 + const param3 = transitionConfiguration[ValueTransitionConfigurationTypes.UNKNOWN_3]?.readUInt8(0) // when present it is always 1 if (!param3) { // if HomeKit just sends the iid, we consider that as "disable adaptive lighting" (assumption) - this.handleAdaptiveLightingDisabled(); - return tlv.encode(TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION, Buffer.alloc(0)); + this.handleAdaptiveLightingDisabled() + return encode(TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION, Buffer.alloc(0)) } - const parametersTLV = tlv.decode(transitionConfiguration[ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS]); - const curveConfiguration = tlv.decodeWithLists(transitionConfiguration[ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION]); - const updateInterval = transitionConfiguration[ValueTransitionConfigurationTypes.UPDATE_INTERVAL]?.readUInt16LE(0); - const notifyIntervalThreshold = transitionConfiguration[ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD].readUInt32LE(0); + const parametersTLV = decode(transitionConfiguration[ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS]) + const curveConfiguration = decodeWithLists(transitionConfiguration[ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION]) + const updateInterval = transitionConfiguration[ValueTransitionConfigurationTypes.UPDATE_INTERVAL]?.readUInt16LE(0) + const notifyIntervalThreshold = transitionConfiguration[ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD].readUInt32LE(0) - const transitionId = parametersTLV[ValueTransitionParametersTypes.TRANSITION_ID]; - const startTime = parametersTLV[ValueTransitionParametersTypes.START_TIME]; - const id3 = parametersTLV[ValueTransitionParametersTypes.UNKNOWN_3]; // this may be undefined + const transitionId = parametersTLV[ValueTransitionParametersTypes.TRANSITION_ID] + const startTime = parametersTLV[ValueTransitionParametersTypes.START_TIME] + const id3 = parametersTLV[ValueTransitionParametersTypes.UNKNOWN_3] // this may be undefined - const startTimeMillis = epochMillisFromMillisSince2001_01_01Buffer(startTime); - const timeMillisOffset = Date.now() - startTimeMillis; + const startTimeMillis = epochMillisFromMillisSince2001_01_01Buffer(startTime) + const timeMillisOffset = Date.now() - startTimeMillis - const transitionCurve: AdaptiveLightingTransitionCurveEntry[] = []; - let previous: AdaptiveLightingTransitionCurveEntry | undefined = undefined; + const transitionCurve: AdaptiveLightingTransitionCurveEntry[] = [] + let previous: AdaptiveLightingTransitionCurveEntry | undefined - const transitions = curveConfiguration[TransitionCurveConfigurationTypes.TRANSITION_ENTRY] as Buffer[]; + const transitions = curveConfiguration[TransitionCurveConfigurationTypes.TRANSITION_ENTRY] as Buffer[] for (const entry of transitions) { - const tlvEntry = tlv.decode(entry); + const tlvEntry = decode(entry) - const adjustmentFactor = tlvEntry[TransitionEntryTypes.ADJUSTMENT_FACTOR].readFloatLE(0); - const value = tlvEntry[TransitionEntryTypes.VALUE].readFloatLE(0); + const adjustmentFactor = tlvEntry[TransitionEntryTypes.ADJUSTMENT_FACTOR].readFloatLE(0) + const value = tlvEntry[TransitionEntryTypes.VALUE].readFloatLE(0) - const transitionOffset = tlv.readVariableUIntLE(tlvEntry[TransitionEntryTypes.TRANSITION_OFFSET]); + const transitionOffset = readVariableUIntLE(tlvEntry[TransitionEntryTypes.TRANSITION_OFFSET]) - const duration = tlvEntry[TransitionEntryTypes.DURATION]? tlv.readVariableUIntLE(tlvEntry[TransitionEntryTypes.DURATION]): undefined; + const duration = tlvEntry[TransitionEntryTypes.DURATION] ? readVariableUIntLE(tlvEntry[TransitionEntryTypes.DURATION]) : undefined if (previous) { - previous.duration = duration; + previous.duration = duration } previous = { temperature: value, brightnessAdjustmentFactor: adjustmentFactor, transitionTime: transitionOffset, - }; - transitionCurve.push(previous); + } + transitionCurve.push(previous) } - const adjustmentIID = tlv.readVariableUIntLE((curveConfiguration[TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID] as Buffer)); - const adjustmentMultiplierRange = tlv.decode(curveConfiguration[TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE] as Buffer); - const minAdjustmentMultiplier = adjustmentMultiplierRange[TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER].readUInt32LE(0); - const maxAdjustmentMultiplier = adjustmentMultiplierRange[TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER].readUInt32LE(0); + const adjustmentIID = readVariableUIntLE((curveConfiguration[TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID] as Buffer)) + const adjustmentMultiplierRange = decode(curveConfiguration[TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE] as Buffer) + const minAdjustmentMultiplier = adjustmentMultiplierRange[TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER].readUInt32LE(0) + const maxAdjustmentMultiplier = adjustmentMultiplierRange[TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER].readUInt32LE(0) this.activeTransition = { - iid: iid, + iid, transitionStartMillis: startTimeMillis, - timeMillisOffset: timeMillisOffset, + timeMillisOffset, - transitionId: uuid.unparse(transitionId), - transitionStartBuffer: startTime.toString("hex"), - id3: id3?.toString("hex"), + transitionId: unparse(transitionId), + transitionStartBuffer: startTime.toString('hex'), + id3: id3?.toString('hex'), brightnessCharacteristicIID: adjustmentIID, brightnessAdjustmentRange: { @@ -1255,25 +1302,25 @@ export class AdaptiveLightingController maxBrightnessValue: maxAdjustmentMultiplier, }, - transitionCurve: transitionCurve, + transitionCurve, updateInterval: updateInterval ?? 60000, - notifyIntervalThreshold: notifyIntervalThreshold, - }; + notifyIntervalThreshold, + } if (this.updateTimeout) { - clearTimeout(this.updateTimeout); - this.updateTimeout = undefined; - debug("[%s] Adaptive lighting was renewed.", this.lightbulb.displayName); + clearTimeout(this.updateTimeout) + this.updateTimeout = undefined + debug('[%s] Adaptive lighting was renewed.', this.lightbulb.displayName) } else { - debug("[%s] Adaptive lighting was enabled.", this.lightbulb.displayName); + debug('[%s] Adaptive lighting was enabled.', this.lightbulb.displayName) } - this.handleActiveTransitionUpdated(); + this.handleActiveTransitionUpdated() - return tlv.encode( - TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION, this.buildTransitionControlResponseBuffer(0), - ); + return encode( + TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION, + this.buildTransitionControlResponseBuffer(0), + ) } - } diff --git a/src/lib/controller/CameraController.spec.ts b/src/lib/controller/CameraController.spec.ts index b0964be85..6b6a93ca9 100644 --- a/src/lib/controller/CameraController.spec.ts +++ b/src/lib/controller/CameraController.spec.ts @@ -1,41 +1,46 @@ -import crypto from "crypto"; -import { - CameraController, +import type { CameraControllerOptions, CameraRecordingDelegate, CameraStreamingDelegate, - DoorbellController, PrepareStreamCallback, - ResourceRequestReason, SnapshotRequestCallback, StreamRequestCallback, -} from "."; +} from '.' +import type { + CameraRecordingConfiguration, + CameraRecordingOptions, + CameraStreamingOptions, + PrepareStreamRequest, + RecordingPacket, + SnapshotRequest, + StreamingRequest, +} from '../camera' +import type { HDSProtocolSpecificErrorReason } from '../datastream' + +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' + +import { beforeEach, describe, expect, it } from 'vitest' + import { AudioBitrate, AudioRecordingCodecType, AudioRecordingSamplerate, AudioStreamingCodecType, AudioStreamingSamplerate, - CameraRecordingConfiguration, - CameraRecordingOptions, - CameraStreamingOptions, EventTriggerOption, H264Level, H264Profile, MediaContainerType, - PrepareStreamRequest, - RecordingPacket, - SnapshotRequest, SRTPCryptoSuites, - StreamingRequest, VideoCodecType, -} from "../camera"; -import { Characteristic } from "../Characteristic"; -import { HDSProtocolSpecificErrorReason } from "../datastream"; -import "../definitions"; -import { HAPStatus } from "../HAPServer"; +} from '../camera/index.js' +import { Characteristic } from '../Characteristic.js' +import '../definitions/index.js' +import { HAPStatus } from '../HAPServer.js' +import { CameraController, DoorbellController, ResourceRequestReason } from './index.js' -export const MOCK_IMAGE = crypto.randomBytes(64); +export const MOCK_IMAGE = randomBytes(64) export const mockStreamingOptions: CameraStreamingOptions = { supportedCryptoSuites: [SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80], @@ -65,7 +70,7 @@ export const mockStreamingOptions: CameraStreamingOptions = { samplerate: AudioStreamingSamplerate.KHZ_24, }], }, -}; +} export const mockRecordingOptions: CameraRecordingOptions = { prebufferLength: 4000, @@ -87,39 +92,39 @@ export const mockRecordingOptions: CameraRecordingOptions = { samplerate: AudioRecordingSamplerate.KHZ_48, }], }, -}; +} export class MockDelegate implements CameraStreamingDelegate, CameraRecordingDelegate { handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void { - callback(undefined, MOCK_IMAGE); + callback(undefined, MOCK_IMAGE) } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void { - throw Error("Unsupported!"); + throw new Error('Unsupported!') } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void { - throw Error("Unsupported!"); + throw new Error('Unsupported!') } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars async *handleRecordingStreamRequest(streamId: number): AsyncGenerator { - yield { data: Buffer.alloc(64, 0), isLast: true }; + yield { data: Buffer.alloc(64, 0), isLast: true } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars closeRecordingStream(streamId: number, reason: HDSProtocolSpecificErrorReason): void { // do nothing } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars updateRecordingActive(active: boolean): void { // do nothing } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void { // do nothing } @@ -132,176 +137,175 @@ export function createCameraControllerOptions( delegate: CameraStreamingDelegate & CameraRecordingDelegate = new MockDelegate(), ): CameraControllerOptions { return { - cameraStreamCount: cameraStreamCount, - delegate: delegate, - streamingOptions: streamingOptions, + cameraStreamCount, + delegate, + streamingOptions, recording: { options: recordingOptions, - delegate: delegate, + delegate, }, sensors: { motion: true, occupancy: true, }, - }; + } } -describe("CameraController", () => { - let controller: CameraController; +describe('cameraController', () => { + let controller: CameraController beforeEach(() => { - controller = new CameraController(createCameraControllerOptions()); + controller = new CameraController(createCameraControllerOptions()) // basic controller init - controller.constructServices(); - controller.configureServices(); - }); + controller.constructServices() + controller.configureServices() + }) - test("init", () => { + it('init', () => { expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value) - .toBe(1); + .toBe(1) expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.PeriodicSnapshotsActive).value) - .toBe(1); + .toBe(1) expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.EventSnapshotsActive).value) - .toBe(1); + .toBe(1) expect(controller.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive).value) - .toBe(0); + .toBe(0) expect(controller.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.Active).value) - .toBe(Characteristic.Active.INACTIVE); + .toBe(Characteristic.Active.INACTIVE) for (const streamManagement of controller.streamManagements) { expect(streamManagement.getService().getCharacteristic(Characteristic.Active).value) - .toBe(Characteristic.Active.ACTIVE); + .toBe(Characteristic.Active.ACTIVE) } - expect(controller.motionService?.testCharacteristic(Characteristic.StatusActive)).toBeTruthy(); + expect(controller.motionService?.testCharacteristic(Characteristic.StatusActive)).toBeTruthy() expect(controller.motionService!.getCharacteristic(Characteristic.StatusActive).value) - .toEqual(true); + .toEqual(true) - expect(controller.occupancyService?.testCharacteristic(Characteristic.StatusActive)).toBeTruthy(); + expect(controller.occupancyService?.testCharacteristic(Characteristic.StatusActive)).toBeTruthy() expect(controller.occupancyService!.getCharacteristic(Characteristic.StatusActive).value) - .toEqual(true); - }); + .toEqual(true) + }) - describe("retrieveEventTriggerOptions", () => { - test("with motion service", () => { + describe('retrieveEventTriggerOptions', () => { + it('with motion service', () => { // @ts-expect-error (private access) - expect(controller.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION); - }); + expect(controller.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION) + }) - test("with motion service and doorbell", () => { - const doorbell = new DoorbellController(createCameraControllerOptions()); - doorbell.constructServices(); - doorbell.configureServices(); + it('with motion service and doorbell', () => { + const doorbell = new DoorbellController(createCameraControllerOptions()) + doorbell.constructServices() + doorbell.configureServices() // @ts-expect-error (private access) - expect(doorbell.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL); - }); + expect(doorbell.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL) + }) - test("with motion service and doorbell and override", () => { - const options = mockRecordingOptions; - options.overrideEventTriggerOptions = [EventTriggerOption.MOTION]; - const doorbell = new DoorbellController(createCameraControllerOptions(options)); - doorbell.constructServices(); - doorbell.configureServices(); + it('with motion service and doorbell and override', () => { + const options = mockRecordingOptions + options.overrideEventTriggerOptions = [EventTriggerOption.MOTION] + const doorbell = new DoorbellController(createCameraControllerOptions(options)) + doorbell.constructServices() + doorbell.configureServices() // @ts-expect-error (private access) - expect(doorbell.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL); - }); + expect(doorbell.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL) + }) - test("with motion service and doorbell specified as override", () => { - const options = mockRecordingOptions; - options.overrideEventTriggerOptions = [EventTriggerOption.DOORBELL]; - const camera = new CameraController(createCameraControllerOptions(options)); - camera.constructServices(); - camera.configureServices(); + it('with motion service and doorbell specified as override', () => { + const options = mockRecordingOptions + options.overrideEventTriggerOptions = [EventTriggerOption.DOORBELL] + const camera = new CameraController(createCameraControllerOptions(options)) + camera.constructServices() + camera.configureServices() // @ts-expect-error (private access) - expect(camera.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL); - }); - }); + expect(camera.recordingManagement?.eventTriggerOptions).toBe(EventTriggerOption.MOTION | EventTriggerOption.DOORBELL) + }) + }) - describe("handleSnapshotRequest", () => { + describe('handleSnapshotRequest', () => { beforeEach(() => { controller.recordingManagement?.operatingModeService - .setCharacteristic(Characteristic.PeriodicSnapshotsActive, true); - }); + .setCharacteristic(Characteristic.PeriodicSnapshotsActive, true) + }) - test("simple handleSnapshotRequest", async () => { - const result = controller.handleSnapshotRequest(100, 100, "SomeAccessory", undefined); - await expect(result).resolves.toEqual(MOCK_IMAGE); - }); + it('simple handleSnapshotRequest', async () => { + const result = controller.handleSnapshotRequest(100, 100, 'SomeAccessory', undefined) + await expect(result).resolves.toEqual(MOCK_IMAGE) + }) - test("handleSnapshot considering PeriodicSnapshotsActive state", async () => { - controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.PeriodicSnapshotsActive, false); + it('handleSnapshot considering PeriodicSnapshotsActive state', async () => { + controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.PeriodicSnapshotsActive, false) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", undefined), - ).rejects.toEqual(HAPStatus.INSUFFICIENT_PRIVILEGES); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', undefined), + ).rejects.toEqual(HAPStatus.INSUFFICIENT_PRIVILEGES) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", ResourceRequestReason.PERIODIC), - ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', ResourceRequestReason.PERIODIC), + ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", ResourceRequestReason.EVENT), - ).resolves.toEqual(MOCK_IMAGE); - }); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', ResourceRequestReason.EVENT), + ).resolves.toEqual(MOCK_IMAGE) + }) - test("handleSnapshot considering EventSnapshotsActive state", async () => { - controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.EventSnapshotsActive, false); + it('handleSnapshot considering EventSnapshotsActive state', async () => { + controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.EventSnapshotsActive, false) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", undefined), - ).rejects.toEqual(HAPStatus.INSUFFICIENT_PRIVILEGES); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', undefined), + ).rejects.toEqual(HAPStatus.INSUFFICIENT_PRIVILEGES) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", ResourceRequestReason.PERIODIC), - ).resolves.toEqual(MOCK_IMAGE); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', ResourceRequestReason.PERIODIC), + ).resolves.toEqual(MOCK_IMAGE) await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", ResourceRequestReason.EVENT), - ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); - }); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', ResourceRequestReason.EVENT), + ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) + }) - test("handleSnapshot considering HomeKitCameraActive state", async () => { - controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.HomeKitCameraActive, false); + it('handleSnapshot considering HomeKitCameraActive state', async () => { + controller.recordingManagement?.operatingModeService.setCharacteristic(Characteristic.HomeKitCameraActive, false) for (const reason of [undefined, ResourceRequestReason.PERIODIC, ResourceRequestReason.EVENT]) { await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", reason), - ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', reason), + ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } - }); + }) - test("handleSnapshot considering RTPStreamManagement.Active state", async () => { + it('handleSnapshot considering RTPStreamManagement.Active state', async () => { for (const management of controller.streamManagements) { - management.service.setCharacteristic(Characteristic.Active, false); + management.service.setCharacteristic(Characteristic.Active, false) } for (const reason of [undefined, ResourceRequestReason.PERIODIC, ResourceRequestReason.EVENT]) { await expect( - controller.handleSnapshotRequest(100, 100, "SomeAccessory", reason), - ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + controller.handleSnapshotRequest(100, 100, 'SomeAccessory', reason), + ).rejects.toEqual(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } - }); - }); - - describe("serialization", () => { - test("identity", () => { - const serialized0 = controller.serialize()!; - controller.deserialize(serialized0); - const serialized1 = controller.serialize(); + }) + }) - expect(serialized0).toEqual(serialized1); - }); + describe('serialization', () => { + it('identity', () => { + const serialized0 = controller.serialize()! + controller.deserialize(serialized0) + const serialized1 = controller.serialize() + expect(serialized0).toEqual(serialized1) + }) - test("should restore existing configuration", () => { - const selectedConfiguration = "AR0BBKAPAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQICAQIDBNAHAAAEBKAPAAADC" + - "wECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBBQQEQAAAAA=="; + it('should restore existing configuration', () => { + const selectedConfiguration = 'AR0BBKAPAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQICAQIDBNAHAAAEBKAPAAADC' + + 'wECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBBQQEQAAAAA==' const data = JSON.parse(`{ "type": "camera", "controllerData": { @@ -324,40 +328,40 @@ describe("CameraController", () => { } } } - }`); + }`) - controller.deserialize(data.controllerData.data); + controller.deserialize(data.controllerData.data) expect(controller.streamManagements[0].service.getCharacteristic(Characteristic.Active).value) - .toBe(Characteristic.Active.ACTIVE); + .toBe(Characteristic.Active.ACTIVE) expect(controller.streamManagements[1].service.getCharacteristic(Characteristic.Active).value) - .toBe(Characteristic.Active.INACTIVE); + .toBe(Characteristic.Active.INACTIVE) // @ts-expect-error (private access) expect(controller.recordingManagement!.selectedConfiguration) - .toBeUndefined(); // the hash is from a different configuration, we expect to drop selected config when the supported configuration changes! + .toBeUndefined() // the hash is from a different configuration, we expect to drop selected config when the supported configuration changes! expect(controller.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.Active).value) - .toBe(Characteristic.Active.ACTIVE); + .toBe(Characteristic.Active.ACTIVE) expect(controller.recordingManagement?.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive).value) - .toBe(1); + .toBe(1) expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value) - .toBe(0); + .toBe(0) expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.PeriodicSnapshotsActive).value) - .toBe(0); + .toBe(0) expect(controller.recordingManagement?.operatingModeService.getCharacteristic(Characteristic.EventSnapshotsActive).value) - .toBe(0); - }); - }); - - describe("handleFactoryReset", () => { - test("default configuration", () => { - const serialized0 = controller.serialize(); - controller.handleFactoryReset(); - const serialized1 = controller.serialize(); - - expect(serialized0).toEqual(serialized1); - }); - }); -}); + .toBe(0) + }) + }) + + describe('handleFactoryReset', () => { + it('default configuration', () => { + const serialized0 = controller.serialize() + controller.handleFactoryReset() + const serialized1 = controller.serialize() + + expect(serialized0).toEqual(serialized1) + }) + }) +}) diff --git a/src/lib/controller/CameraController.ts b/src/lib/controller/CameraController.ts index f8124508e..d75355d0f 100644 --- a/src/lib/controller/CameraController.ts +++ b/src/lib/controller/CameraController.ts @@ -1,25 +1,22 @@ -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { CharacteristicValue, SessionIdentifier } from "../../types"; -import { +/* global NodeJS */ +import type { Buffer } from 'node:buffer' + +import type { CharacteristicValue, SessionIdentifier } from '../../types' +import type { CameraRecordingConfiguration, CameraRecordingOptions, CameraStreamingOptions, - EventTriggerOption, PrepareStreamRequest, PrepareStreamResponse, - RecordingManagement, RecordingManagementState, RecordingPacket, - RTPStreamManagement, RTPStreamManagementState, SnapshotRequest, StreamingRequest, -} from "../camera"; -import { Characteristic, CharacteristicEventTypes, CharacteristicGetCallback, CharacteristicSetCallback } from "../Characteristic"; -import { DataStreamManagement, HDSProtocolSpecificErrorReason } from "../datastream"; -import { +} from '../camera' +import type { CharacteristicGetCallback, CharacteristicSetCallback } from '../Characteristic' +import type { HDSProtocolSpecificErrorReason } from '../datastream' +import type { CameraOperatingMode, CameraRecordingManagement, DataStreamTransportManagement, @@ -28,13 +25,23 @@ import { MotionSensor, OccupancySensor, Speaker, -} from "../definitions"; -import { HAPStatus } from "../HAPServer"; -import { Service } from "../Service"; -import { HapStatusError } from "../util/hapStatusError"; -import { ControllerIdentifier, ControllerServiceMap, DefaultControllerType, SerializableController, StateChangeDelegate } from "./Controller"; +} from '../definitions' +import type { ControllerIdentifier, ControllerServiceMap, SerializableController, StateChangeDelegate } from './Controller' + +import { randomBytes } from 'node:crypto' +import { EventEmitter } from 'node:events' + +import createDebug from 'debug' -const debug = createDebug("HAP-NodeJS:Camera:Controller"); +import { EventTriggerOption, RecordingManagement, RTPStreamManagement } from '../camera/index.js' +import { Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { DataStreamManagement } from '../datastream/index.js' +import { HAPStatus } from '../HAPServer.js' +import { Service } from '../Service.js' +import { HapStatusError } from '../util/hapStatusError.js' +import { DefaultControllerType } from './Controller.js' + +const debug = createDebug('HAP-NodeJS:Camera:Controller') /** * @group Camera @@ -47,17 +54,17 @@ export interface CameraControllerOptions { * * Default value: 1 */ - cameraStreamCount?: number, + cameraStreamCount?: number /** * Delegate which handles the actual RTP/RTCP video/audio streaming and Snapshot requests. */ - delegate: CameraStreamingDelegate, + delegate: CameraStreamingDelegate /** * Options regarding video/audio streaming */ - streamingOptions: CameraStreamingOptions, + streamingOptions: CameraStreamingOptions /** * When supplying this option, it will enable support for HomeKit Secure Video. @@ -71,12 +78,12 @@ export interface CameraControllerOptions { /** * Options regarding Recordings (Secure Video) */ - options: CameraRecordingOptions, + options: CameraRecordingOptions /** - * Delegate which handles the audio/video recording data streaming on motion. - */ - delegate: CameraRecordingDelegate, + * Delegate which handles the audio/video recording data streaming on motion. + */ + delegate: CameraRecordingDelegate } /** @@ -101,7 +108,7 @@ export interface CameraControllerOptions { * If supplied, this sensor will be used as a {@link EventTriggerOption.MOTION} trigger. * The characteristic {@link Characteristic.StatusActive} will be added, which is used to enable or disable the sensor. */ - motion?: Service | boolean; + motion?: Service | boolean /** * Define if a {@link Service.OccupancySensor} should be created/associated with the controller. * @@ -111,26 +118,27 @@ export interface CameraControllerOptions { * * The characteristic {@link Characteristic.StatusActive} will be added, which is used to enable or disable the sensor. */ - occupancy?: Service | boolean; + occupancy?: Service | boolean } } /** * @group Camera */ -export type SnapshotRequestCallback = (error?: Error | HAPStatus, buffer?: Buffer) => void; +export type SnapshotRequestCallback = (error?: Error | HAPStatus, buffer?: Buffer) => void /** * @group Camera */ -export type PrepareStreamCallback = (error?: Error, response?: PrepareStreamResponse) => void; +export type PrepareStreamCallback = (error?: Error, response?: PrepareStreamResponse) => void /** * @group Camera */ -export type StreamRequestCallback = (error?: Error) => void; +export type StreamRequestCallback = (error?: Error) => void /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum ResourceRequestReason { /** * The reason describes periodic resource requests. @@ -141,7 +149,7 @@ export const enum ResourceRequestReason { * The resource request is the result of some event. * In the example of camera image snapshots, requests are made due to e.g. a motion event or similar. */ - EVENT = 1 + EVENT = 1, } /** @@ -159,17 +167,17 @@ export interface CameraStreamingDelegate { * @param request - Request containing image size. * @param callback - Callback supplied with the resulting Buffer */ - handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void; - - prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void; - handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void; - + /* eslint-disable ts/method-signature-style */ + handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void + prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void + handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void + /* eslint-enable ts/method-signature-style */ } /** * A `CameraRecordingDelegate` is responsible for handling recordings of a HomeKit Secure Video camera. * - * It is responsible for maintaining the prebuffer (see {@link CameraRecordingOptions.prebufferLength}, + * It is responsible for maintaining the prebuffer (see {@link CameraRecordingOptions.prebufferLength}), * once recording was activated (see {@link updateRecordingActive}). * * Before recording is considered enabled two things must happen: @@ -219,7 +227,8 @@ export interface CameraRecordingDelegate { * * @param active - Specifies if recording is active or not. */ - updateRecordingActive(active: boolean): void; + // eslint-disable-next-line ts/method-signature-style + updateRecordingActive(active: boolean): void /** * A call to this method signals that the selected (by the HomeKit Controller) @@ -234,11 +243,12 @@ export interface CameraRecordingDelegate { * currently running stream and only apply the updated configuration to the next stream. * * @param configuration - The {@link CameraRecordingConfiguration}. Reconfigure your recording pipeline accordingly. - * The parameter might be `undefined` when the selected configuration became invalid. This typically ony happens - * e.g. due to a factory reset (when all pairings are removed). Disable the recording pipeline in such a case - * even if recording is still enabled for the camera. + * The parameter might be `undefined` when the selected configuration became invalid. This typically ony happens + * e.g. due to a factory reset (when all pairings are removed). Disable the recording pipeline in such a case + * even if recording is still enabled for the camera. */ - updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void; + // eslint-disable-next-line ts/method-signature-style + updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void /** * This method is called to stream the next recording event. @@ -272,7 +282,7 @@ export interface CameraRecordingDelegate { * Once a close of stream is signaled, the `AsyncGenerator` function must return gracefully. * * For more information about `AsyncGenerator`s you might have a look at: - * * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of * * NOTE: HAP-NodeJS guarantees that this method is only called with a valid selected {@link CameraRecordingConfiguration}. * @@ -280,7 +290,8 @@ export interface CameraRecordingDelegate { * * @param streamId - The streamId of the currently ongoing stream. */ - handleRecordingStreamRequest(streamId: number): AsyncGenerator; + // eslint-disable-next-line ts/method-signature-style + handleRecordingStreamRequest(streamId: number): AsyncGenerator /** * This method is called once the HomeKit Controller acknowledges the `endOfStream`. @@ -289,7 +300,8 @@ export interface CameraRecordingDelegate { * * @param streamId - The streamId of the acknowledged stream. */ - acknowledgeStream?(streamId: number): void; + // eslint-disable-next-line ts/method-signature-style + acknowledgeStream?(streamId: number): void /** * This method is called to notify the delegate that a recording stream started via {@link handleRecordingStreamRequest} was closed. @@ -301,9 +313,10 @@ export interface CameraRecordingDelegate { * * @param streamId - The streamId for which the close event was sent. * @param reason - The reason with which the stream was closed. - * NOTE: This method is also called in case of a closed connection. This is encoded by setting the `reason` to undefined. + * NOTE: This method is also called in case of a closed connection. This is encoded by setting the `reason` to undefined. */ - closeRecordingStream(streamId: number, reason: HDSProtocolSpecificErrorReason | undefined): void; + // eslint-disable-next-line ts/method-signature-style + closeRecordingStream(streamId: number, reason: HDSProtocolSpecificErrorReason | undefined): void } /** @@ -312,56 +325,58 @@ export interface CameraRecordingDelegate { export interface CameraControllerServiceMap extends ControllerServiceMap { // "streamManagement%d": CameraRTPStreamManagement, // format to map all stream management services; indexed by zero - microphone?: Microphone, - speaker?: Speaker, + microphone?: Microphone + speaker?: Speaker - cameraEventRecordingManagement?: CameraRecordingManagement, - cameraOperatingMode?: CameraOperatingMode, - dataStreamTransportManagement?: DataStreamTransportManagement, + cameraEventRecordingManagement?: CameraRecordingManagement + cameraOperatingMode?: CameraOperatingMode + dataStreamTransportManagement?: DataStreamTransportManagement - motionService?: MotionSensor, - occupancyService?: OccupancySensor, + motionService?: MotionSensor + occupancyService?: OccupancySensor // this ServiceMap is also used by the DoorbellController; there is no necessity to declare it, // but I think its good practice to reserve the namespace - doorbell?: Doorbell; + doorbell?: Doorbell } /** * @group Camera */ export interface CameraControllerState { - streamManagements: RTPStreamManagementState[]; - recordingManagement?: RecordingManagementState; + streamManagements: RTPStreamManagementState[] + recordingManagement?: RecordingManagementState } /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum CameraControllerEvents { /** * Emitted when the mute state or the volume changed. The Apple Home App typically does not set those values * except the mute state. When you adjust the volume in the Camera view it will reset the muted state if it was set previously. * The value of volume has nothing to do with the volume slider in the Camera view of the Home app. */ - MICROPHONE_PROPERTIES_CHANGED = "microphone-change", + MICROPHONE_PROPERTIES_CHANGED = 'microphone-change', /** * Emitted when the mute state or the volume changed. The Apple Home App typically does not set those values * except the mute state. When you unmute the device microphone it will reset the mute state if it was set previously. */ - SPEAKER_PROPERTIES_CHANGED = "speaker-change", + SPEAKER_PROPERTIES_CHANGED = 'speaker-change', } /** * @group Camera */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface CameraController { - on(event: "microphone-change", listener: (muted: boolean, volume: number) => void): this; - on(event: "speaker-change", listener: (muted: boolean, volume: number) => void): this; - - emit(event: "microphone-change", muted: boolean, volume: number): boolean; - emit(event: "speaker-change", muted: boolean, volume: number): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'microphone-change', listener: (muted: boolean, volume: number) => void): this + on(event: 'speaker-change', listener: (muted: boolean, volume: number) => void): this + emit(event: 'microphone-change', muted: boolean, volume: number): boolean + emit(event: 'speaker-change', muted: boolean, volume: number): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -369,72 +384,74 @@ export declare interface CameraController { * * @group Camera */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class CameraController extends EventEmitter implements SerializableController { - private static readonly STREAM_MANAGEMENT = "streamManagement"; // key to index all RTPStreamManagement services + private static readonly STREAM_MANAGEMENT = 'streamManagement' // key to index all RTPStreamManagement services - private stateChangeDelegate?: StateChangeDelegate; + private stateChangeDelegate?: StateChangeDelegate - private readonly streamCount: number; - private readonly delegate: CameraStreamingDelegate; - private readonly streamingOptions: CameraStreamingOptions; + private readonly streamCount: number + private readonly delegate: CameraStreamingDelegate + private readonly streamingOptions: CameraStreamingOptions /** * **Temporary** storage for {@link CameraRecordingOptions} and {@link CameraRecordingDelegate}. * This property is reset to `undefined` after the CameraController was fully initialized. * You can still access those values via the {@link CameraController.recordingManagement}. */ private recording?: { - options: CameraRecordingOptions, - delegate: CameraRecordingDelegate, - }; + options: CameraRecordingOptions + delegate: CameraRecordingDelegate + } + /** * Temporary storage for the sensor option. */ private sensorOptions?: { - motion?: Service | boolean; - occupancy?: Service | boolean; - }; - private readonly legacyMode: boolean = false; + motion?: Service | boolean + occupancy?: Service | boolean + } + + private readonly legacyMode: boolean = false /** * @private */ - streamManagements: RTPStreamManagement[] = []; + streamManagements: RTPStreamManagement[] = [] /** * The {@link RecordingManagement} which is responsible for handling HomeKit Secure Video. * This property is only present if recording was configured. */ - recordingManagement?: RecordingManagement; + recordingManagement?: RecordingManagement - private microphoneService?: Microphone; - private speakerService?: Speaker; + private microphoneService?: Microphone + private speakerService?: Speaker - private microphoneMuted = false; - private microphoneVolume = 100; - private speakerMuted = false; - private speakerVolume = 100; + private microphoneMuted = false + private microphoneVolume = 100 + private speakerMuted = false + private speakerVolume = 100 - motionService?: MotionSensor; - private motionServiceExternallySupplied = false; - occupancyService?: OccupancySensor; - private occupancyServiceExternallySupplied = false; + motionService?: MotionSensor + private motionServiceExternallySupplied = false + occupancyService?: OccupancySensor + private occupancyServiceExternallySupplied = false constructor(options: CameraControllerOptions, legacyMode = false) { - super(); - this.streamCount = Math.max(1, options.cameraStreamCount || 1); - this.delegate = options.delegate; - this.streamingOptions = options.streamingOptions; - this.recording = options.recording; - this.sensorOptions = options.sensors; - - this.legacyMode = legacyMode; // legacy mode will prevent from Microphone and Speaker services to get created to avoid collisions + super() + this.streamCount = Math.max(1, options.cameraStreamCount || 1) + this.delegate = options.delegate + this.streamingOptions = options.streamingOptions + this.recording = options.recording + this.sensorOptions = options.sensors + + this.legacyMode = legacyMode // legacy mode will prevent from Microphone and Speaker services to get created to avoid collisions } /** * @private */ controllerId(): ControllerIdentifier { - return DefaultControllerType.CAMERA; + return DefaultControllerType.CAMERA } // ----------------------------------- STREAM API ------------------------------------ @@ -446,63 +463,63 @@ export class CameraController extends EventEmitter implements SerializableContro * @param sessionId - id of the current ongoing streaming session */ public forceStopStreamingSession(sessionId: SessionIdentifier): void { - this.streamManagements.forEach(management => { + this.streamManagements.forEach((management) => { if (management.sessionIdentifier === sessionId) { - management.forceStop(); + management.forceStop() } - }); + }) } public static generateSynchronisationSource(): number { - const ssrc = crypto.randomBytes(4); // range [-2.14748e+09 - 2.14748e+09] - ssrc[0] = 0; - return ssrc.readInt32BE(0); + const ssrc = randomBytes(4) // range [-2.14748e+09 - 2.14748e+09] + ssrc[0] = 0 + return ssrc.readInt32BE(0) } // ----------------------------- MICROPHONE/SPEAKER API ------------------------------ public setMicrophoneMuted(muted = true): void { if (!this.microphoneService) { - return; + return } - this.microphoneMuted = muted; - this.microphoneService.updateCharacteristic(Characteristic.Mute, muted); + this.microphoneMuted = muted + this.microphoneService.updateCharacteristic(Characteristic.Mute, muted) } public setMicrophoneVolume(volume: number): void { if (!this.microphoneService) { - return; + return } - this.microphoneVolume = volume; - this.microphoneService.updateCharacteristic(Characteristic.Volume, volume); + this.microphoneVolume = volume + this.microphoneService.updateCharacteristic(Characteristic.Volume, volume) } public setSpeakerMuted(muted = true): void { if (!this.speakerService) { - return; + return } - this.speakerMuted = muted; - this.speakerService.updateCharacteristic(Characteristic.Mute, muted); + this.speakerMuted = muted + this.speakerService.updateCharacteristic(Characteristic.Mute, muted) } public setSpeakerVolume(volume: number): void { if (!this.speakerService) { - return; + return } - this.speakerVolume = volume; - this.speakerService.updateCharacteristic(Characteristic.Volume, volume); + this.speakerVolume = volume + this.speakerService.updateCharacteristic(Characteristic.Volume, volume) } private emitMicrophoneChange() { - this.emit(CameraControllerEvents.MICROPHONE_PROPERTIES_CHANGED, this.microphoneMuted, this.microphoneVolume); + this.emit(CameraControllerEvents.MICROPHONE_PROPERTIES_CHANGED, this.microphoneMuted, this.microphoneVolume) } private emitSpeakerChange() { - this.emit(CameraControllerEvents.SPEAKER_PROPERTIES_CHANGED, this.speakerMuted, this.speakerVolume); + this.emit(CameraControllerEvents.SPEAKER_PROPERTIES_CHANGED, this.speakerMuted, this.speakerVolume) } // ----------------------------------------------------------------------------------- @@ -511,112 +528,109 @@ export class CameraController extends EventEmitter implements SerializableContro */ constructServices(): CameraControllerServiceMap { for (let i = 0; i < this.streamCount; i++) { - const rtp = new RTPStreamManagement(i, this.streamingOptions, this.delegate, undefined, this.rtpStreamManagementDisabledThroughOperatingMode.bind(this)); - this.streamManagements.push(rtp); + const rtp = new RTPStreamManagement(i, this.streamingOptions, this.delegate, undefined, this.rtpStreamManagementDisabledThroughOperatingMode.bind(this)) + this.streamManagements.push(rtp) } if (!this.legacyMode && this.streamingOptions.audio) { // In theory the Microphone Service is a necessity. In practice, it's not. lol. // So we just add it if the user wants to support audio - this.microphoneService = new Service.Microphone("", ""); - this.microphoneService.setCharacteristic(Characteristic.Volume, this.microphoneVolume); + this.microphoneService = new Service.Microphone('', '') + this.microphoneService.setCharacteristic(Characteristic.Volume, this.microphoneVolume) if (this.streamingOptions.audio.twoWayAudio) { - this.speakerService = new Service.Speaker("", ""); - this.speakerService.setCharacteristic(Characteristic.Volume, this.speakerVolume); + this.speakerService = new Service.Speaker('', '') + this.speakerService.setCharacteristic(Characteristic.Volume, this.speakerVolume) } } if (this.recording) { - this.recordingManagement = new RecordingManagement(this.recording.options, this.recording.delegate, this.retrieveEventTriggerOptions()); + this.recordingManagement = new RecordingManagement(this.recording.options, this.recording.delegate, this.retrieveEventTriggerOptions()) } - if (this.sensorOptions?.motion) { - if (typeof this.sensorOptions.motion === "boolean") { - this.motionService = new Service.MotionSensor("", ""); + if (typeof this.sensorOptions.motion === 'boolean') { + this.motionService = new Service.MotionSensor('', '') } else { - this.motionService = this.sensorOptions.motion; - this.motionServiceExternallySupplied = true; + this.motionService = this.sensorOptions.motion + this.motionServiceExternallySupplied = true } - this.motionService.setCharacteristic(Characteristic.StatusActive, true); - this.recordingManagement?.recordingManagementService.addLinkedService(this.motionService); + this.motionService.setCharacteristic(Characteristic.StatusActive, true) + this.recordingManagement?.recordingManagementService.addLinkedService(this.motionService) } if (this.sensorOptions?.occupancy) { - if (typeof this.sensorOptions.occupancy === "boolean") { - this.occupancyService = new Service.OccupancySensor("", ""); + if (typeof this.sensorOptions.occupancy === 'boolean') { + this.occupancyService = new Service.OccupancySensor('', '') } else { - this.occupancyService = this.sensorOptions.occupancy; - this.occupancyServiceExternallySupplied = true; + this.occupancyService = this.sensorOptions.occupancy + this.occupancyServiceExternallySupplied = true } - this.occupancyService.setCharacteristic(Characteristic.StatusActive, true); - this.recordingManagement?.recordingManagementService.addLinkedService(this.occupancyService); + this.occupancyService.setCharacteristic(Characteristic.StatusActive, true) + this.recordingManagement?.recordingManagementService.addLinkedService(this.occupancyService) } - const serviceMap: CameraControllerServiceMap = { microphone: this.microphoneService, speaker: this.speakerService, motionService: !this.motionServiceExternallySupplied ? this.motionService : undefined, occupancyService: !this.occupancyServiceExternallySupplied ? this.occupancyService : undefined, - }; + } if (this.recordingManagement) { - serviceMap.cameraEventRecordingManagement = this.recordingManagement.recordingManagementService; - serviceMap.cameraOperatingMode = this.recordingManagement.operatingModeService; - serviceMap.dataStreamTransportManagement = this.recordingManagement.dataStreamManagement.getService(); + serviceMap.cameraEventRecordingManagement = this.recordingManagement.recordingManagementService + serviceMap.cameraOperatingMode = this.recordingManagement.operatingModeService + serviceMap.dataStreamTransportManagement = this.recordingManagement.dataStreamManagement.getService() } this.streamManagements.forEach((management, index) => { - serviceMap[CameraController.STREAM_MANAGEMENT + index] = management.getService(); - }); + serviceMap[CameraController.STREAM_MANAGEMENT + index] = management.getService() + }) - this.recording = undefined; - this.sensorOptions = undefined; + this.recording = undefined + this.sensorOptions = undefined - return serviceMap; + return serviceMap } /** * @private */ initWithServices(serviceMap: CameraControllerServiceMap): void | CameraControllerServiceMap { - const result = this._initWithServices(serviceMap); + const result = this._initWithServices(serviceMap) if (result.updated) { // serviceMap must only be returned if anything actually changed - return result.serviceMap; + return result.serviceMap } } protected _initWithServices(serviceMap: CameraControllerServiceMap): { serviceMap: CameraControllerServiceMap, updated: boolean } { - let modifiedServiceMap = false; + let modifiedServiceMap = false - // eslint-disable-next-line no-constant-condition for (let i = 0; true; i++) { - const streamManagementService = serviceMap[CameraController.STREAM_MANAGEMENT + i]; + const streamManagementService = serviceMap[CameraController.STREAM_MANAGEMENT + i] if (i < this.streamCount) { - const operatingModeClosure = this.rtpStreamManagementDisabledThroughOperatingMode.bind(this); + const operatingModeClosure = this.rtpStreamManagementDisabledThroughOperatingMode.bind(this) if (streamManagementService) { // normal init - this.streamManagements.push(new RTPStreamManagement(i, this.streamingOptions, this.delegate, streamManagementService, operatingModeClosure)); + this.streamManagements.push(new RTPStreamManagement(i, this.streamingOptions, this.delegate, streamManagementService, operatingModeClosure)) } else { // stream count got bigger, we need to create a new service - const management = new RTPStreamManagement(i, this.streamingOptions, this.delegate, undefined, operatingModeClosure); + const management = new RTPStreamManagement(i, this.streamingOptions, this.delegate, undefined, operatingModeClosure) - this.streamManagements.push(management); - serviceMap[CameraController.STREAM_MANAGEMENT + i] = management.getService(); + this.streamManagements.push(management) + serviceMap[CameraController.STREAM_MANAGEMENT + i] = management.getService() - modifiedServiceMap = true; + modifiedServiceMap = true } } else { if (streamManagementService) { // stream count got reduced, we need to remove old service - delete serviceMap[CameraController.STREAM_MANAGEMENT + i]; - modifiedServiceMap = true; + delete serviceMap[CameraController.STREAM_MANAGEMENT + i] + modifiedServiceMap = true } else { - break; // we finished counting, and we got no saved service; we are finished + break // we finished counting, and we got no saved service; we are finished } } } @@ -624,42 +638,42 @@ export class CameraController extends EventEmitter implements SerializableContro // MICROPHONE if (!this.legacyMode && this.streamingOptions.audio) { // microphone should be present if (serviceMap.microphone) { - this.microphoneService = serviceMap.microphone; + this.microphoneService = serviceMap.microphone } else { // microphone wasn't created yet => create a new one - this.microphoneService = new Service.Microphone("", ""); - this.microphoneService.setCharacteristic(Characteristic.Volume, this.microphoneVolume); + this.microphoneService = new Service.Microphone('', '') + this.microphoneService.setCharacteristic(Characteristic.Volume, this.microphoneVolume) - serviceMap.microphone = this.microphoneService; - modifiedServiceMap = true; + serviceMap.microphone = this.microphoneService + modifiedServiceMap = true } } else if (serviceMap.microphone) { // microphone service supplied, though settings seemed to have changed // we need to remove it - delete serviceMap.microphone; - modifiedServiceMap = true; + delete serviceMap.microphone + modifiedServiceMap = true } // SPEAKER if (!this.legacyMode && this.streamingOptions.audio?.twoWayAudio) { // speaker should be present if (serviceMap.speaker) { - this.speakerService = serviceMap.speaker; + this.speakerService = serviceMap.speaker } else { // speaker wasn't created yet => create a new one - this.speakerService = new Service.Speaker("", ""); - this.speakerService.setCharacteristic(Characteristic.Volume, this.speakerVolume); + this.speakerService = new Service.Speaker('', '') + this.speakerService.setCharacteristic(Characteristic.Volume, this.speakerVolume) - serviceMap.speaker = this.speakerService; - modifiedServiceMap = true; + serviceMap.speaker = this.speakerService + modifiedServiceMap = true } } else if (serviceMap.speaker) { // speaker service supplied, though settings seemed to have changed // we need to remove it - delete serviceMap.speaker; - modifiedServiceMap = true; + delete serviceMap.speaker + modifiedServiceMap = true } // RECORDING if (this.recording) { - const eventTriggers = this.retrieveEventTriggerOptions(); + const eventTriggers = this.retrieveEventTriggerOptions() // RECORDING MANAGEMENT if (serviceMap.cameraEventRecordingManagement && serviceMap.cameraOperatingMode && serviceMap.dataStreamTransportManagement) { @@ -672,139 +686,139 @@ export class CameraController extends EventEmitter implements SerializableContro operatingMode: serviceMap.cameraOperatingMode, dataStreamManagement: new DataStreamManagement(serviceMap.dataStreamTransportManagement), }, - ); + ) } else { this.recordingManagement = new RecordingManagement( this.recording.options, this.recording.delegate, eventTriggers, - ); + ) - serviceMap.cameraEventRecordingManagement = this.recordingManagement.recordingManagementService; - serviceMap.cameraOperatingMode = this.recordingManagement.operatingModeService; - serviceMap.dataStreamTransportManagement = this.recordingManagement.dataStreamManagement.getService(); - modifiedServiceMap = true; + serviceMap.cameraEventRecordingManagement = this.recordingManagement.recordingManagementService + serviceMap.cameraOperatingMode = this.recordingManagement.operatingModeService + serviceMap.dataStreamTransportManagement = this.recordingManagement.dataStreamManagement.getService() + modifiedServiceMap = true } } else { if (serviceMap.cameraEventRecordingManagement) { - delete serviceMap.cameraEventRecordingManagement; - modifiedServiceMap = true; + delete serviceMap.cameraEventRecordingManagement + modifiedServiceMap = true } if (serviceMap.cameraOperatingMode) { - delete serviceMap.cameraOperatingMode; - modifiedServiceMap = true; + delete serviceMap.cameraOperatingMode + modifiedServiceMap = true } if (serviceMap.dataStreamTransportManagement) { - delete serviceMap.dataStreamTransportManagement; - modifiedServiceMap = true; + delete serviceMap.dataStreamTransportManagement + modifiedServiceMap = true } } // MOTION SENSOR if (this.sensorOptions?.motion) { - if (typeof this.sensorOptions.motion === "boolean") { + if (typeof this.sensorOptions.motion === 'boolean') { if (serviceMap.motionService) { - this.motionService = serviceMap.motionService; + this.motionService = serviceMap.motionService } else { // it could be the case that we previously had a manually supplied motion service // at this point we can't remove the iid from the list of linked services from the recording management! - this.motionService = new Service.MotionSensor("", ""); + this.motionService = new Service.MotionSensor('', '') } } else { - this.motionService = this.sensorOptions.motion; - this.motionServiceExternallySupplied = true; + this.motionService = this.sensorOptions.motion + this.motionServiceExternallySupplied = true if (serviceMap.motionService) { // motion service previously supplied as bool option - this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.motionService); - delete serviceMap.motionService; - modifiedServiceMap = true; + this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.motionService) + delete serviceMap.motionService + modifiedServiceMap = true } } - this.motionService.setCharacteristic(Characteristic.StatusActive, true); - this.recordingManagement?.recordingManagementService.addLinkedService(this.motionService); + this.motionService.setCharacteristic(Characteristic.StatusActive, true) + this.recordingManagement?.recordingManagementService.addLinkedService(this.motionService) } else { if (serviceMap.motionService) { - this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.motionService); - delete serviceMap.motionService; - modifiedServiceMap = true; + this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.motionService) + delete serviceMap.motionService + modifiedServiceMap = true } } // OCCUPANCY SENSOR if (this.sensorOptions?.occupancy) { - if (typeof this.sensorOptions.occupancy === "boolean") { + if (typeof this.sensorOptions.occupancy === 'boolean') { if (serviceMap.occupancyService) { - this.occupancyService = serviceMap.occupancyService; + this.occupancyService = serviceMap.occupancyService } else { // it could be the case that we previously had a manually supplied occupancy service // at this point we can't remove the iid from the list of linked services from the recording management! - this.occupancyService = new Service.OccupancySensor("", ""); + this.occupancyService = new Service.OccupancySensor('', '') } } else { - this.occupancyService = this.sensorOptions.occupancy; - this.occupancyServiceExternallySupplied = true; + this.occupancyService = this.sensorOptions.occupancy + this.occupancyServiceExternallySupplied = true if (serviceMap.occupancyService) { // occupancy service previously supplied as bool option - this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.occupancyService); - delete serviceMap.occupancyService; - modifiedServiceMap = true; + this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.occupancyService) + delete serviceMap.occupancyService + modifiedServiceMap = true } } - this.occupancyService.setCharacteristic(Characteristic.StatusActive, true); - this.recordingManagement?.recordingManagementService.addLinkedService(this.occupancyService); + this.occupancyService.setCharacteristic(Characteristic.StatusActive, true) + this.recordingManagement?.recordingManagementService.addLinkedService(this.occupancyService) } else { if (serviceMap.occupancyService) { - this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.occupancyService); - delete serviceMap.occupancyService; - modifiedServiceMap = true; + this.recordingManagement?.recordingManagementService.removeLinkedService(serviceMap.occupancyService) + delete serviceMap.occupancyService + modifiedServiceMap = true } } if (this.migrateFromDoorbell(serviceMap)) { - modifiedServiceMap = true; + modifiedServiceMap = true } - this.recording = undefined; - this.sensorOptions = undefined; + this.recording = undefined + this.sensorOptions = undefined return { - serviceMap: serviceMap, + serviceMap, updated: modifiedServiceMap, - }; + } } // overwritten in DoorbellController (to avoid cyclic dependencies, I hate typescript for that) protected migrateFromDoorbell(serviceMap: ControllerServiceMap): boolean { if (serviceMap.doorbell) { // See NOTICE in DoorbellController - delete serviceMap.doorbell; - return true; + delete serviceMap.doorbell + return true } - return false; + return false } protected retrieveEventTriggerOptions(): Set { if (!this.recording) { - return new Set(); + return new Set() } - const triggerOptions = new Set(); + const triggerOptions = new Set() if (this.recording.options.overrideEventTriggerOptions) { for (const option of this.recording.options.overrideEventTriggerOptions) { - triggerOptions.add(option); + triggerOptions.add(option) } } if (this.sensorOptions?.motion) { - triggerOptions.add(EventTriggerOption.MOTION); + triggerOptions.add(EventTriggerOption.MOTION) } // this method is overwritten by the `DoorbellController` to automatically configure EventTriggerOption.DOORBELL - return triggerOptions; + return triggerOptions } /** @@ -814,111 +828,110 @@ export class CameraController extends EventEmitter implements SerializableContro if (this.microphoneService) { this.microphoneService.getCharacteristic(Characteristic.Mute)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(undefined, this.microphoneMuted); + callback(undefined, this.microphoneMuted) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - this.microphoneMuted = value as boolean; - callback(); - this.emitMicrophoneChange(); - }); + this.microphoneMuted = value as boolean + callback() + this.emitMicrophoneChange() + }) this.microphoneService.getCharacteristic(Characteristic.Volume)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(undefined, this.microphoneVolume); + callback(undefined, this.microphoneVolume) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - this.microphoneVolume = value as number; - callback(); - this.emitMicrophoneChange(); - }); + this.microphoneVolume = value as number + callback() + this.emitMicrophoneChange() + }) } if (this.speakerService) { this.speakerService.getCharacteristic(Characteristic.Mute)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(undefined, this.speakerMuted); + callback(undefined, this.speakerMuted) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - this.speakerMuted = value as boolean; - callback(); - this.emitSpeakerChange(); - }); + this.speakerMuted = value as boolean + callback() + this.emitSpeakerChange() + }) this.speakerService.getCharacteristic(Characteristic.Volume)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(undefined, this.speakerVolume); + callback(undefined, this.speakerVolume) }) .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - this.speakerVolume = value as number; - callback(); - this.emitSpeakerChange(); - }); + this.speakerVolume = value as number + callback() + this.emitSpeakerChange() + }) } // make the sensor services available to the RecordingManagement. if (this.motionService) { - this.recordingManagement?.sensorServices.push(this.motionService); + this.recordingManagement?.sensorServices.push(this.motionService) } if (this.occupancyService) { - this.recordingManagement?.sensorServices.push(this.occupancyService); + this.recordingManagement?.sensorServices.push(this.occupancyService) } } private rtpStreamManagementDisabledThroughOperatingMode(): boolean { return this.recordingManagement ? !this.recordingManagement.operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value - : false; + : false } - /** * @private */ handleControllerRemoved(): void { - this.handleFactoryReset(); + this.handleFactoryReset() for (const management of this.streamManagements) { - management.destroy(); + management.destroy() } - this.streamManagements.splice(0, this.streamManagements.length); + this.streamManagements.splice(0, this.streamManagements.length) - this.microphoneService = undefined; - this.speakerService = undefined; + this.microphoneService = undefined + this.speakerService = undefined - this.recordingManagement?.destroy(); - this.recordingManagement = undefined; + this.recordingManagement?.destroy() + this.recordingManagement = undefined - this.removeAllListeners(); + this.removeAllListeners() } /** * @private */ handleFactoryReset(): void { - this.streamManagements.forEach(management => management.handleFactoryReset()); - this.recordingManagement?.handleFactoryReset(); + this.streamManagements.forEach(management => management.handleFactoryReset()) + this.recordingManagement?.handleFactoryReset() - this.microphoneMuted = false; - this.microphoneVolume = 100; - this.speakerMuted = false; - this.speakerVolume = 100; + this.microphoneMuted = false + this.microphoneVolume = 100 + this.speakerMuted = false + this.speakerVolume = 100 } /** * @private */ serialize(): CameraControllerState | undefined { - const streamManagementStates: RTPStreamManagementState[] = []; + const streamManagementStates: RTPStreamManagementState[] = [] for (const management of this.streamManagements) { - const serializedState = management.serialize(); + const serializedState = management.serialize() if (serializedState) { - streamManagementStates.push(serializedState); + streamManagementStates.push(serializedState) } } return { streamManagements: streamManagementStates, recordingManagement: this.recordingManagement?.serialize(), - }; + } } /** @@ -926,22 +939,22 @@ export class CameraController extends EventEmitter implements SerializableContro */ deserialize(serialized: CameraControllerState): void { for (const streamManagementState of serialized.streamManagements) { - const streamManagement = this.streamManagements[streamManagementState.id]; + const streamManagement = this.streamManagements[streamManagementState.id] if (streamManagement) { - streamManagement.deserialize(streamManagementState); + streamManagement.deserialize(streamManagementState) } } if (serialized.recordingManagement) { if (this.recordingManagement) { - this.recordingManagement.deserialize(serialized.recordingManagement); + this.recordingManagement.deserialize(serialized.recordingManagement) } else { // Active characteristic cannot be controlled if removing HSV, ensure they are all active! for (const streamManagement of this.streamManagements) { - streamManagement.service.updateCharacteristic(Characteristic.Active, true); + streamManagement.service.updateCharacteristic(Characteristic.Active, true) } - this.stateChangeDelegate?.(); + this.stateChangeDelegate?.() } } } @@ -950,13 +963,13 @@ export class CameraController extends EventEmitter implements SerializableContro * @private */ setupStateChangeDelegate(delegate?: StateChangeDelegate): void { - this.stateChangeDelegate = delegate; + this.stateChangeDelegate = delegate for (const streamManagement of this.streamManagements) { - streamManagement.setupStateChangeDelegate(delegate); + streamManagement.setupStateChangeDelegate(delegate) } - this.recordingManagement?.setupStateChangeDelegate(delegate); + this.recordingManagement?.setupStateChangeDelegate(delegate) } /** @@ -966,42 +979,42 @@ export class CameraController extends EventEmitter implements SerializableContro // first step is to verify that the reason is applicable to our current policy const streamingDisabled = this.streamManagements .map(management => !management.getService().getCharacteristic(Characteristic.Active).value) - .reduce((previousValue, currentValue) => previousValue && currentValue); + .reduce((previousValue, currentValue) => previousValue && currentValue) if (streamingDisabled) { - debug("[%s] Rejecting snapshot as streaming is disabled.", accessoryName); - return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + debug('[%s] Rejecting snapshot as streaming is disabled.', accessoryName) + return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } if (this.recordingManagement) { - const operatingModeService = this.recordingManagement.operatingModeService; + const operatingModeService = this.recordingManagement.operatingModeService if (!operatingModeService.getCharacteristic(Characteristic.HomeKitCameraActive).value) { - debug("[%s] Rejecting snapshot as HomeKit camera is disabled.", accessoryName); - return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + debug('[%s] Rejecting snapshot as HomeKit camera is disabled.', accessoryName) + return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } const eventSnapshotsActive = operatingModeService .getCharacteristic(Characteristic.EventSnapshotsActive) - .value; + .value if (!eventSnapshotsActive) { if (reason == null) { - debug("[%s] Rejecting snapshot as reason is required due to disabled event snapshots.", accessoryName); - return Promise.reject(HAPStatus.INSUFFICIENT_PRIVILEGES); + debug('[%s] Rejecting snapshot as reason is required due to disabled event snapshots.', accessoryName) + return Promise.reject(HAPStatus.INSUFFICIENT_PRIVILEGES) } else if (reason === ResourceRequestReason.EVENT) { - debug("[%s] Rejecting snapshot as even snapshots are disabled.", accessoryName); - return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + debug('[%s] Rejecting snapshot as even snapshots are disabled.', accessoryName) + return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } } const periodicSnapshotsActive = operatingModeService .getCharacteristic(Characteristic.PeriodicSnapshotsActive) - .value; + .value if (!periodicSnapshotsActive) { if (reason == null) { - debug("[%s] Rejecting snapshot as reason is required due to disabled periodic snapshots.", accessoryName); - return Promise.reject(HAPStatus.INSUFFICIENT_PRIVILEGES); + debug('[%s] Rejecting snapshot as reason is required due to disabled periodic snapshots.', accessoryName) + return Promise.reject(HAPStatus.INSUFFICIENT_PRIVILEGES) } else if (reason === ResourceRequestReason.PERIODIC) { - debug("[%s] Rejecting snapshot as periodic snapshots are disabled.", accessoryName); - return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE); + debug('[%s] Rejecting snapshot as periodic snapshots are disabled.', accessoryName) + return Promise.reject(HAPStatus.NOT_ALLOWED_IN_CURRENT_STATE) } } } @@ -1012,62 +1025,62 @@ export class CameraController extends EventEmitter implements SerializableContro let timeout: NodeJS.Timeout | undefined = setTimeout(() => { console.warn( `[${accessoryName}] The image snapshot handler for the given accessory is slow to respond! See https://homebridge.io/w/JtMGR for more info.`, - ); + ) timeout = setTimeout(() => { - timeout = undefined; + timeout = undefined console.warn( `[${accessoryName}] The image snapshot handler for the given accessory didn't respond at all! See https://homebridge.io/w/JtMGR for more info.`, - ); + ) - reject(HAPStatus.OPERATION_TIMED_OUT); - }, 17000); - timeout.unref(); - }, 8000); - timeout.unref(); + reject(HAPStatus.OPERATION_TIMED_OUT) + }, 17000) + timeout.unref() + }, 8000) + timeout.unref() try { this.delegate.handleSnapshotRequest({ - height: height, - width: width, - reason: reason, + height, + width, + reason, }, (error, buffer) => { if (!timeout) { - return; + return } else { - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout) + timeout = undefined } if (error) { - if (typeof error === "number") { - reject(error); + if (typeof error === 'number') { + reject(error) } else { - debug("[%s] Error getting snapshot: %s", accessoryName, error.stack); - reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + debug('[%s] Error getting snapshot: %s', accessoryName, error.stack) + reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - return; + return } if (!buffer || buffer.length === 0) { - console.warn(`[${accessoryName}] Snapshot request handler provided empty image buffer!`); - reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + console.warn(`[${accessoryName}] Snapshot request handler provided empty image buffer!`) + reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE) } else { - resolve(buffer); + resolve(buffer) } - }); + }) } catch (error) { if (!timeout) { - return; + return } else { - clearTimeout(timeout); - timeout = undefined; + clearTimeout(timeout) + timeout = undefined } - console.warn(`[${accessoryName}] Unhandled error thrown inside snapshot request handler: ${error.stack}`); - reject(error instanceof HapStatusError ? error.hapStatus : HAPStatus.SERVICE_COMMUNICATION_FAILURE); + console.warn(`[${accessoryName}] Unhandled error thrown inside snapshot request handler: ${error.stack}`) + reject(error instanceof HapStatusError ? error.hapStatus : HAPStatus.SERVICE_COMMUNICATION_FAILURE) } - }); + }) } } diff --git a/src/lib/controller/Controller.ts b/src/lib/controller/Controller.ts index af186cbb6..34149536d 100644 --- a/src/lib/controller/Controller.ts +++ b/src/lib/controller/Controller.ts @@ -1,4 +1,4 @@ -import { Service } from "../Service"; +import type { Service } from '../Service' /** * A ControllerServiceMap represents all services used by a Controller. @@ -7,7 +7,7 @@ import { Service } from "../Service"; * @group Controller API */ export interface ControllerServiceMap { - [name: string]: Service | undefined, + [name: string]: Service | undefined } /** @@ -18,39 +18,40 @@ export interface ControllerServiceMap { * You can define custom ControllerTypes if you wish to, but be careful that it does not collide with any known definitions. * @group Controller API */ -export type ControllerType = string | DefaultControllerType; +export type ControllerType = string | DefaultControllerType /** * @group Controller API */ +// eslint-disable-next-line no-restricted-syntax export const enum DefaultControllerType { - CAMERA = "camera", // or doorbell - REMOTE = "remote", - TV = "tv", - ROUTER = "router", - LOCK = "lock", - CHARACTERISTIC_TRANSITION = "characteristic-transition", + CAMERA = 'camera', // or doorbell + REMOTE = 'remote', + TV = 'tv', + ROUTER = 'router', + LOCK = 'lock', + CHARACTERISTIC_TRANSITION = 'characteristic-transition', } /** * @group Controller API */ -export type ControllerIdentifier = string | ControllerType; +export type ControllerIdentifier = string | ControllerType /** * @group Controller API */ -export type StateChangeDelegate = () => void; +export type StateChangeDelegate = () => void /** * @group Controller API */ export interface ControllerConstructor { - new(): Controller; + new(): Controller } /** - * A Controller represents a somewhat more complex arrangement of multiple services which together form a accessory + * A Controller represents a somewhat more complex arrangement of multiple services which together form an accessory * like for example cameras, remotes, tvs or routers. * Controllers implementing this interface are capable of being serialized and thus stored on and recreated from disk. * Meaning services, characteristic configurations and optionally additional controller states can be persistently saved. @@ -64,85 +65,90 @@ export interface ControllerConstructor { * The constructor of a Controller should only initialize controller specific configuration and states * and MUST NOT create any services or characteristics. * Additionally, it must implement all necessary methods as noted below. Those methods will get called - * when the accessory gets added to an Accessory or a Accessory is restored from disk. + * when the accessory gets added to an Accessory or an Accessory is restored from disk. * @group Controller API */ export interface Controller { - /** - * Every instance of a Controller must define appropriate identifying material. - * The returned identifier MUST NOT change over the lifetime of the Controller object. - * - * Note: The controller can choose to return the same identifier for all controllers of the same type. - * This will result in the user only being able to add ONE instance of an Controller to an accessory. - * - * Some predefined identifiers can be found in {@link ControllerIdentifier}. - */ - controllerId(): ControllerIdentifier; - - /** - * This method is called by the accessory the controller is added to. This method is only called if a new controller - * is constructed (aka the controller is not restored from disk {@link initWithServices}). - * It MUST create all needed services and characteristics. - * It MAY create links between services or mark them as hidden or primary. - * It MUST NOT configure any event handlers. - * The controller SHOULD save created services in internal properties for later access. - * - * The method must return all created services in a ServiceMap. - * A {@link ControllerServiceMap} basically maps a name to every service on the controller. - * It is used to potentially recreate a controller for a given ServiceMap using {@link initWithServices}. - * - * The set of services represented by the Controller MUST remain static and can only change over new version of - * the Controller implementation (see {@link initWithServices}) - * - * @returns a {@link ControllerServiceMap} representing all services of a controller indexed by a controller chosen name. - */ - constructServices(): M; - - /** - * This method is called to initialize the controller with already created services. - * The controller SHOULD save the passed services in internal properties for later access. - * - * The controller can return a ServiceMap to signal that the set of services changed. - * A Controller MUST modify the ServiceMap which is passed to the method and MUST NOT create a new one (to support inheritance). - * It MUST NOT return a ServiceMap if the service configuration did not change! - * It MUST be able to restore services using a ServiceMap from any point in time. - * - * @param serviceMap - A {@link ControllerServiceMap} that represents all services of a controller indexed by the controller chosen name. - * @returns optionally a {@link ControllerServiceMap}. This can be used to alter the services configuration of a controller. - */ - initWithServices(serviceMap: M): M | void; - - /** - * This method is called to configure the services and their characteristics of the controller. - * When this method is called, it is guaranteed that either {@link constructServices} or {@link initWithServices} - * were called before and all services are already created. - * - * This method SHOULD set up all necessary event handlers for services and characteristics. - */ - configureServices(): void; - - /** - * This method is called once the Controller is removed from the accessory. - * The controller MUST reset everything to its initial state (just as it would have been constructed freshly) - * form the constructor. - * Adding the Controller back to an accessory after it was removed MUST be supported! - * If the controller is a {@link SerializableController} it MUST NOT call the {@link StateChangeDelegate} - * as a result of a call to this method. - * - * All service contained in the {@link ControllerServiceMap} returned by {@link constructServices} - * will be automatically removed from the Accessory. The Controller MUST remove any references to those services. - */ - handleControllerRemoved(): void; - - /** - * This method is called to signal a factory reset of the controller and its services and characteristics. - * A controller MUST reset any configuration or states to default values. - * - * This method is called once the accessory gets unpaired or the Controller gets removed from the Accessory. - */ - handleFactoryReset?(): void; - + /** + * Every instance of a Controller must define appropriate identifying material. + * The returned identifier MUST NOT change over the lifetime of the Controller object. + * + * Note: The controller can choose to return the same identifier for all controllers of the same type. + * This will result in the user only being able to add ONE instance of a Controller to an accessory. + * + * Some predefined identifiers can be found in {@link ControllerIdentifier}. + */ + // eslint-disable-next-line ts/method-signature-style + controllerId(): ControllerIdentifier + + /** + * This method is called by the accessory the controller is added to. This method is only called if a new controller + * is constructed (aka the controller is not restored from disk {@link initWithServices}). + * It MUST create all needed services and characteristics. + * It MAY create links between services or mark them as hidden or primary. + * It MUST NOT configure any event handlers. + * The controller SHOULD save created services in internal properties for later access. + * + * The method must return all created services in a ServiceMap. + * A {@link ControllerServiceMap} basically maps a name to every service on the controller. + * It is used to potentially recreate a controller for a given ServiceMap using {@link initWithServices}. + * + * The set of services represented by the Controller MUST remain static and can only change over new version of + * the Controller implementation (see {@link initWithServices}) + * + * @returns a {@link ControllerServiceMap} representing all services of a controller indexed by a controller chosen name. + */ + // eslint-disable-next-line ts/method-signature-style + constructServices(): M + + /** + * This method is called to initialize the controller with already created services. + * The controller SHOULD save the passed services in internal properties for later access. + * + * The controller can return a ServiceMap to signal that the set of services changed. + * A Controller MUST modify the ServiceMap which is passed to the method and MUST NOT create a new one (to support inheritance). + * It MUST NOT return a ServiceMap if the service configuration did not change! + * It MUST be able to restore services using a ServiceMap from any point in time. + * + * @param serviceMap - A {@link ControllerServiceMap} that represents all services of a controller indexed by the controller chosen name. + * @returns optionally a {@link ControllerServiceMap}. This can be used to alter the services configuration of a controller. + */ + // eslint-disable-next-line ts/method-signature-style + initWithServices(serviceMap: M): M | void + + /** + * This method is called to configure the services and their characteristics of the controller. + * When this method is called, it is guaranteed that either {@link constructServices} or {@link initWithServices} + * were called before and all services are already created. + * + * This method SHOULD set up all necessary event handlers for services and characteristics. + */ + // eslint-disable-next-line ts/method-signature-style + configureServices(): void + + /** + * This method is called once the Controller is removed from the accessory. + * The controller MUST reset everything to its initial state (just as it would have been constructed freshly) + * form the constructor. + * Adding the Controller back to an accessory after it was removed MUST be supported! + * If the controller is a {@link SerializableController} it MUST NOT call the {@link StateChangeDelegate} + * as a result of a call to this method. + * + * All service contained in the {@link ControllerServiceMap} returned by {@link constructServices} + * will be automatically removed from the Accessory. The Controller MUST remove any references to those services. + */ + // eslint-disable-next-line ts/method-signature-style + handleControllerRemoved(): void + + /** + * This method is called to signal a factory reset of the controller and its services and characteristics. + * A controller MUST reset any configuration or states to default values. + * + * This method is called once the accessory gets unpaired or the Controller gets removed from the Accessory. + */ + // eslint-disable-next-line ts/method-signature-style + handleFactoryReset?(): void } /** @@ -150,41 +156,44 @@ export interface Controller extends Controller { - /** - * This method can be used to persistently save controller related configuration across reboots. - * It should return undefined, if the controller data was reset to default values and nothing needs to be stored anymore. - * - * @returns an arbitrary Controller defined object containing all necessary data - */ - serialize(): S | undefined; - - /** - * This method is called to restore the controller state from disk. - * This is only called once, when the data was loaded from disk and the Accessory is to be published. - * A controller MUST provide backwards compatibility for any configuration layout exposed at any time. - * A Controller MUST NOT depend on any specific calling order. - * - * @param serialized - */ - deserialize(serialized: S): void; - - /** - * This method is inherited from {@link Controller.handleFactoryReset} though is required with {@link SerializableController}. - */ - handleFactoryReset(): void; - - /** - * This method is called once upon setup. It supplies a function used by the Controller to signal state changes. - * The implementing controller SHOULD store the function and call it every time the internal controller state changes. - * It should be expected that the {@link serialize} method will be called next and that the state will be stored - * to disk. - * The delegate parameter can be undefined when the controller is removed and the state change delegate is reset. - * - * @param delegate - The {@link StateChangeDelegate} to call when controller state has changed - */ - setupStateChangeDelegate(delegate?: StateChangeDelegate): void; + /** + * This method can be used to persistently save controller related configuration across reboots. + * It should return undefined, if the controller data was reset to default values and nothing needs to be stored anymore. + * + * @returns an arbitrary Controller defined object containing all necessary data + */ + // eslint-disable-next-line ts/method-signature-style + serialize(): S | undefined + + /** + * This method is called to restore the controller state from disk. + * This is only called once, when the data was loaded from disk and the Accessory is to be published. + * A controller MUST provide backwards compatibility for any configuration layout exposed at any time. + * A Controller MUST NOT depend on any specific calling order. + * + * @param serialized + */ + // eslint-disable-next-line ts/method-signature-style + deserialize(serialized: S): void + + /** + * This method is inherited from {@link Controller.handleFactoryReset} though is required with {@link SerializableController}. + */ + // eslint-disable-next-line ts/method-signature-style + handleFactoryReset(): void + + /** + * This method is called once upon setup. It supplies a function used by the Controller to signal state changes. + * The implementing controller SHOULD store the function and call it every time the internal controller state changes. + * It should be expected that the {@link serialize} method will be called next and that the state will be stored + * to disk. + * The delegate parameter can be undefined when the controller is removed and the state change delegate is reset. + * + * @param delegate - The {@link StateChangeDelegate} to call when controller state has changed + */ + // eslint-disable-next-line ts/method-signature-style + setupStateChangeDelegate(delegate?: StateChangeDelegate): void } /** @@ -192,5 +201,5 @@ export interface SerializableController { - test("event trigger options", () => { - const controller = new DoorbellController(createCameraControllerOptions()); +import { EventTriggerOption } from '../camera/index.js' +import { createCameraControllerOptions } from './CameraController.spec.js' +import { DoorbellController } from './DoorbellController.js' + +describe('doorbellController', () => { + it('event trigger options', () => { + const controller = new DoorbellController(createCameraControllerOptions()) // @ts-expect-error: protected access - const options = controller.retrieveEventTriggerOptions(); - expect(new Array(...options).sort()).toEqual([EventTriggerOption.MOTION, EventTriggerOption.DOORBELL]); + const options = controller.retrieveEventTriggerOptions() + expect([...options].sort()).toEqual([EventTriggerOption.MOTION, EventTriggerOption.DOORBELL]) - controller.constructServices(); - controller.configureServices(); + controller.constructServices() + controller.configureServices() - expect(controller.recordingManagement).toBeTruthy(); + expect(controller.recordingManagement).toBeTruthy() // @ts-expect-error: private access - expect(controller.recordingManagement.eventTriggerOptions).toEqual(3); - }); -}); + expect(controller.recordingManagement.eventTriggerOptions).toEqual(3) + }) +}) diff --git a/src/lib/controller/DoorbellController.ts b/src/lib/controller/DoorbellController.ts index e7f7a20fa..df68e5a6d 100644 --- a/src/lib/controller/DoorbellController.ts +++ b/src/lib/controller/DoorbellController.ts @@ -1,9 +1,11 @@ -import { EventTriggerOption } from "../camera"; -import { Characteristic } from "../Characteristic"; -import type { Doorbell } from "../definitions"; -import { Service } from "../Service"; -import { CameraController, CameraControllerOptions, CameraControllerServiceMap } from "./CameraController"; -import { ControllerServiceMap } from "./Controller"; +import type { Doorbell } from '../definitions' +import type { CameraControllerOptions, CameraControllerServiceMap } from './CameraController' +import type { ControllerServiceMap } from './Controller' + +import { EventTriggerOption } from '../camera/index.js' +import { Characteristic } from '../Characteristic.js' +import { Service } from '../Service.js' +import { CameraController } from './CameraController.js' /** * Options which are additionally supplied for a {@link DoorbellController}. @@ -14,7 +16,7 @@ export interface DoorbellOptions { /** * Name used to for the {@link Service.Doorbell} service. */ - name?: string; + name?: string /** * This property may be used to supply an external {@link Service.Doorbell}. @@ -39,100 +41,100 @@ export interface DoorbellOptions { * @group Doorbell */ export class DoorbellController extends CameraController { - private doorbellService?: Doorbell; - private doorbellServiceExternallySupplied = false; + private doorbellService?: Doorbell + private doorbellServiceExternallySupplied = false /** * Temporary storage. Erased after init. */ - private doorbellOptions?: DoorbellOptions; + private doorbellOptions?: DoorbellOptions /** * Initializes a new `DoorbellController`. * @param options - The {@link CameraControllerOptions} and optional {@link DoorbellOptions}. */ constructor(options: CameraControllerOptions & DoorbellOptions) { - super(options); + super(options) this.doorbellOptions = { name: options.name, externalDoorbellService: options.externalDoorbellService, - }; + } } /** * Call this method to signal a doorbell button press. */ public ringDoorbell(): void { - this.doorbellService!.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); + this.doorbellService!.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS) } constructServices(): CameraControllerServiceMap { if (this.doorbellOptions?.externalDoorbellService) { - this.doorbellService = this.doorbellOptions.externalDoorbellService; - this.doorbellServiceExternallySupplied = true; + this.doorbellService = this.doorbellOptions.externalDoorbellService + this.doorbellServiceExternallySupplied = true } else { - this.doorbellService = new Service.Doorbell(this.doorbellOptions?.name ?? "", ""); + this.doorbellService = new Service.Doorbell(this.doorbellOptions?.name ?? '', '') } - this.doorbellService.setPrimaryService(); + this.doorbellService.setPrimaryService() - const serviceMap = super.constructServices(); + const serviceMap = super.constructServices() if (!this.doorbellServiceExternallySupplied) { - serviceMap.doorbell = this.doorbellService; + serviceMap.doorbell = this.doorbellService } - return serviceMap; + return serviceMap } initWithServices(serviceMap: CameraControllerServiceMap): void | CameraControllerServiceMap { - const result = super._initWithServices(serviceMap); + const result = super._initWithServices(serviceMap) if (this.doorbellOptions?.externalDoorbellService) { - this.doorbellService = this.doorbellOptions.externalDoorbellService; - this.doorbellServiceExternallySupplied = true; + this.doorbellService = this.doorbellOptions.externalDoorbellService + this.doorbellServiceExternallySupplied = true if (result.serviceMap.doorbell) { - delete result.serviceMap.doorbell; - result.updated = true; + delete result.serviceMap.doorbell + result.updated = true } } else { - this.doorbellService = result.serviceMap.doorbell; + this.doorbellService = result.serviceMap.doorbell if (!this.doorbellService) { // see NOTICE above - this.doorbellService = new Service.Doorbell(this.doorbellOptions?.name ?? "", ""); + this.doorbellService = new Service.Doorbell(this.doorbellOptions?.name ?? '', '') - result.serviceMap.doorbell = this.doorbellService; - result.updated = true; + result.serviceMap.doorbell = this.doorbellService + result.updated = true } } - this.doorbellService.setPrimaryService(); + this.doorbellService.setPrimaryService() if (result.updated) { - return result.serviceMap; + return result.serviceMap } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line unused-imports/no-unused-vars protected migrateFromDoorbell(serviceMap: ControllerServiceMap): boolean { - return false; + return false } protected retrieveEventTriggerOptions(): Set { - const result = super.retrieveEventTriggerOptions(); - result.add(EventTriggerOption.DOORBELL); - return result; + const result = super.retrieveEventTriggerOptions() + result.add(EventTriggerOption.DOORBELL) + return result } handleControllerRemoved(): void { - super.handleControllerRemoved(); + super.handleControllerRemoved() - this.doorbellService = undefined; + this.doorbellService = undefined } configureServices(): void { - super.configureServices(); + super.configureServices() this.doorbellService!.getCharacteristic(Characteristic.ProgrammableSwitchEvent) - .onGet(() => null); // a value of null represent nothing is pressed + .onGet(() => null) // a value of null represent nothing is pressed - this.doorbellOptions = undefined; + this.doorbellOptions = undefined } } diff --git a/src/lib/controller/RemoteController.ts b/src/lib/controller/RemoteController.ts index 5bbeb2864..0d43927b1 100644 --- a/src/lib/controller/RemoteController.ts +++ b/src/lib/controller/RemoteController.ts @@ -1,67 +1,82 @@ -import assert from "assert"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { CharacteristicValue } from "../../types"; -import { AudioBitrate, AudioSamplerate } from "../camera"; -import { - Characteristic, - CharacteristicEventTypes, - CharacteristicGetCallback, - CharacteristicSetCallback, -} from "../Characteristic"; -import { - HDSProtocolSpecificErrorReason, +/* global NodeJS */ +import type { CharacteristicValue } from '../../types' +import type { CharacteristicGetCallback, CharacteristicSetCallback } from '../Characteristic' +import type { DataStreamConnection, - DataStreamConnectionEvent, - DataStreamManagement, DataStreamProtocolHandler, - DataStreamServerEvent, EventHandler, - Float32, - HDSStatus, - Int64, - Protocols, RequestHandler, - Topics, -} from "../datastream"; +} from '../datastream' import type { AudioStreamManagement, DataStreamTransportManagement, Siri, TargetControl, TargetControlManagement, -} from "../definitions"; -import { HAPStatus } from "../HAPServer"; -import { Service } from "../Service"; -import { HAPConnection, HAPConnectionEvent } from "../util/eventedhttp"; -import * as tlv from "../util/tlv"; -import { +} from '../definitions' +import type { HAPConnection } from '../util/eventedhttp' +import type { ControllerIdentifier, ControllerServiceMap, - DefaultControllerType, SerializableController, StateChangeDelegate, -} from "./Controller"; +} from './Controller' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { EventEmitter } from 'node:events' -const debug = createDebug("HAP-NodeJS:Remote:Controller"); +import createDebug from 'debug' +import { AudioBitrate, AudioSamplerate } from '../camera/index.js' +import { Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { + DataStreamConnectionEvent, + DataStreamManagement, + DataStreamServerEvent, + Float32, + HDSProtocolSpecificErrorReason, + HDSStatus, + Int64, + Protocols, + Topics, +} from '../datastream/index.js' +import { HAPStatus } from '../HAPServer.js' +import { Service } from '../Service.js' +import { HAPConnectionEvent } from '../util/eventedhttp.js' +import { + decode, + decodeList, + encode, + readUInt16, + readUInt32, + writeUInt16, + writeUInt32, + writeVariableUIntLE, +} from '../util/tlv.js' +import { DefaultControllerType } from './Controller.js' + +const debug = createDebug('HAP-NodeJS:Remote:Controller') + +// eslint-disable-next-line no-restricted-syntax const enum TargetControlCommands { MAXIMUM_TARGETS = 0x01, TICKS_PER_SECOND = 0x02, SUPPORTED_BUTTON_CONFIGURATION = 0x03, - TYPE = 0x04 + TYPE = 0x04, } +// eslint-disable-next-line no-restricted-syntax const enum SupportedButtonConfigurationTypes { BUTTON_ID = 0x01, - BUTTON_TYPE = 0x02 + BUTTON_TYPE = 0x02, } /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum ButtonType { - // noinspection JSUnusedGlobalSymbols UNDEFINED = 0x00, MENU = 0x01, PLAY_PAUSE = 0x02, @@ -75,47 +90,49 @@ export const enum ButtonType { VOLUME_DOWN = 0x0A, SIRI = 0x0B, POWER = 0x0C, - GENERIC = 0x0D + GENERIC = 0x0D, } - +// eslint-disable-next-line no-restricted-syntax const enum TargetControlList { OPERATION = 0x01, - TARGET_CONFIGURATION = 0x02 + TARGET_CONFIGURATION = 0x02, } enum Operation { - // noinspection JSUnusedGlobalSymbols UNDEFINED = 0x00, LIST = 0x01, ADD = 0x02, REMOVE = 0x03, RESET = 0x04, - UPDATE = 0x05 + UPDATE = 0x05, } +// eslint-disable-next-line no-restricted-syntax const enum TargetConfigurationTypes { TARGET_IDENTIFIER = 0x01, TARGET_NAME = 0x02, TARGET_CATEGORY = 0x03, - BUTTON_CONFIGURATION = 0x04 + BUTTON_CONFIGURATION = 0x04, } /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum TargetCategory { - // noinspection JSUnusedGlobalSymbols UNDEFINED = 0x00, - APPLE_TV = 0x18 + APPLE_TV = 0x18, } +// eslint-disable-next-line no-restricted-syntax const enum ButtonConfigurationTypes { BUTTON_ID = 0x01, BUTTON_TYPE = 0x02, BUTTON_NAME = 0x03, } +// eslint-disable-next-line no-restricted-syntax const enum ButtonEvent { BUTTON_ID = 0x01, BUTTON_STATE = 0x02, @@ -126,62 +143,63 @@ const enum ButtonEvent { /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum ButtonState { UP = 0x00, - DOWN = 0x01 + DOWN = 0x01, } - /** * @group Apple TV Remote */ -export type SupportedConfiguration = { - maximumTargets: number, - ticksPerSecond: number, - supportedButtonConfiguration: SupportedButtonConfiguration[], +export interface SupportedConfiguration { + maximumTargets: number + ticksPerSecond: number + supportedButtonConfiguration: SupportedButtonConfiguration[] hardwareImplemented: boolean } /** * @group Apple TV Remote */ -export type SupportedButtonConfiguration = { - buttonID: number, +export interface SupportedButtonConfiguration { + buttonID: number buttonType: ButtonType } /** * @group Apple TV Remote */ -export type TargetConfiguration = { - targetIdentifier: number, - targetName?: string, // on Operation.UPDATE targetName is left out - targetCategory?: TargetCategory, // on Operation.UPDATE targetCategory is left out +export interface TargetConfiguration { + targetIdentifier: number + targetName?: string // on Operation.UPDATE targetName is left out + targetCategory?: TargetCategory // on Operation.UPDATE targetCategory is left out buttonConfiguration: Record // button configurations indexed by their ID } /** * @group Apple TV Remote */ -export type ButtonConfiguration = { - buttonID: number, - buttonType: ButtonType, +export interface ButtonConfiguration { + buttonID: number + buttonType: ButtonType buttonName?: string } - +// eslint-disable-next-line no-restricted-syntax const enum SelectedAudioInputStreamConfigurationTypes { SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION = 0x01, } // ---------- +// eslint-disable-next-line no-restricted-syntax const enum SupportedAudioStreamConfigurationTypes { - // noinspection JSUnusedGlobalSymbols AUDIO_CODEC_CONFIGURATION = 0x01, COMFORT_NOISE_SUPPORT = 0x02, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecConfigurationTypes { CODEC_TYPE = 0x01, CODEC_PARAMETERS = 0x02, @@ -190,8 +208,8 @@ const enum AudioCodecConfigurationTypes { /** * @group Camera */ +// eslint-disable-next-line no-restricted-syntax export const enum AudioCodecTypes { // only really by HAP supported codecs are AAC-ELD and OPUS - // noinspection JSUnusedGlobalSymbols PCMU = 0x00, PCMA = 0x01, AAC_ELD = 0x02, @@ -201,47 +219,48 @@ export const enum AudioCodecTypes { // only really by HAP supported codecs are A AMR_WB = 0x06, } +// eslint-disable-next-line no-restricted-syntax const enum AudioCodecParametersTypes { CHANNEL = 0x01, BIT_RATE = 0x02, SAMPLE_RATE = 0x03, - PACKET_TIME = 0x04 // only present in selected audio codec parameters tlv + PACKET_TIME = 0x04, // only present in selected audio codec parameters tlv } // ---------- -type SupportedAudioStreamConfiguration = { - audioCodecConfiguration: AudioCodecConfiguration, +interface SupportedAudioStreamConfiguration { + audioCodecConfiguration: AudioCodecConfiguration } -type SelectedAudioStreamConfiguration = { - audioCodecConfiguration: AudioCodecConfiguration, +interface SelectedAudioStreamConfiguration { + audioCodecConfiguration: AudioCodecConfiguration } /** * @group Apple TV Remote */ -export type AudioCodecConfiguration = { - codecType: AudioCodecTypes, - parameters: AudioCodecParameters, +export interface AudioCodecConfiguration { + codecType: AudioCodecTypes + parameters: AudioCodecParameters } /** * @group Apple TV Remote */ -export type AudioCodecParameters = { - channels: number, // number of audio channels, default is 1 - bitrate: AudioBitrate, - samplerate: AudioSamplerate, - rtpTime?: RTPTime, // only present in SelectedAudioCodecParameters TLV +export interface AudioCodecParameters { + channels: number // number of audio channels, default is 1 + bitrate: AudioBitrate + samplerate: AudioSamplerate + rtpTime?: RTPTime // only present in SelectedAudioCodecParameters TLV } /** * @group Apple TV Remote */ -export type RTPTime = 20 | 30 | 40 | 60; - +export type RTPTime = 20 | 30 | 40 | 60 +// eslint-disable-next-line no-restricted-syntax const enum SiriAudioSessionState { STARTING = 0, // we are currently waiting for a response for the start request SENDING = 1, // we are sending data @@ -249,47 +268,45 @@ const enum SiriAudioSessionState { CLOSED = 3, // the close event was sent } -type DataSendMessageData = { - packets: AudioFramePacket[], - streamId: Int64, - endOfStream: boolean, +interface DataSendMessageData { + packets: AudioFramePacket[] + streamId: Int64 + endOfStream: boolean } /** * @group Apple TV Remote */ -export type AudioFrame = { - data: Buffer, - rms: number, // root-mean-square +export interface AudioFrame { + data: Buffer + rms: number // root-mean-square } -type AudioFramePacket = { - data: Buffer, +interface AudioFramePacket { + data: Buffer metadata: { - rms: Float32, // root-mean-square - sequenceNumber: Int64, - }, + rms: Float32 // root-mean-square + sequenceNumber: Int64 + } } - /** * @group Apple TV Remote */ -export type FrameHandler = (frame: AudioFrame) => void; +export type FrameHandler = (frame: AudioFrame) => void /** * @group Apple TV Remote */ -export type ErrorHandler = (error: HDSProtocolSpecificErrorReason) => void; +export type ErrorHandler = (error: HDSProtocolSpecificErrorReason) => void /** * @group Apple TV Remote */ export interface SiriAudioStreamProducer { - - startAudioProduction(selectedAudioConfiguration: AudioCodecConfiguration): void; - - stopAudioProduction(): void; - + /* eslint-disable ts/method-signature-style */ + startAudioProduction(selectedAudioConfiguration: AudioCodecConfiguration): void + stopAudioProduction(): void + /* eslint-enable ts/method-signature-style */ } /** @@ -304,14 +321,14 @@ export interface SiriAudioStreamProducerConstructor { * @param errorHandler - should be called with an appropriate reason when the producing process errored * @param options - optional parameter for passing any configuration related options */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new(frameHandler: FrameHandler, errorHandler: ErrorHandler, options?: any): SiriAudioStreamProducer; + new(frameHandler: FrameHandler, errorHandler: ErrorHandler, options?: any): SiriAudioStreamProducer } /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum TargetUpdates { NAME, CATEGORY, @@ -322,73 +339,73 @@ export const enum TargetUpdates { /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum RemoteControllerEvents { /** * This event is emitted when the active state of the remote has changed. * active = true indicates that there is currently an Apple TV listening of button presses and audio streams. */ - ACTIVE_CHANGE = "active-change", + ACTIVE_CHANGE = 'active-change', /** * This event is emitted when the currently selected target has changed. * Possible reasons for a changed active identifier: manual change via api call, first target configuration * gets added, active target gets removed, accessory gets unpaired, reset request was sent. * An activeIdentifier of 0 indicates that no target is selected. */ - ACTIVE_IDENTIFIER_CHANGE = "active-identifier-change", + ACTIVE_IDENTIFIER_CHANGE = 'active-identifier-change', /** * This event is emitted when a new target configuration is received. As we currently do not persistently store * configured targets, this will be called at every startup for every Apple TV configured in the home. */ - TARGET_ADDED = "target-add", + TARGET_ADDED = 'target-add', /** * This event is emitted when an existing target was updated. * The 'updates' array indicates what exactly was changed for the target. */ - TARGET_UPDATED = "target-update", + TARGET_UPDATED = 'target-update', /** * This event is emitted when an existing configuration for a target was removed. */ - TARGET_REMOVED = "target-remove", + TARGET_REMOVED = 'target-remove', /** * This event is emitted when a reset of the target configuration is requested. * With this event every configuration made should be reset. This event is also called * when the accessory gets unpaired. */ - TARGETS_RESET = "targets-reset", + TARGETS_RESET = 'targets-reset', } /** * @group Apple TV Remote */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface RemoteController { - on(event: "active-change", listener: (active: boolean) => void): this; - on(event: "active-identifier-change", listener: (activeIdentifier: number) => void): this; - - on(event: "target-add", listener: (targetConfiguration: TargetConfiguration) => void): this; - on(event: "target-update", listener: (targetConfiguration: TargetConfiguration, updates: TargetUpdates[]) => void): this; - on(event: "target-remove", listener: (targetIdentifier: number) => void): this; - on(event: "targets-reset", listener: () => void): this; - - emit(event: "active-change", active: boolean): boolean; - emit(event: "active-identifier-change", activeIdentifier: number): boolean; - - emit(event: "target-add", targetConfiguration: TargetConfiguration): boolean; - emit(event: "target-update", targetConfiguration: TargetConfiguration, updates: TargetUpdates[]): boolean; - emit(event: "target-remove", targetIdentifier: number): boolean; - emit(event: "targets-reset"): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'active-change', listener: (active: boolean) => void): this + on(event: 'active-identifier-change', listener: (activeIdentifier: number) => void): this + on(event: 'target-add', listener: (targetConfiguration: TargetConfiguration) => void): this + on(event: 'target-update', listener: (targetConfiguration: TargetConfiguration, updates: TargetUpdates[]) => void): this + on(event: 'target-remove', listener: (targetIdentifier: number) => void): this + on(event: 'targets-reset', listener: () => void): this + emit(event: 'active-change', active: boolean): boolean + emit(event: 'active-identifier-change', activeIdentifier: number): boolean + emit(event: 'target-add', targetConfiguration: TargetConfiguration): boolean + emit(event: 'target-update', targetConfiguration: TargetConfiguration, updates: TargetUpdates[]): boolean + emit(event: 'target-remove', targetIdentifier: number): boolean + emit(event: 'targets-reset'): boolean + /* eslint-enable ts/method-signature-style */ } /** * @group Apple TV Remote */ export interface RemoteControllerServiceMap extends ControllerServiceMap { - targetControlManagement: TargetControlManagement, - targetControl: TargetControl, + targetControlManagement: TargetControlManagement + targetControl: TargetControl - siri?: Siri, - audioStreamManagement?: AudioStreamManagement, + siri?: Siri + audioStreamManagement?: AudioStreamManagement dataStreamTransportManagement?: DataStreamTransportManagement } @@ -396,8 +413,8 @@ export interface RemoteControllerServiceMap extends ControllerServiceMap { * @group Apple TV Remote */ export interface SerializedControllerState { - activeIdentifier: number, - targetConfigurations: Record; + activeIdentifier: number + targetConfigurations: Record } /** @@ -405,50 +422,49 @@ export interface SerializedControllerState { * * @group Apple TV Remote */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class RemoteController extends EventEmitter implements SerializableController, DataStreamProtocolHandler { - private stateChangeDelegate?: StateChangeDelegate; + private stateChangeDelegate?: StateChangeDelegate - private readonly audioSupported: boolean; - private readonly audioProducerConstructor?: SiriAudioStreamProducerConstructor; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly audioProducerOptions?: any; + private readonly audioSupported: boolean + private readonly audioProducerConstructor?: SiriAudioStreamProducerConstructor + private readonly audioProducerOptions?: any - private targetControlManagementService?: TargetControlManagement; - private targetControlService?: TargetControl; + private targetControlManagementService?: TargetControlManagement + private targetControlService?: TargetControl - private siriService?: Siri; - private audioStreamManagementService?: AudioStreamManagement; - private dataStreamManagement?: DataStreamManagement; + private siriService?: Siri + private audioStreamManagementService?: AudioStreamManagement + private dataStreamManagement?: DataStreamManagement - private buttons: Record = {}; // internal mapping of buttonId to buttonType for supported buttons - private readonly supportedConfiguration: string; - targetConfigurations: Map = new Map(); - private targetConfigurationsString = ""; + private buttons: Record = {} // internal mapping of buttonId to buttonType for supported buttons + private readonly supportedConfiguration: string + targetConfigurations: Map = new Map() + private targetConfigurationsString = '' - private lastButtonEvent = ""; + private lastButtonEvent = '' - activeIdentifier = 0; // id of 0 means no device selected - private activeConnection?: HAPConnection; // session which marked this remote as active and listens for events and siri - private activeConnectionDisconnectListener?: () => void; + activeIdentifier = 0 // id of 0 means no device selected + private activeConnection?: HAPConnection // session which marked this remote as active and listens for events and siri + private activeConnectionDisconnectListener?: () => void - private readonly supportedAudioConfiguration: string; - private selectedAudioConfiguration: AudioCodecConfiguration; - private selectedAudioConfigurationString: string; + private readonly supportedAudioConfiguration: string + private selectedAudioConfiguration: AudioCodecConfiguration + private selectedAudioConfigurationString: string - private dataStreamConnections: Map = new Map(); // maps targetIdentifiers to active data stream connections - private activeAudioSession?: SiriAudioSession; - private nextAudioSession?: SiriAudioSession; + private dataStreamConnections: Map = new Map() // maps targetIdentifiers to active data stream connections + private activeAudioSession?: SiriAudioSession + private nextAudioSession?: SiriAudioSession /** * @private */ - eventHandler?: Record; + eventHandler?: Record /** * @private */ - requestHandler?: Record; + requestHandler?: Record /** * Creates a new RemoteController. @@ -459,20 +475,19 @@ export class RemoteController extends EventEmitter * * @param audioProducerConstructor - constructor for a SiriAudioStreamProducer * @param producerOptions - if supplied this argument will be supplied as third argument of the SiriAudioStreamProducer - * constructor. This should be used to supply configurations to the stream producer. + * constructor. This should be used to supply configurations to the stream producer. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/explicit-module-boundary-types public constructor(audioProducerConstructor?: SiriAudioStreamProducerConstructor, producerOptions?: any) { - super(); - this.audioSupported = audioProducerConstructor !== undefined; - this.audioProducerConstructor = audioProducerConstructor; - this.audioProducerOptions = producerOptions; + super() + this.audioSupported = audioProducerConstructor !== undefined + this.audioProducerConstructor = audioProducerConstructor + this.audioProducerOptions = producerOptions - const configuration: SupportedConfiguration = this.constructSupportedConfiguration(); - this.supportedConfiguration = this.buildTargetControlSupportedConfigurationTLV(configuration); + const configuration: SupportedConfiguration = this.constructSupportedConfiguration() + this.supportedConfiguration = this.buildTargetControlSupportedConfigurationTLV(configuration) - const audioConfiguration: SupportedAudioStreamConfiguration = this.constructSupportedAudioConfiguration(); - this.supportedAudioConfiguration = RemoteController.buildSupportedAudioConfigurationTLV(audioConfiguration); + const audioConfiguration: SupportedAudioStreamConfiguration = this.constructSupportedAudioConfiguration() + this.supportedAudioConfiguration = RemoteController.buildSupportedAudioConfigurationTLV(audioConfiguration) this.selectedAudioConfiguration = { // set the required defaults codecType: AudioCodecTypes.OPUS, @@ -482,17 +497,17 @@ export class RemoteController extends EventEmitter samplerate: AudioSamplerate.KHZ_16, rtpTime: 20, }, - }; + } this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({ audioCodecConfiguration: this.selectedAudioConfiguration, - }); + }) } /** * @private */ controllerId(): ControllerIdentifier { - return DefaultControllerType.REMOTE; + return DefaultControllerType.REMOTE } /** @@ -502,30 +517,30 @@ export class RemoteController extends EventEmitter */ public setActiveIdentifier(activeIdentifier: number): void { if (activeIdentifier === this.activeIdentifier) { - return; + return } if (activeIdentifier !== 0 && !this.targetConfigurations.has(activeIdentifier)) { - throw Error("Tried setting unconfigured targetIdentifier to active"); + throw new Error('Tried setting unconfigured targetIdentifier to active') } - debug("%d is now the active target", activeIdentifier); - this.activeIdentifier = activeIdentifier; - this.targetControlService!.getCharacteristic(Characteristic.ActiveIdentifier)!.updateValue(activeIdentifier); + debug('%d is now the active target', activeIdentifier) + this.activeIdentifier = activeIdentifier + this.targetControlService!.getCharacteristic(Characteristic.ActiveIdentifier)!.updateValue(activeIdentifier) if (this.activeAudioSession) { - this.handleSiriAudioStop(); + this.handleSiriAudioStop() } - setTimeout(() => this.emit(RemoteControllerEvents.ACTIVE_IDENTIFIER_CHANGE, activeIdentifier), 0); - this.setInactive(); + setTimeout(() => this.emit(RemoteControllerEvents.ACTIVE_IDENTIFIER_CHANGE, activeIdentifier), 0) + this.setInactive() } /** * @returns if the current target is active, meaning the active device is listening for button events or audio sessions */ public isActive(): boolean { - return !!this.activeConnection; + return !!this.activeConnection } /** @@ -534,7 +549,7 @@ export class RemoteController extends EventEmitter * @param targetIdentifier - The target identifier. */ public isConfigured(targetIdentifier: number): boolean { - return this.targetConfigurations.has(targetIdentifier); + return this.targetConfigurations.has(targetIdentifier) } /** @@ -544,13 +559,13 @@ export class RemoteController extends EventEmitter * @returns The targetIdentifier of the device or undefined if not existent. */ public getTargetIdentifierByName(name: string): number | undefined { - for (const [ activeIdentifier, configuration ] of Object.entries(this.targetConfigurations)) { + for (const [activeIdentifier, configuration] of Object.entries(this.targetConfigurations)) { if (configuration.targetName === name) { - return parseInt(activeIdentifier, 10); + return Number.parseInt(activeIdentifier, 10) } } - return undefined; + return undefined } /** @@ -559,7 +574,7 @@ export class RemoteController extends EventEmitter * @param button - button to be pressed */ public pushButton(button: ButtonType): void { - this.sendButtonEvent(button, ButtonState.DOWN); + this.sendButtonEvent(button, ButtonState.DOWN) } /** @@ -568,7 +583,7 @@ export class RemoteController extends EventEmitter * @param button - button which was released */ public releaseButton(button: ButtonType): void { - this.sendButtonEvent(button, ButtonState.UP); + this.sendButtonEvent(button, ButtonState.UP) } /** @@ -578,8 +593,8 @@ export class RemoteController extends EventEmitter * @param time - time in milliseconds (defaults to 200ms) */ public pushAndReleaseButton(button: ButtonType, time = 200): void { - this.pushButton(button); - setTimeout(() => this.releaseButton(button), time); + this.pushButton(button) + setTimeout(() => this.releaseButton(button), time) } // ---------------------------------- CONFIGURATION ---------------------------------- @@ -591,27 +606,36 @@ export class RemoteController extends EventEmitter ticksPerSecond: 1000, // we rely on unix timestamps supportedButtonConfiguration: [], hardwareImplemented: this.audioSupported, // siri is only allowed for hardware implemented remotes - }; + } const supportedButtons = [ - ButtonType.MENU, ButtonType.PLAY_PAUSE, ButtonType.TV_HOME, ButtonType.SELECT, - ButtonType.ARROW_UP, ButtonType.ARROW_RIGHT, ButtonType.ARROW_DOWN, ButtonType.ARROW_LEFT, - ButtonType.VOLUME_UP, ButtonType.VOLUME_DOWN, ButtonType.POWER, ButtonType.GENERIC, - ]; + ButtonType.MENU, + ButtonType.PLAY_PAUSE, + ButtonType.TV_HOME, + ButtonType.SELECT, + ButtonType.ARROW_UP, + ButtonType.ARROW_RIGHT, + ButtonType.ARROW_DOWN, + ButtonType.ARROW_LEFT, + ButtonType.VOLUME_UP, + ButtonType.VOLUME_DOWN, + ButtonType.POWER, + ButtonType.GENERIC, + ] if (this.audioSupported) { // add siri button if this remote supports it - supportedButtons.push(ButtonType.SIRI); + supportedButtons.push(ButtonType.SIRI) } - supportedButtons.forEach(button => { + supportedButtons.forEach((button) => { const buttonConfiguration: SupportedButtonConfiguration = { buttonID: 100 + button, buttonType: button, - }; - configuration.supportedButtonConfiguration.push(buttonConfiguration); - this.buttons[button] = buttonConfiguration.buttonID; // also saving mapping of type to id locally - }); + } + configuration.supportedButtonConfiguration.push(buttonConfiguration) + this.buttons[button] = buttonConfiguration.buttonID // also saving mapping of type to id locally + }) - return configuration; + return configuration } protected constructSupportedAudioConfiguration(): SupportedAudioStreamConfiguration { @@ -625,586 +649,602 @@ export class RemoteController extends EventEmitter samplerate: AudioSamplerate.KHZ_16, }, }, - }; + } } // --------------------------------- TARGET CONTROL ---------------------------------- - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleTargetControlWrite(value: any, callback: CharacteristicSetCallback): void { - const data = Buffer.from(value, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value, 'base64') + const objects = decode(data) - const operation = objects[TargetControlList.OPERATION][0] as Operation; + const operation = objects[TargetControlList.OPERATION][0] as Operation - let targetConfiguration: TargetConfiguration | undefined = undefined; + let targetConfiguration: TargetConfiguration | undefined if (objects[TargetControlList.TARGET_CONFIGURATION]) { // if target configuration was sent, parse it - targetConfiguration = this.parseTargetConfigurationTLV(objects[TargetControlList.TARGET_CONFIGURATION]); + targetConfiguration = this.parseTargetConfigurationTLV(objects[TargetControlList.TARGET_CONFIGURATION]) } - debug("Received TargetControl write operation %s", Operation[operation]); + debug('Received TargetControl write operation %s', Operation[operation]) - let handler: (targetConfiguration?: TargetConfiguration) => HAPStatus; + let handler: (targetConfiguration?: TargetConfiguration) => HAPStatus switch (operation) { - case Operation.ADD: - handler = this.handleAddTarget.bind(this); - break; - case Operation.UPDATE: - handler = this.handleUpdateTarget.bind(this); - break; - case Operation.REMOVE: - handler = this.handleRemoveTarget.bind(this); - break; - case Operation.RESET: - handler = this.handleResetTargets.bind(this); - break; - case Operation.LIST: - handler = this.handleListTargets.bind(this); - break; - default: - callback(HAPStatus.INVALID_VALUE_IN_REQUEST, undefined); - return; - } - - const status = handler(targetConfiguration); + case Operation.ADD: + handler = this.handleAddTarget.bind(this) + break + case Operation.UPDATE: + handler = this.handleUpdateTarget.bind(this) + break + case Operation.REMOVE: + handler = this.handleRemoveTarget.bind(this) + break + case Operation.RESET: + handler = this.handleResetTargets.bind(this) + break + case Operation.LIST: + handler = this.handleListTargets.bind(this) + break + default: + callback(HAPStatus.INVALID_VALUE_IN_REQUEST, undefined) + return + } + + const status = handler(targetConfiguration) if (status === HAPStatus.SUCCESS) { - callback(undefined, this.targetConfigurationsString); // passing value for write response + callback(undefined, this.targetConfigurationsString) // passing value for write response if (operation === Operation.ADD && this.activeIdentifier === 0) { - this.setActiveIdentifier(targetConfiguration!.targetIdentifier); + this.setActiveIdentifier(targetConfiguration!.targetIdentifier) } } else { - callback(new Error(status + "")); + callback(new Error(`${status}`)) } } private handleAddTarget(targetConfiguration?: TargetConfiguration): HAPStatus { if (!targetConfiguration) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } - this.targetConfigurations.set(targetConfiguration.targetIdentifier, targetConfiguration); + this.targetConfigurations.set(targetConfiguration.targetIdentifier, targetConfiguration) - debug("Configured new target '" + targetConfiguration.targetName + "' with targetIdentifier '" + targetConfiguration.targetIdentifier + "'"); + debug(`Configured new target '${targetConfiguration.targetName}' with targetIdentifier '${targetConfiguration.targetIdentifier}'`) - setTimeout(() => this.emit(RemoteControllerEvents.TARGET_ADDED, targetConfiguration), 0); + setTimeout(() => this.emit(RemoteControllerEvents.TARGET_ADDED, targetConfiguration), 0) - this.updatedTargetConfiguration(); // set response - return HAPStatus.SUCCESS; + this.updatedTargetConfiguration() // set response + return HAPStatus.SUCCESS } private handleUpdateTarget(targetConfiguration?: TargetConfiguration): HAPStatus { if (!targetConfiguration) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } - const updates: TargetUpdates[] = []; + const updates: TargetUpdates[] = [] - const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier); + const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier) if (!configuredTarget) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } if (targetConfiguration.targetName) { - debug("Target name was updated '%s' => '%s' (%d)", - configuredTarget.targetName, targetConfiguration.targetName, configuredTarget.targetIdentifier); + debug('Target name was updated \'%s\' => \'%s\' (%d)', configuredTarget.targetName, targetConfiguration.targetName, configuredTarget.targetIdentifier) - configuredTarget.targetName = targetConfiguration.targetName; - updates.push(TargetUpdates.NAME); + configuredTarget.targetName = targetConfiguration.targetName + updates.push(TargetUpdates.NAME) } if (targetConfiguration.targetCategory) { - debug("Target category was updated '%d' => '%d' for target '%s' (%d)", - configuredTarget.targetCategory, targetConfiguration.targetCategory, - configuredTarget.targetName, configuredTarget.targetIdentifier); + debug('Target category was updated \'%d\' => \'%d\' for target \'%s\' (%d)', configuredTarget.targetCategory, targetConfiguration.targetCategory, configuredTarget.targetName, configuredTarget.targetIdentifier) - configuredTarget.targetCategory = targetConfiguration.targetCategory; - updates.push(TargetUpdates.CATEGORY); + configuredTarget.targetCategory = targetConfiguration.targetCategory + updates.push(TargetUpdates.CATEGORY) } if (targetConfiguration.buttonConfiguration) { - debug("%d button configurations were updated for target '%s' (%d)", - Object.keys(targetConfiguration.buttonConfiguration).length, - configuredTarget.targetName, configuredTarget.targetIdentifier); + debug('%d button configurations were updated for target \'%s\' (%d)', Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier) for (const configuration of Object.values(targetConfiguration.buttonConfiguration)) { - const savedConfiguration = configuredTarget.buttonConfiguration[configuration.buttonID]; + const savedConfiguration = configuredTarget.buttonConfiguration[configuration.buttonID] - savedConfiguration.buttonType = configuration.buttonType; - savedConfiguration.buttonName = configuration.buttonName; + savedConfiguration.buttonType = configuration.buttonType + savedConfiguration.buttonName = configuration.buttonName } - updates.push(TargetUpdates.UPDATED_BUTTONS); + updates.push(TargetUpdates.UPDATED_BUTTONS) } - setTimeout(() => this.emit(RemoteControllerEvents.TARGET_UPDATED, targetConfiguration, updates), 0); + setTimeout(() => this.emit(RemoteControllerEvents.TARGET_UPDATED, targetConfiguration, updates), 0) - this.updatedTargetConfiguration(); // set response - return HAPStatus.SUCCESS; + this.updatedTargetConfiguration() // set response + return HAPStatus.SUCCESS } private handleRemoveTarget(targetConfiguration?: TargetConfiguration): HAPStatus { if (!targetConfiguration) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } - const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier); + const configuredTarget = this.targetConfigurations.get(targetConfiguration.targetIdentifier) if (!configuredTarget) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } if (targetConfiguration.buttonConfiguration) { for (const key in targetConfiguration.buttonConfiguration) { if (Object.prototype.hasOwnProperty.call(targetConfiguration.buttonConfiguration, key)) { - delete configuredTarget.buttonConfiguration[key]; + delete configuredTarget.buttonConfiguration[key] } } - debug("Removed %d button configurations of target '%s' (%d)", - Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier); - setTimeout(() => this.emit(RemoteControllerEvents.TARGET_UPDATED, configuredTarget, [TargetUpdates.REMOVED_BUTTONS]), 0); + debug('Removed %d button configurations of target \'%s\' (%d)', Object.keys(targetConfiguration.buttonConfiguration).length, configuredTarget.targetName, configuredTarget.targetIdentifier) + setTimeout(() => this.emit(RemoteControllerEvents.TARGET_UPDATED, configuredTarget, [TargetUpdates.REMOVED_BUTTONS]), 0) } else { - this.targetConfigurations.delete(targetConfiguration.targetIdentifier); + this.targetConfigurations.delete(targetConfiguration.targetIdentifier) - debug ("Target '%s' (%d) was removed", configuredTarget.targetName, configuredTarget.targetIdentifier); - setTimeout(() => this.emit(RemoteControllerEvents.TARGET_REMOVED, targetConfiguration.targetIdentifier), 0); + debug ('Target \'%s\' (%d) was removed', configuredTarget.targetName, configuredTarget.targetIdentifier) + setTimeout(() => this.emit(RemoteControllerEvents.TARGET_REMOVED, targetConfiguration.targetIdentifier), 0) - const keys = Object.keys(this.targetConfigurations); - this.setActiveIdentifier(keys.length === 0? 0: parseInt(keys[0], 10)); // switch to next available remote + const keys = Object.keys(this.targetConfigurations) + this.setActiveIdentifier(keys.length === 0 ? 0 : Number.parseInt(keys[0], 10)) // switch to next available remote } - this.updatedTargetConfiguration(); // set response - return HAPStatus.SUCCESS; + this.updatedTargetConfiguration() // set response + return HAPStatus.SUCCESS } private handleResetTargets(targetConfiguration?: TargetConfiguration): HAPStatus { if (targetConfiguration) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } - debug("Resetting all target configurations"); - this.targetConfigurations = new Map(); - this.updatedTargetConfiguration(); // set response + debug('Resetting all target configurations') + this.targetConfigurations = new Map() + this.updatedTargetConfiguration() // set response - setTimeout(() => this.emit(RemoteControllerEvents.TARGETS_RESET), 0); - this.setActiveIdentifier(0); // resetting active identifier (also sets active to false) + setTimeout(() => this.emit(RemoteControllerEvents.TARGETS_RESET), 0) + this.setActiveIdentifier(0) // resetting active identifier (also sets active to false) - return HAPStatus.SUCCESS; + return HAPStatus.SUCCESS } private handleListTargets(targetConfiguration?: TargetConfiguration): HAPStatus { if (targetConfiguration) { - return HAPStatus.INVALID_VALUE_IN_REQUEST; + return HAPStatus.INVALID_VALUE_IN_REQUEST } // this.targetConfigurationsString is updated after each change, so we basically don't need to do anything here - debug("Returning " + Object.keys(this.targetConfigurations).length + " target configurations"); - return HAPStatus.SUCCESS; + debug(`Returning ${Object.keys(this.targetConfigurations).length} target configurations`) + return HAPStatus.SUCCESS } private handleActiveWrite(value: CharacteristicValue, callback: CharacteristicSetCallback, connection: HAPConnection): void { if (this.activeIdentifier === 0) { - debug("Tried to change active state. There is no active target set though"); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + debug('Tried to change active state. There is no active target set though') + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } if (this.activeConnection) { - this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener!); - this.activeConnection = undefined; - this.activeConnectionDisconnectListener = undefined; + this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener!) + this.activeConnection = undefined + this.activeConnectionDisconnectListener = undefined } - this.activeConnection = value? connection: undefined; + this.activeConnection = value ? connection : undefined if (this.activeConnection) { // register listener when hap connection disconnects - this.activeConnectionDisconnectListener = this.handleActiveSessionDisconnected.bind(this, this.activeConnection); - this.activeConnection.on(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener); + this.activeConnectionDisconnectListener = this.handleActiveSessionDisconnected.bind(this, this.activeConnection) + this.activeConnection.on(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener) } - const activeTarget = this.targetConfigurations.get(this.activeIdentifier); + const activeTarget = this.targetConfigurations.get(this.activeIdentifier) if (!activeTarget) { - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } - debug("Remote with activeTarget '%s' (%d) was set to %s", activeTarget.targetName, this.activeIdentifier, value ? "ACTIVE" : "INACTIVE"); + debug('Remote with activeTarget \'%s\' (%d) was set to %s', activeTarget.targetName, this.activeIdentifier, value ? 'ACTIVE' : 'INACTIVE') - callback(); + callback() - this.emit(RemoteControllerEvents.ACTIVE_CHANGE, value as boolean); + this.emit(RemoteControllerEvents.ACTIVE_CHANGE, value as boolean) } private setInactive(): void { if (this.activeConnection === undefined) { - return; + return } - this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener!); - this.activeConnection = undefined; - this.activeConnectionDisconnectListener = undefined; + this.activeConnection.removeListener(HAPConnectionEvent.CLOSED, this.activeConnectionDisconnectListener!) + this.activeConnection = undefined + this.activeConnectionDisconnectListener = undefined - this.targetControlService!.getCharacteristic(Characteristic.Active)!.updateValue(false); - debug("Remote was set to INACTIVE"); + this.targetControlService!.getCharacteristic(Characteristic.Active)!.updateValue(false) + debug('Remote was set to INACTIVE') - setTimeout(() => this.emit(RemoteControllerEvents.ACTIVE_CHANGE, false), 0); + setTimeout(() => this.emit(RemoteControllerEvents.ACTIVE_CHANGE, false), 0) } private handleActiveSessionDisconnected(connection: HAPConnection): void { if (connection !== this.activeConnection) { - return; + return } - debug("Active hap session disconnected!"); - this.setInactive(); + debug('Active hap session disconnected!') + this.setInactive() } private sendButtonEvent(button: ButtonType, buttonState: ButtonState) { - const buttonID = this.buttons[button]; + const buttonID = this.buttons[button] if (buttonID === undefined || buttonID === 0) { - throw new Error("Tried sending button event for unsupported button (" + button + ")"); + throw new Error(`Tried sending button event for unsupported button (${button})`) } if (this.activeIdentifier === 0) { // cannot press button if no device is selected - throw new Error("Tried sending button event although no target was selected"); + throw new Error('Tried sending button event although no target was selected') } if (!this.isActive()) { // cannot press button if device is not active (aka no Apple TV is listening) - throw new Error("Tried sending button event although target was not marked as active"); + throw new Error('Tried sending button event although target was not marked as active') } if (button === ButtonType.SIRI && this.audioSupported) { if (buttonState === ButtonState.DOWN) { // start streaming session - this.handleSiriAudioStart(); + this.handleSiriAudioStart() } else if (buttonState === ButtonState.UP) { // stop streaming session - this.handleSiriAudioStop(); + this.handleSiriAudioStop() } - return; + return } - const buttonIdTlv = tlv.encode( - ButtonEvent.BUTTON_ID, buttonID, - ); + const buttonIdTlv = encode( + ButtonEvent.BUTTON_ID, + buttonID, + ) - const buttonStateTlv = tlv.encode( - ButtonEvent.BUTTON_STATE, buttonState, - ); + const buttonStateTlv = encode( + ButtonEvent.BUTTON_STATE, + buttonState, + ) - const timestampTlv = tlv.encode( - ButtonEvent.TIMESTAMP, tlv.writeVariableUIntLE(new Date().getTime()), + const timestampTlv = encode( + ButtonEvent.TIMESTAMP, + writeVariableUIntLE(new Date().getTime()), // timestamp should be uint64. bigint though is only supported by node 10.4.0 and above // thus we just interpret timestamp as a regular number - ); + ) - const activeIdentifierTlv = tlv.encode( - ButtonEvent.ACTIVE_IDENTIFIER, tlv.writeUInt32(this.activeIdentifier), - ); + const activeIdentifierTlv = encode( + ButtonEvent.ACTIVE_IDENTIFIER, + writeUInt32(this.activeIdentifier), + ) this.lastButtonEvent = Buffer.concat([ - buttonIdTlv, buttonStateTlv, timestampTlv, activeIdentifierTlv, - ]).toString("base64"); - this.targetControlService!.getCharacteristic(Characteristic.ButtonEvent)!.sendEventNotification(this.lastButtonEvent); + buttonIdTlv, + buttonStateTlv, + timestampTlv, + activeIdentifierTlv, + ]).toString('base64') + this.targetControlService!.getCharacteristic(Characteristic.ButtonEvent)!.sendEventNotification(this.lastButtonEvent) } private parseTargetConfigurationTLV(data: Buffer): TargetConfiguration { - const configTLV = tlv.decode(data); + const configTLV = decode(data) - const identifier = tlv.readUInt32(configTLV[TargetConfigurationTypes.TARGET_IDENTIFIER]); + const identifier = readUInt32(configTLV[TargetConfigurationTypes.TARGET_IDENTIFIER]) - let name = undefined; + let name if (configTLV[TargetConfigurationTypes.TARGET_NAME]) { - name = configTLV[TargetConfigurationTypes.TARGET_NAME].toString(); + name = configTLV[TargetConfigurationTypes.TARGET_NAME].toString() } - let category = undefined; + let category if (configTLV[TargetConfigurationTypes.TARGET_CATEGORY]) { - category = tlv.readUInt16(configTLV[TargetConfigurationTypes.TARGET_CATEGORY]); + category = readUInt16(configTLV[TargetConfigurationTypes.TARGET_CATEGORY]) } - const buttonConfiguration: Record = {}; + const buttonConfiguration: Record = {} if (configTLV[TargetConfigurationTypes.BUTTON_CONFIGURATION]) { - const buttonConfigurationTLV = tlv.decodeList(configTLV[TargetConfigurationTypes.BUTTON_CONFIGURATION], ButtonConfigurationTypes.BUTTON_ID); - buttonConfigurationTLV.forEach(entry => { - const buttonId = entry[ButtonConfigurationTypes.BUTTON_ID][0]; - const buttonType = tlv.readUInt16(entry[ButtonConfigurationTypes.BUTTON_TYPE]); - let buttonName; + const buttonConfigurationTLV = decodeList(configTLV[TargetConfigurationTypes.BUTTON_CONFIGURATION], ButtonConfigurationTypes.BUTTON_ID) + buttonConfigurationTLV.forEach((entry) => { + const buttonId = entry[ButtonConfigurationTypes.BUTTON_ID][0] + const buttonType = readUInt16(entry[ButtonConfigurationTypes.BUTTON_TYPE]) + let buttonName if (entry[ButtonConfigurationTypes.BUTTON_NAME]) { - buttonName = entry[ButtonConfigurationTypes.BUTTON_NAME].toString(); + buttonName = entry[ButtonConfigurationTypes.BUTTON_NAME].toString() } else { // @ts-expect-error: forceConsistentCasingInFileNames compiler option - buttonName = ButtonType[buttonType as ButtonType]; + buttonName = ButtonType[buttonType as ButtonType] } buttonConfiguration[buttonId] = { buttonID: buttonId, - buttonType: buttonType, - buttonName: buttonName, - }; - }); + buttonType, + buttonName, + } + }) } return { targetIdentifier: identifier, targetName: name, targetCategory: category, - buttonConfiguration: buttonConfiguration, - }; + buttonConfiguration, + } } private updatedTargetConfiguration(): void { - const bufferList = []; + const bufferList = [] for (const configuration of Object.values(this.targetConfigurations)) { - const targetIdentifier = tlv.encode( - TargetConfigurationTypes.TARGET_IDENTIFIER, tlv.writeUInt32(configuration.targetIdentifier), - ); - - const targetName = tlv.encode( - TargetConfigurationTypes.TARGET_NAME, configuration.targetName!, - ); - - const targetCategory = tlv.encode( - TargetConfigurationTypes.TARGET_CATEGORY, tlv.writeUInt16(configuration.targetCategory!), - ); - - const buttonConfigurationBuffers: Buffer[] = []; + const targetIdentifier = encode( + TargetConfigurationTypes.TARGET_IDENTIFIER, + writeUInt32(configuration.targetIdentifier), + ) + + const targetName = encode( + TargetConfigurationTypes.TARGET_NAME, + configuration.targetName!, + ) + + const targetCategory = encode( + TargetConfigurationTypes.TARGET_CATEGORY, + writeUInt16(configuration.targetCategory!), + ) + + const buttonConfigurationBuffers: Buffer[] = [] for (const value of configuration.buttonConfiguration.values()) { - let tlvBuffer = tlv.encode( - ButtonConfigurationTypes.BUTTON_ID, value.buttonID, - ButtonConfigurationTypes.BUTTON_TYPE, tlv.writeUInt16(value.buttonType), - ); + let tlvBuffer = encode( + ButtonConfigurationTypes.BUTTON_ID, + value.buttonID, + ButtonConfigurationTypes.BUTTON_TYPE, + writeUInt16(value.buttonType), + ) if (value.buttonName) { tlvBuffer = Buffer.concat([ tlvBuffer, - tlv.encode( - ButtonConfigurationTypes.BUTTON_NAME, value.buttonName, + encode( + ButtonConfigurationTypes.BUTTON_NAME, + value.buttonName, ), - ]); + ]) } - buttonConfigurationBuffers.push(tlvBuffer); + buttonConfigurationBuffers.push(tlvBuffer) } - const buttonConfiguration = tlv.encode( - TargetConfigurationTypes.BUTTON_CONFIGURATION, Buffer.concat(buttonConfigurationBuffers), - ); + const buttonConfiguration = encode( + TargetConfigurationTypes.BUTTON_CONFIGURATION, + Buffer.concat(buttonConfigurationBuffers), + ) const targetConfiguration = Buffer.concat( [targetIdentifier, targetName, targetCategory, buttonConfiguration], - ); + ) - bufferList.push(tlv.encode(TargetControlList.TARGET_CONFIGURATION, targetConfiguration)); + bufferList.push(encode(TargetControlList.TARGET_CONFIGURATION, targetConfiguration)) } - this.targetConfigurationsString = Buffer.concat(bufferList).toString("base64"); - this.stateChangeDelegate?.(); + this.targetConfigurationsString = Buffer.concat(bufferList).toString('base64') + this.stateChangeDelegate?.() } private buildTargetControlSupportedConfigurationTLV(configuration: SupportedConfiguration): string { - const maximumTargets = tlv.encode( - TargetControlCommands.MAXIMUM_TARGETS, configuration.maximumTargets, - ); - - const ticksPerSecond = tlv.encode( - TargetControlCommands.TICKS_PER_SECOND, tlv.writeVariableUIntLE(configuration.ticksPerSecond), - ); - - const supportedButtonConfigurationBuffers: Uint8Array[] = []; - configuration.supportedButtonConfiguration.forEach(value => { - const tlvBuffer = tlv.encode( - SupportedButtonConfigurationTypes.BUTTON_ID, value.buttonID, - SupportedButtonConfigurationTypes.BUTTON_TYPE, tlv.writeUInt16(value.buttonType), - ); - supportedButtonConfigurationBuffers.push(tlvBuffer); - }); - const supportedButtonConfiguration = tlv.encode( - TargetControlCommands.SUPPORTED_BUTTON_CONFIGURATION, Buffer.concat(supportedButtonConfigurationBuffers), - ); - - const type = tlv.encode(TargetControlCommands.TYPE, configuration.hardwareImplemented ? 1 : 0); + const maximumTargets = encode( + TargetControlCommands.MAXIMUM_TARGETS, + configuration.maximumTargets, + ) + + const ticksPerSecond = encode( + TargetControlCommands.TICKS_PER_SECOND, + writeVariableUIntLE(configuration.ticksPerSecond), + ) + + const supportedButtonConfigurationBuffers: Uint8Array[] = [] + configuration.supportedButtonConfiguration.forEach((value) => { + const tlvBuffer = encode( + SupportedButtonConfigurationTypes.BUTTON_ID, + value.buttonID, + SupportedButtonConfigurationTypes.BUTTON_TYPE, + writeUInt16(value.buttonType), + ) + supportedButtonConfigurationBuffers.push(tlvBuffer) + }) + const supportedButtonConfiguration = encode( + TargetControlCommands.SUPPORTED_BUTTON_CONFIGURATION, + Buffer.concat(supportedButtonConfigurationBuffers), + ) + + const type = encode(TargetControlCommands.TYPE, configuration.hardwareImplemented ? 1 : 0) return Buffer.concat( [maximumTargets, ticksPerSecond, supportedButtonConfiguration, type], - ).toString("base64"); + ).toString('base64') } // --------------------------------- SIRI/DATA STREAM -------------------------------- - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleTargetControlWhoAmI(connection: DataStreamConnection, message: Record): void { - const targetIdentifier = message.identifier; - this.dataStreamConnections.set(targetIdentifier, connection); - debug("Discovered HDS connection for targetIdentifier %s", targetIdentifier); + const targetIdentifier = message.identifier + this.dataStreamConnections.set(targetIdentifier, connection) + debug('Discovered HDS connection for targetIdentifier %s', targetIdentifier) - connection.addProtocolHandler(Protocols.DATA_SEND, this); + connection.addProtocolHandler(Protocols.DATA_SEND, this) } private handleSiriAudioStart(): void { if (!this.audioSupported) { - throw new Error("Cannot start siri stream on remote where siri is not supported"); + throw new Error('Cannot start siri stream on remote where siri is not supported') } if (!this.isActive()) { - debug("Tried opening Siri audio stream, however no controller is connected!"); - return; + debug('Tried opening Siri audio stream, however no controller is connected!') + return } if (this.activeAudioSession && (!this.activeAudioSession.isClosing() || this.nextAudioSession)) { // there is already a session running, which is not in closing state and/or there is even already a // nextAudioSession running. ignoring start request - debug("Tried opening Siri audio stream, however there is already one in progress"); - return; + debug('Tried opening Siri audio stream, however there is already one in progress') + return } - const connection = this.dataStreamConnections.get(this.activeIdentifier); // get connection for current target + const connection = this.dataStreamConnections.get(this.activeIdentifier) // get connection for current target if (connection === undefined) { // target seems not connected, ignore it - debug("Tried opening Siri audio stream however target is not connected via HDS"); - return; + debug('Tried opening Siri audio stream however target is not connected via HDS') + return } - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const audioSession = new SiriAudioSession(connection, this.selectedAudioConfiguration, this.audioProducerConstructor!, this.audioProducerOptions); + // eslint-disable-next-line ts/no-use-before-define + const audioSession = new SiriAudioSession(connection, this.selectedAudioConfiguration, this.audioProducerConstructor!, this.audioProducerOptions) if (!this.activeAudioSession) { - this.activeAudioSession = audioSession; + this.activeAudioSession = audioSession } else { // we checked above that this only happens if the activeAudioSession is in closing state, // so no collision with the input device can happen - this.nextAudioSession = audioSession; + this.nextAudioSession = audioSession } - audioSession.on(SiriAudioSessionEvents.CLOSE, this.handleSiriAudioSessionClosed.bind(this, audioSession)); - audioSession.start(); + // eslint-disable-next-line ts/no-use-before-define + audioSession.on(SiriAudioSessionEvents.CLOSE, this.handleSiriAudioSessionClosed.bind(this, audioSession)) + audioSession.start() } private handleSiriAudioStop(): void { if (this.activeAudioSession) { if (!this.activeAudioSession.isClosing()) { - this.activeAudioSession.stop(); - return; + this.activeAudioSession.stop() + return } else if (this.nextAudioSession && !this.nextAudioSession.isClosing()) { - this.nextAudioSession.stop(); - return; + this.nextAudioSession.stop() + return } } - debug("handleSiriAudioStop called although no audio session was started"); + debug('handleSiriAudioStop called although no audio session was started') } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleDataSendAckEvent(message: Record): void { // transfer was successful - const streamId = message.streamId; - const endOfStream = message.endOfStream; + const streamId = message.streamId + const endOfStream = message.endOfStream if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) { - this.activeAudioSession.handleDataSendAckEvent(endOfStream); + this.activeAudioSession.handleDataSendAckEvent(endOfStream) } else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) { - this.nextAudioSession.handleDataSendAckEvent(endOfStream); + this.nextAudioSession.handleDataSendAckEvent(endOfStream) } else { - debug("Received dataSend acknowledgment event for unknown streamId '%s'", streamId); + debug('Received dataSend acknowledgment event for unknown streamId \'%s\'', streamId) } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleDataSendCloseEvent(message: Record): void { // controller indicates he can't handle audio request currently - const streamId = message.streamId; - const reason: HDSProtocolSpecificErrorReason = message.reason; + const streamId = message.streamId + const reason: HDSProtocolSpecificErrorReason = message.reason if (this.activeAudioSession && this.activeAudioSession.streamId === streamId) { - this.activeAudioSession.handleDataSendCloseEvent(reason); + this.activeAudioSession.handleDataSendCloseEvent(reason) } else if (this.nextAudioSession && this.nextAudioSession.streamId === streamId) { - this.nextAudioSession.handleDataSendCloseEvent(reason); + this.nextAudioSession.handleDataSendCloseEvent(reason) } else { - debug("Received dataSend close event for unknown streamId '%s'", streamId); + debug('Received dataSend close event for unknown streamId \'%s\'', streamId) } } private handleSiriAudioSessionClosed(session: SiriAudioSession): void { if (session === this.activeAudioSession) { - this.activeAudioSession = this.nextAudioSession; - this.nextAudioSession = undefined; + this.activeAudioSession = this.nextAudioSession + this.nextAudioSession = undefined } else if (session === this.nextAudioSession) { - this.nextAudioSession = undefined; + this.nextAudioSession = undefined } } private handleDataStreamConnectionClosed(connection: DataStreamConnection): void { - for (const [ targetIdentifier, connection0 ] of this.dataStreamConnections) { + for (const [targetIdentifier, connection0] of this.dataStreamConnections) { if (connection === connection0) { - debug("HDS connection disconnected for targetIdentifier %s", targetIdentifier); - this.dataStreamConnections.delete(targetIdentifier); - break; + debug('HDS connection disconnected for targetIdentifier %s', targetIdentifier) + this.dataStreamConnections.delete(targetIdentifier) + break } } } // ------------------------------- AUDIO CONFIGURATION ------------------------------- - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleSelectedAudioConfigurationWrite(value: any, callback: CharacteristicSetCallback): void { - const data = Buffer.from(value, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value, 'base64') + const objects = decode(data) - const selectedAudioStreamConfiguration = tlv.decode( + const selectedAudioStreamConfiguration = decode( objects[SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION], - ); + ) - const codec = selectedAudioStreamConfiguration[AudioCodecConfigurationTypes.CODEC_TYPE][0]; - const parameters = tlv.decode(selectedAudioStreamConfiguration[AudioCodecConfigurationTypes.CODEC_PARAMETERS]); + const codec = selectedAudioStreamConfiguration[AudioCodecConfigurationTypes.CODEC_TYPE][0] + const parameters = decode(selectedAudioStreamConfiguration[AudioCodecConfigurationTypes.CODEC_PARAMETERS]) - const channels = parameters[AudioCodecParametersTypes.CHANNEL][0]; - const bitrate = parameters[AudioCodecParametersTypes.BIT_RATE][0]; - const samplerate = parameters[AudioCodecParametersTypes.SAMPLE_RATE][0]; + const channels = parameters[AudioCodecParametersTypes.CHANNEL][0] + const bitrate = parameters[AudioCodecParametersTypes.BIT_RATE][0] + const samplerate = parameters[AudioCodecParametersTypes.SAMPLE_RATE][0] this.selectedAudioConfiguration = { codecType: codec, parameters: { - channels: channels, - bitrate: bitrate, - samplerate: samplerate, + channels, + bitrate, + samplerate, rtpTime: 20, }, - }; + } this.selectedAudioConfigurationString = RemoteController.buildSelectedAudioConfigurationTLV({ audioCodecConfiguration: this.selectedAudioConfiguration, - }); + }) - callback(); + callback() } private static buildSupportedAudioConfigurationTLV(configuration: SupportedAudioStreamConfiguration): string { - const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration); + const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration) - const supportedAudioStreamConfiguration = tlv.encode( - SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION, codecConfigurationTLV, - ); - return supportedAudioStreamConfiguration.toString("base64"); + const supportedAudioStreamConfiguration = encode( + SupportedAudioStreamConfigurationTypes.AUDIO_CODEC_CONFIGURATION, + codecConfigurationTLV, + ) + return supportedAudioStreamConfiguration.toString('base64') } private static buildSelectedAudioConfigurationTLV(configuration: SelectedAudioStreamConfiguration): string { - const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration); + const codecConfigurationTLV = RemoteController.buildCodecConfigurationTLV(configuration.audioCodecConfiguration) - const supportedAudioStreamConfiguration = tlv.encode( - SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION, codecConfigurationTLV, - ); - return supportedAudioStreamConfiguration.toString("base64"); + const supportedAudioStreamConfiguration = encode( + SelectedAudioInputStreamConfigurationTypes.SELECTED_AUDIO_INPUT_STREAM_CONFIGURATION, + codecConfigurationTLV, + ) + return supportedAudioStreamConfiguration.toString('base64') } private static buildCodecConfigurationTLV(codecConfiguration: AudioCodecConfiguration): Buffer { - const parameters = codecConfiguration.parameters; - - let parametersTLV = tlv.encode( - AudioCodecParametersTypes.CHANNEL, parameters.channels, - AudioCodecParametersTypes.BIT_RATE, parameters.bitrate, - AudioCodecParametersTypes.SAMPLE_RATE, parameters.samplerate, - ); + const parameters = codecConfiguration.parameters + + let parametersTLV = encode( + AudioCodecParametersTypes.CHANNEL, + parameters.channels, + AudioCodecParametersTypes.BIT_RATE, + parameters.bitrate, + AudioCodecParametersTypes.SAMPLE_RATE, + parameters.samplerate, + ) if (parameters.rtpTime) { parametersTLV = Buffer.concat([ parametersTLV, - tlv.encode(AudioCodecParametersTypes.PACKET_TIME, parameters.rtpTime), - ]); + encode(AudioCodecParametersTypes.PACKET_TIME, parameters.rtpTime), + ]) } - return tlv.encode( - AudioCodecConfigurationTypes.CODEC_TYPE, codecConfiguration.codecType, - AudioCodecConfigurationTypes.CODEC_PARAMETERS, parametersTLV, - ); + return encode( + AudioCodecConfigurationTypes.CODEC_TYPE, + codecConfiguration.codecType, + AudioCodecConfigurationTypes.CODEC_PARAMETERS, + parametersTLV, + ) } // ----------------------------------------------------------------------------------- @@ -1213,30 +1253,30 @@ export class RemoteController extends EventEmitter * @private */ constructServices(): RemoteControllerServiceMap { - this.targetControlManagementService = new Service.TargetControlManagement("", ""); - this.targetControlManagementService.setCharacteristic(Characteristic.TargetControlSupportedConfiguration, this.supportedConfiguration); - this.targetControlManagementService.setCharacteristic(Characteristic.TargetControlList, this.targetConfigurationsString); - this.targetControlManagementService.setPrimaryService(); + this.targetControlManagementService = new Service.TargetControlManagement('', '') + this.targetControlManagementService.setCharacteristic(Characteristic.TargetControlSupportedConfiguration, this.supportedConfiguration) + this.targetControlManagementService.setCharacteristic(Characteristic.TargetControlList, this.targetConfigurationsString) + this.targetControlManagementService.setPrimaryService() // you can also expose multiple TargetControl services to control multiple apple tvs simultaneously. // should we extend this class to support multiple TargetControl services or should users just create a second accessory? - this.targetControlService = new Service.TargetControl("", ""); - this.targetControlService.setCharacteristic(Characteristic.ActiveIdentifier, 0); - this.targetControlService.setCharacteristic(Characteristic.Active, false); - this.targetControlService.setCharacteristic(Characteristic.ButtonEvent, this.lastButtonEvent); + this.targetControlService = new Service.TargetControl('', '') + this.targetControlService.setCharacteristic(Characteristic.ActiveIdentifier, 0) + this.targetControlService.setCharacteristic(Characteristic.Active, false) + this.targetControlService.setCharacteristic(Characteristic.ButtonEvent, this.lastButtonEvent) if (this.audioSupported) { - this.siriService = new Service.Siri("", ""); - this.siriService.setCharacteristic(Characteristic.SiriInputType, Characteristic.SiriInputType.PUSH_BUTTON_TRIGGERED_APPLE_TV); + this.siriService = new Service.Siri('', '') + this.siriService.setCharacteristic(Characteristic.SiriInputType, Characteristic.SiriInputType.PUSH_BUTTON_TRIGGERED_APPLE_TV) - this.audioStreamManagementService = new Service.AudioStreamManagement("", ""); - this.audioStreamManagementService.setCharacteristic(Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioConfiguration); - this.audioStreamManagementService.setCharacteristic(Characteristic.SelectedAudioStreamConfiguration, this.selectedAudioConfigurationString); + this.audioStreamManagementService = new Service.AudioStreamManagement('', '') + this.audioStreamManagementService.setCharacteristic(Characteristic.SupportedAudioStreamConfiguration, this.supportedAudioConfiguration) + this.audioStreamManagementService.setCharacteristic(Characteristic.SelectedAudioStreamConfiguration, this.selectedAudioConfigurationString) - this.dataStreamManagement = new DataStreamManagement(); + this.dataStreamManagement = new DataStreamManagement() - this.siriService.addLinkedService(this.dataStreamManagement!.getService()); - this.siriService.addLinkedService(this.audioStreamManagementService!); + this.siriService.addLinkedService(this.dataStreamManagement!.getService()) + this.siriService.addLinkedService(this.audioStreamManagementService!) } return { @@ -1246,19 +1286,19 @@ export class RemoteController extends EventEmitter siri: this.siriService, audioStreamManagement: this.audioStreamManagementService, dataStreamTransportManagement: this.dataStreamManagement?.getService(), - }; + } } /** * @private */ initWithServices(serviceMap: RemoteControllerServiceMap): void | RemoteControllerServiceMap { - this.targetControlManagementService = serviceMap.targetControlManagement; - this.targetControlService = serviceMap.targetControl; + this.targetControlManagementService = serviceMap.targetControlManagement + this.targetControlService = serviceMap.targetControl - this.siriService = serviceMap.siri; - this.audioStreamManagementService = serviceMap.audioStreamManagement; - this.dataStreamManagement = new DataStreamManagement(serviceMap.dataStreamTransportManagement); + this.siriService = serviceMap.siri + this.audioStreamManagementService = serviceMap.audioStreamManagement + this.dataStreamManagement = new DataStreamManagement(serviceMap.dataStreamTransportManagement) } /** @@ -1266,52 +1306,52 @@ export class RemoteController extends EventEmitter */ configureServices(): void { if (!this.targetControlManagementService || !this.targetControlService) { - throw new Error("Unexpected state: Services not configured!"); // playing it save + throw new Error('Unexpected state: Services not configured!') // playing it save } this.targetControlManagementService.getCharacteristic(Characteristic.TargetControlList)! - .on(CharacteristicEventTypes.GET, callback => { - callback(null, this.targetConfigurationsString); + .on(CharacteristicEventTypes.GET, (callback) => { + callback(null, this.targetConfigurationsString) }) - .on(CharacteristicEventTypes.SET, this.handleTargetControlWrite.bind(this)); + .on(CharacteristicEventTypes.SET, this.handleTargetControlWrite.bind(this)) this.targetControlService.getCharacteristic(Characteristic.ActiveIdentifier)! - .on(CharacteristicEventTypes.GET, callback => { - callback(undefined, this.activeIdentifier); - }); + .on(CharacteristicEventTypes.GET, (callback) => { + callback(undefined, this.activeIdentifier) + }) this.targetControlService.getCharacteristic(Characteristic.Active)! - .on(CharacteristicEventTypes.GET, callback => { - callback(undefined, this.isActive()); + .on(CharacteristicEventTypes.GET, (callback) => { + callback(undefined, this.isActive()) }) .on(CharacteristicEventTypes.SET, (value, callback, context, connection) => { if (!connection) { - debug("Set event handler for Remote.Active cannot be called from plugin. Connection undefined!"); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + debug('Set event handler for Remote.Active cannot be called from plugin. Connection undefined!') + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } - this.handleActiveWrite(value, callback, connection); - }); + this.handleActiveWrite(value, callback, connection) + }) this.targetControlService.getCharacteristic(Characteristic.ButtonEvent)! .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => { - callback(undefined, this.lastButtonEvent); - }); + callback(undefined, this.lastButtonEvent) + }) if (this.audioSupported) { this.audioStreamManagementService!.getCharacteristic(Characteristic.SelectedAudioStreamConfiguration)! - .on(CharacteristicEventTypes.GET, callback => { - callback(null, this.selectedAudioConfigurationString); + .on(CharacteristicEventTypes.GET, (callback) => { + callback(null, this.selectedAudioConfigurationString) }) .on(CharacteristicEventTypes.SET, this.handleSelectedAudioConfigurationWrite.bind(this)) - .updateValue(this.selectedAudioConfigurationString); + .updateValue(this.selectedAudioConfigurationString) this.dataStreamManagement! .onEventMessage(Protocols.TARGET_CONTROL, Topics.WHOAMI, this.handleTargetControlWhoAmI.bind(this)) - .onServerEvent(DataStreamServerEvent.CONNECTION_CLOSED, this.handleDataStreamConnectionClosed.bind(this)); + .onServerEvent(DataStreamServerEvent.CONNECTION_CLOSED, this.handleDataStreamConnectionClosed.bind(this)) this.eventHandler = { // eventHandlers which gets subscribed to on open connections on whoami [Topics.ACK]: this.handleDataSendAckEvent.bind(this), [Topics.CLOSE]: this.handleDataSendCloseEvent.bind(this), - }; + } } } @@ -1319,16 +1359,16 @@ export class RemoteController extends EventEmitter * @private */ handleControllerRemoved(): void { - this.targetControlManagementService = undefined; - this.targetControlService = undefined; - this.siriService = undefined; - this.audioStreamManagementService = undefined; + this.targetControlManagementService = undefined + this.targetControlService = undefined + this.siriService = undefined + this.audioStreamManagementService = undefined - this.eventHandler = undefined; - this.requestHandler = undefined; + this.eventHandler = undefined + this.requestHandler = undefined - this.dataStreamManagement?.destroy(); - this.dataStreamManagement = undefined; + this.dataStreamManagement?.destroy() + this.dataStreamManagement = undefined // the call to dataStreamManagement.destroy will close any open data stream connection // which will result in a call to this.handleDataStreamConnectionClosed, cleaning up this.dataStreamConnections. @@ -1340,9 +1380,9 @@ export class RemoteController extends EventEmitter * @private */ handleFactoryReset(): void { - debug("Running factory reset. Resetting targets..."); - this.handleResetTargets(undefined); - this.lastButtonEvent = ""; + debug('Running factory reset. Resetting targets...') + this.handleResetTargets(undefined) + this.lastButtonEvent = '' } /** @@ -1350,316 +1390,314 @@ export class RemoteController extends EventEmitter */ serialize(): SerializedControllerState | undefined { if (!this.activeIdentifier && Object.keys(this.targetConfigurations).length === 0) { - return undefined; + return undefined } return { activeIdentifier: this.activeIdentifier, - targetConfigurations: [...this.targetConfigurations].reduce((obj: Record, [ key, value ]) => { - obj[key] = value; - return obj; + targetConfigurations: [...this.targetConfigurations].reduce((obj: Record, [key, value]) => { + obj[key] = value + return obj }, {}), - }; + } } /** * @private */ deserialize(serialized: SerializedControllerState): void { - this.activeIdentifier = serialized.activeIdentifier; - this.targetConfigurations = Object.entries(serialized.targetConfigurations).reduce((map: Map, [ key, value ]) => { - const identifier = parseInt(key, 10); - map.set(identifier, value); - return map; - }, new Map()); - this.updatedTargetConfiguration(); + this.activeIdentifier = serialized.activeIdentifier + this.targetConfigurations = Object.entries(serialized.targetConfigurations).reduce((map: Map, [key, value]) => { + const identifier = Number.parseInt(key, 10) + map.set(identifier, value) + return map + }, new Map()) + this.updatedTargetConfiguration() } /** * @private */ setupStateChangeDelegate(delegate?: StateChangeDelegate): void { - this.stateChangeDelegate = delegate; + this.stateChangeDelegate = delegate } - } /** * @group Apple TV Remote */ +// eslint-disable-next-line no-restricted-syntax export const enum SiriAudioSessionEvents { - CLOSE = "close", + CLOSE = 'close', } /** * @group Apple TV Remote */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface SiriAudioSession { - on(event: "close", listener: () => void): this; - - emit(event: "close"): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'close', listener: () => void): this + emit(event: 'close'): boolean + /* eslint-enable ts/method-signature-style */ } /** * Represents an ongoing audio transmission * @group Apple TV Remote */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class SiriAudioSession extends EventEmitter { - readonly connection: DataStreamConnection; - private readonly selectedAudioConfiguration: AudioCodecConfiguration; + readonly connection: DataStreamConnection + private readonly selectedAudioConfiguration: AudioCodecConfiguration - private readonly producer: SiriAudioStreamProducer; - private producerRunning = false; // indicates if the producer is running - private producerTimer?: NodeJS.Timeout; // producer has a 3s timeout to produce the first frame, otherwise transmission will be cancelled + private readonly producer: SiriAudioStreamProducer + private producerRunning = false // indicates if the producer is running + private producerTimer?: NodeJS.Timeout // producer has a 3s timeout to produce the first frame, otherwise transmission will be cancelled /** - * @private file private API + * @private */ - state: SiriAudioSessionState = SiriAudioSessionState.STARTING; - streamId?: number; // present when state >= SENDING - endOfStream = false; + state: SiriAudioSessionState = SiriAudioSessionState.STARTING + streamId?: number // present when state >= SENDING + endOfStream = false - private audioFrameQueue: AudioFrame[] = []; - private readonly maxQueueSize = 1024; - private sequenceNumber = 0; + private audioFrameQueue: AudioFrame[] = [] + private readonly maxQueueSize = 1024 + private sequenceNumber = 0 - private readonly closeListener: () => void; + private readonly closeListener: () => void constructor( connection: DataStreamConnection, selectedAudioConfiguration: AudioCodecConfiguration, producerConstructor: SiriAudioStreamProducerConstructor, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any producerOptions?: any, ) { - super(); - this.connection = connection; - this.selectedAudioConfiguration = selectedAudioConfiguration; + super() + this.connection = connection + this.selectedAudioConfiguration = selectedAudioConfiguration - this.producer = new producerConstructor(this.handleSiriAudioFrame.bind(this), this.handleProducerError.bind(this), producerOptions); + // eslint-disable-next-line new-cap + this.producer = new producerConstructor(this.handleSiriAudioFrame.bind(this), this.handleProducerError.bind(this), producerOptions) - this.connection.on(DataStreamConnectionEvent.CLOSED, this.closeListener = this.handleDataStreamConnectionClosed.bind(this)); + this.connection.on(DataStreamConnectionEvent.CLOSED, this.closeListener = this.handleDataStreamConnectionClosed.bind(this)) } /** * Called when siri button is pressed */ start(): void { - debug("Sending request to start siri audio stream"); + debug('Sending request to start siri audio stream') // opening dataSend this.connection.sendRequest(Protocols.DATA_SEND, Topics.OPEN, { - target: "controller", - type: "audio.siri", + target: 'controller', + type: 'audio.siri', }, (error, status, message) => { if (this.state === SiriAudioSessionState.CLOSED) { - debug("Ignoring dataSend open response as the session is already closed"); - return; + debug('Ignoring dataSend open response as the session is already closed') + return } - assert.strictEqual(this.state, SiriAudioSessionState.STARTING); - this.state = SiriAudioSessionState.SENDING; + assert.strictEqual(this.state, SiriAudioSessionState.STARTING) + this.state = SiriAudioSessionState.SENDING if (error || status) { if (error) { // errors get produced by hap-nodejs - debug("Error occurred trying to start siri audio stream: %s", error.message); + debug('Error occurred trying to start siri audio stream: %s', error.message) } else if (status) { // status codes are those returned by the hds response - debug("Controller responded with non-zero status code: %s", HDSStatus[status]); + debug('Controller responded with non-zero status code: %s', HDSStatus[status]) } - this.closed(); + this.closed() } else { - this.streamId = message.streamId; + this.streamId = message.streamId if (!this.producerRunning) { // audio producer errored in the meantime - this.sendDataSendCloseEvent(HDSProtocolSpecificErrorReason.CANCELLED); + this.sendDataSendCloseEvent(HDSProtocolSpecificErrorReason.CANCELLED) } else { - debug("Successfully setup siri audio stream with streamId %d", this.streamId); + debug('Successfully setup siri audio stream with streamId %d', this.streamId) } } - }); + }) - this.startAudioProducer(); // start audio producer and queue frames in the meantime + this.startAudioProducer() // start audio producer and queue frames in the meantime } /** * @returns if the audio session is closing */ isClosing(): boolean { - return this.state >= SiriAudioSessionState.CLOSING; + return this.state >= SiriAudioSessionState.CLOSING } /** * Called when siri button is released (or active identifier is changed to another device) */ stop(): void { - assert(this.state <= SiriAudioSessionState.SENDING, "state was higher than SENDING"); + assert(this.state <= SiriAudioSessionState.SENDING, 'state was higher than SENDING') - debug("Stopping siri audio stream with streamId %d", this.streamId); + debug('Stopping siri audio stream with streamId %d', this.streamId) - this.endOfStream = true; // mark as endOfStream - this.stopAudioProducer(); + this.endOfStream = true // mark as endOfStream + this.stopAudioProducer() if (this.state === SiriAudioSessionState.SENDING) { - this.handleSiriAudioFrame(undefined); // send out last few audio frames with endOfStream property set + this.handleSiriAudioFrame(undefined) // send out last few audio frames with endOfStream property set - this.state = SiriAudioSessionState.CLOSING; // we are waiting for an acknowledgment (triggered by endOfStream property) + this.state = SiriAudioSessionState.CLOSING // we are waiting for an acknowledgment (triggered by endOfStream property) } else { // if state is not SENDING (aka state is STARTING) the callback for DATA_SEND OPEN did not yet return (or never will) - this.closed(); + this.closed() } } private startAudioProducer() { - this.producer.startAudioProduction(this.selectedAudioConfiguration); - this.producerRunning = true; + this.producer.startAudioProduction(this.selectedAudioConfiguration) + this.producerRunning = true this.producerTimer = setTimeout(() => { // producer has 3s to start producing audio frames - debug("Didn't receive any frames from audio producer for stream with streamId %s. Canceling the stream now.", this.streamId); - this.producerTimer = undefined; - this.handleProducerError(HDSProtocolSpecificErrorReason.CANCELLED); - }, 3000); - this.producerTimer.unref(); + debug('Didn\'t receive any frames from audio producer for stream with streamId %s. Canceling the stream now.', this.streamId) + this.producerTimer = undefined + this.handleProducerError(HDSProtocolSpecificErrorReason.CANCELLED) + }, 3000) + this.producerTimer.unref() } private stopAudioProducer() { - this.producer.stopAudioProduction(); - this.producerRunning = false; + this.producer.stopAudioProduction() + this.producerRunning = false if (this.producerTimer) { - clearTimeout(this.producerTimer); - this.producerTimer = undefined; + clearTimeout(this.producerTimer) + this.producerTimer = undefined } } private handleSiriAudioFrame(frame?: AudioFrame): void { // called from audio producer if (this.state >= SiriAudioSessionState.CLOSING) { - return; + return } if (this.producerTimer) { // if producerTimer is defined, then this is the first frame we are receiving - clearTimeout(this.producerTimer); - this.producerTimer = undefined; + clearTimeout(this.producerTimer) + this.producerTimer = undefined } if (frame && this.audioFrameQueue.length < this.maxQueueSize) { // add frame to queue whilst it is not full - this.audioFrameQueue.push(frame); + this.audioFrameQueue.push(frame) } if (this.state !== SiriAudioSessionState.SENDING) { // dataSend isn't open yet - return; + return } - let queued; - while ((queued = this.popSome()) !== null) { // send packets - const packets: AudioFramePacket[] = []; - queued.forEach(frame => { + let queued = this.popSome() + while (queued !== null) { // send packets + const packets: AudioFramePacket[] = [] + queued.forEach((frame) => { const packetData: AudioFramePacket = { data: frame.data, metadata: { rms: new Float32(frame.rms), sequenceNumber: new Int64(this.sequenceNumber++), }, - }; - packets.push(packetData); - }); + } + packets.push(packetData) + }) const message: DataSendMessageData = { - packets: packets, + packets, streamId: new Int64(this.streamId!), endOfStream: this.endOfStream, - }; + } try { - this.connection.sendEvent(Protocols.DATA_SEND, Topics.DATA, message); + this.connection.sendEvent(Protocols.DATA_SEND, Topics.DATA, message) } catch (error) { - debug("Error occurred when trying to send audio frame of hds connection: %s", error.message); + debug('Error occurred when trying to send audio frame of hds connection: %s', error.message) - this.stopAudioProducer(); - this.closed(); + this.stopAudioProducer() + this.closed() } - if (this.endOfStream) { - break; // popSome() returns empty list if endOfStream=true - } + queued = this.popSome() } } private handleProducerError(error: HDSProtocolSpecificErrorReason): void { // called from audio producer if (this.state >= SiriAudioSessionState.CLOSING) { - return; + return } - this.stopAudioProducer(); // ensure backend is closed + this.stopAudioProducer() // ensure backend is closed if (this.state === SiriAudioSessionState.SENDING) { // if state is less than sending dataSend isn't open (yet) - this.sendDataSendCloseEvent(error); // cancel submission + this.sendDataSendCloseEvent(error) // cancel submission } } handleDataSendAckEvent(endOfStream: boolean): void { // transfer was successful - assert.strictEqual(endOfStream, true); + assert.strictEqual(endOfStream, true) - debug("Received acknowledgment for siri audio stream with streamId %s, closing it now", this.streamId); + debug('Received acknowledgment for siri audio stream with streamId %s, closing it now', this.streamId) - this.sendDataSendCloseEvent(HDSProtocolSpecificErrorReason.NORMAL); + this.sendDataSendCloseEvent(HDSProtocolSpecificErrorReason.NORMAL) } handleDataSendCloseEvent(reason: HDSProtocolSpecificErrorReason): void { // controller indicates he can't handle audio request currently // @ts-expect-error: forceConsistentCasingInFileNames compiler option - debug("Received close event from controller with reason %s for stream with streamId %s", HDSProtocolSpecificErrorReason[reason], this.streamId); + debug('Received close event from controller with reason %s for stream with streamId %s', HDSProtocolSpecificErrorReason[reason], this.streamId) if (this.state <= SiriAudioSessionState.SENDING) { - this.stopAudioProducer(); + this.stopAudioProducer() } - this.closed(); + this.closed() } private sendDataSendCloseEvent(reason: HDSProtocolSpecificErrorReason): void { - assert(this.state >= SiriAudioSessionState.SENDING, "state was less than SENDING"); - assert(this.state <= SiriAudioSessionState.CLOSING, "state was higher than CLOSING"); + assert(this.state >= SiriAudioSessionState.SENDING, 'state was less than SENDING') + assert(this.state <= SiriAudioSessionState.CLOSING, 'state was higher than CLOSING') this.connection.sendEvent(Protocols.DATA_SEND, Topics.CLOSE, { streamId: new Int64(this.streamId!), reason: new Int64(reason), - }); + }) - this.closed(); + this.closed() } private handleDataStreamConnectionClosed(): void { - debug("Closing audio session with streamId %d", this.streamId); + debug('Closing audio session with streamId %d', this.streamId) if (this.state <= SiriAudioSessionState.SENDING) { - this.stopAudioProducer(); + this.stopAudioProducer() } - this.closed(); + this.closed() } private closed(): void { - const lastState = this.state; - this.state = SiriAudioSessionState.CLOSED; + const lastState = this.state + this.state = SiriAudioSessionState.CLOSED if (lastState !== SiriAudioSessionState.CLOSED) { - this.emit(SiriAudioSessionEvents.CLOSE); - this.connection.removeListener(DataStreamConnectionEvent.CLOSED, this.closeListener); + this.emit(SiriAudioSessionEvents.CLOSE) + this.connection.removeListener(DataStreamConnectionEvent.CLOSED, this.closeListener) } - this.removeAllListeners(); + this.removeAllListeners() } private popSome() { // tries to return 5 elements from the queue, if endOfStream=true also less than 5 if (this.audioFrameQueue.length < 5 && !this.endOfStream) { - return null; + return null } - const size = Math.min(this.audioFrameQueue.length, 5); // 5 frames per hap packet seems fine - const result = []; + const size = Math.min(this.audioFrameQueue.length, 5) // 5 frames per hap packet seems fine + const result = [] for (let i = 0; i < size; i++) { - const element = this.audioFrameQueue.shift()!; // removes first element - result.push(element); + const element = this.audioFrameQueue.shift()! // removes first element + result.push(element) } - return result; + return result } - } diff --git a/src/lib/controller/index.ts b/src/lib/controller/index.ts index 0926de155..4d8b24c68 100644 --- a/src/lib/controller/index.ts +++ b/src/lib/controller/index.ts @@ -1,6 +1,6 @@ -export * from "./Controller"; +export * from './AdaptiveLightingController.js' -export * from "./AdaptiveLightingController"; -export * from "./RemoteController"; -export * from "./CameraController"; -export * from "./DoorbellController"; +export * from './CameraController.js' +export * from './Controller.js' +export * from './DoorbellController.js' +export * from './RemoteController.js' diff --git a/src/lib/datastream/DataStreamManagement.ts b/src/lib/datastream/DataStreamManagement.ts index db602ad95..71c12f92c 100644 --- a/src/lib/datastream/DataStreamManagement.ts +++ b/src/lib/datastream/DataStreamManagement.ts @@ -1,46 +1,56 @@ -import createDebug from "debug"; -import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback } from "../Characteristic"; -import type { DataStreamTransportManagement } from "../definitions"; -import { HAPStatus } from "../HAPServer"; -import { Service } from "../Service"; -import { HAPConnection } from "../util/eventedhttp"; -import * as tlv from "../util/tlv"; -import { +import type { CharacteristicSetCallback } from '../Characteristic' +import type { DataStreamTransportManagement } from '../definitions' +import type { HAPConnection } from '../util/eventedhttp' +import type { DataStreamConnection, - DataStreamServer, DataStreamServerEvent, GlobalEventHandler, - GlobalRequestHandler, Protocols, Topics, -} from "./DataStreamServer"; + GlobalRequestHandler, + Protocols, + Topics, +} from './DataStreamServer' -const debug = createDebug("HAP-NodeJS:DataStream:Management"); +import { Buffer } from 'node:buffer' +import createDebug from 'debug' + +import { Characteristic, CharacteristicEventTypes } from '../Characteristic.js' +import { HAPStatus } from '../HAPServer.js' +import { Service } from '../Service.js' +import { decode, encode, writeUInt16 } from '../util/tlv.js' +import { DataStreamServer } from './DataStreamServer.js' + +const debug = createDebug('HAP-NodeJS:DataStream:Management') + +// eslint-disable-next-line no-restricted-syntax const enum TransferTransportConfigurationTypes { TRANSFER_TRANSPORT_CONFIGURATION = 1, } +// eslint-disable-next-line no-restricted-syntax const enum TransportTypeTypes { TRANSPORT_TYPE = 1, } - +// eslint-disable-next-line no-restricted-syntax const enum SetupDataStreamSessionTypes { SESSION_COMMAND_TYPE = 1, TRANSPORT_TYPE = 2, CONTROLLER_KEY_SALT = 3, } +// eslint-disable-next-line no-restricted-syntax const enum SetupDataStreamWriteResponseTypes { STATUS = 1, TRANSPORT_TYPE_SESSION_PARAMETERS = 2, ACCESSORY_KEY_SALT = 3, } +// eslint-disable-next-line no-restricted-syntax const enum TransportSessionConfiguration { TCP_LISTENING_PORT = 1, } - enum TransportType { HOMEKIT_DATA_STREAM = 0, } @@ -52,46 +62,46 @@ enum SessionCommandType { /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum DataStreamStatus { SUCCESS = 0, GENERIC_ERROR = 1, BUSY = 2, // maximum numbers of sessions } - /** * @group HomeKit Data Streams (HDS) */ export class DataStreamManagement { // one server per accessory is probably the best practice - private readonly dataStreamServer: DataStreamServer = new DataStreamServer(); // TODO how to handle Remote+future HKSV controller at the same time? + private readonly dataStreamServer: DataStreamServer = new DataStreamServer() // TODO how to handle Remote+future HKSV controller at the same time? - private readonly dataStreamTransportManagementService: DataStreamTransportManagement; + private readonly dataStreamTransportManagementService: DataStreamTransportManagement - private readonly supportedDataStreamTransportConfiguration: string; - private lastSetupDataStreamTransportResponse = ""; // stripped. excludes ACCESSORY_KEY_SALT + private readonly supportedDataStreamTransportConfiguration: string + private lastSetupDataStreamTransportResponse = '' // stripped. excludes ACCESSORY_KEY_SALT constructor(service?: DataStreamTransportManagement) { - const supportedConfiguration: TransportType[] = [TransportType.HOMEKIT_DATA_STREAM]; - this.supportedDataStreamTransportConfiguration = this.buildSupportedDataStreamTransportConfigurationTLV(supportedConfiguration); + const supportedConfiguration: TransportType[] = [TransportType.HOMEKIT_DATA_STREAM] + this.supportedDataStreamTransportConfiguration = this.buildSupportedDataStreamTransportConfigurationTLV(supportedConfiguration) - this.dataStreamTransportManagementService = service || this.constructService(); - this.setupServiceHandlers(); + this.dataStreamTransportManagementService = service || this.constructService() + this.setupServiceHandlers() } public destroy(): void { - this.dataStreamServer.destroy(); // removes ALL listeners + this.dataStreamServer.destroy() // removes ALL listeners this.dataStreamTransportManagementService.getCharacteristic(Characteristic.SetupDataStreamTransport) .removeOnGet() - .removeAllListeners(CharacteristicEventTypes.SET); - this.lastSetupDataStreamTransportResponse = ""; + .removeAllListeners(CharacteristicEventTypes.SET) + this.lastSetupDataStreamTransportResponse = '' } /** * @returns the DataStreamTransportManagement service */ getService(): DataStreamTransportManagement { - return this.dataStreamTransportManagementService; + return this.dataStreamTransportManagementService } /** @@ -104,8 +114,8 @@ export class DataStreamManagement { * @param handler - function to be called for every occurring event */ onEventMessage(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this { - this.dataStreamServer.onEventMessage(protocol, event, handler); - return this; + this.dataStreamServer.onEventMessage(protocol, event, handler) + return this } /** @@ -116,8 +126,8 @@ export class DataStreamManagement { * @param handler - registered event handler */ removeEventHandler(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this { - this.dataStreamServer.removeEventHandler(protocol, event, handler); - return this; + this.dataStreamServer.removeEventHandler(protocol, event, handler) + return this } /** @@ -130,8 +140,8 @@ export class DataStreamManagement { * @param handler - function to be called for every occurring request */ onRequestMessage(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this { - this.dataStreamServer.onRequestMessage(protocol, request, handler); - return this; + this.dataStreamServer.onRequestMessage(protocol, request, handler) + return this } /** @@ -142,8 +152,8 @@ export class DataStreamManagement { * @param handler - registered request handler */ removeRequestHandler(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this { - this.dataStreamServer.removeRequestHandler(protocol, request, handler); - return this; + this.dataStreamServer.removeRequestHandler(protocol, request, handler) + return this } /** @@ -154,72 +164,70 @@ export class DataStreamManagement { */ onServerEvent(event: DataStreamServerEvent, listener: (connection: DataStreamConnection) => void): this { // @ts-expect-error: event type - this.dataStreamServer.on(event, listener); - return this; + this.dataStreamServer.on(event, listener) + return this } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleSetupDataStreamTransportWrite(value: any, callback: CharacteristicSetCallback, connection: HAPConnection) { - const data = Buffer.from(value, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value, 'base64') + const objects = decode(data) - const sessionCommandType = objects[SetupDataStreamSessionTypes.SESSION_COMMAND_TYPE][0]; - const transportType = objects[SetupDataStreamSessionTypes.TRANSPORT_TYPE][0]; - const controllerKeySalt = objects[SetupDataStreamSessionTypes.CONTROLLER_KEY_SALT]; + const sessionCommandType = objects[SetupDataStreamSessionTypes.SESSION_COMMAND_TYPE][0] + const transportType = objects[SetupDataStreamSessionTypes.TRANSPORT_TYPE][0] + const controllerKeySalt = objects[SetupDataStreamSessionTypes.CONTROLLER_KEY_SALT] - debug("Received setup write with command %s and transport type %s", SessionCommandType[sessionCommandType], TransportType[transportType]); + debug('Received setup write with command %s and transport type %s', SessionCommandType[sessionCommandType], TransportType[transportType]) if (sessionCommandType === SessionCommandType.START_SESSION) { if (transportType !== TransportType.HOMEKIT_DATA_STREAM || controllerKeySalt.length !== 32) { - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } this.dataStreamServer.prepareSession(connection, controllerKeySalt, (error, preparedSession) => { if (error || !preparedSession) { - callback(error ?? new Error("PreparedSession was undefined!")); - return; + callback(error ?? new Error('PreparedSession was undefined!')) + return } - const listeningPort = tlv.encode(TransportSessionConfiguration.TCP_LISTENING_PORT, tlv.writeUInt16(preparedSession.port!)); + const listeningPort = encode(TransportSessionConfiguration.TCP_LISTENING_PORT, writeUInt16(preparedSession.port!)) let response: Buffer = Buffer.concat([ - tlv.encode(SetupDataStreamWriteResponseTypes.STATUS, DataStreamStatus.SUCCESS), - tlv.encode(SetupDataStreamWriteResponseTypes.TRANSPORT_TYPE_SESSION_PARAMETERS, listeningPort), - ]); - this.lastSetupDataStreamTransportResponse = response.toString("base64"); // save last response without accessory key salt + encode(SetupDataStreamWriteResponseTypes.STATUS, DataStreamStatus.SUCCESS), + encode(SetupDataStreamWriteResponseTypes.TRANSPORT_TYPE_SESSION_PARAMETERS, listeningPort), + ]) + this.lastSetupDataStreamTransportResponse = response.toString('base64') // save last response without accessory key salt response = Buffer.concat([ response, - tlv.encode(SetupDataStreamWriteResponseTypes.ACCESSORY_KEY_SALT, preparedSession.accessoryKeySalt), - ]); - callback(null, response.toString("base64")); - }); + encode(SetupDataStreamWriteResponseTypes.ACCESSORY_KEY_SALT, preparedSession.accessoryKeySalt), + ]) + callback(null, response.toString('base64')) + }) } else { - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) } } private buildSupportedDataStreamTransportConfigurationTLV(supportedConfiguration: TransportType[]): string { - const buffers: Buffer[] = []; - supportedConfiguration.forEach(type => { - const transportType = tlv.encode(TransportTypeTypes.TRANSPORT_TYPE, type); - const transferTransportConfiguration = tlv.encode(TransferTransportConfigurationTypes.TRANSFER_TRANSPORT_CONFIGURATION, transportType); + const buffers: Buffer[] = [] + supportedConfiguration.forEach((type) => { + const transportType = encode(TransportTypeTypes.TRANSPORT_TYPE, type) + const transferTransportConfiguration = encode(TransferTransportConfigurationTypes.TRANSFER_TRANSPORT_CONFIGURATION, transportType) - buffers.push(transferTransportConfiguration); - }); + buffers.push(transferTransportConfiguration) + }) - return Buffer.concat(buffers).toString("base64"); + return Buffer.concat(buffers).toString('base64') } private constructService(): DataStreamTransportManagement { - const dataStreamTransportManagement = new Service.DataStreamTransportManagement("", ""); + const dataStreamTransportManagement = new Service.DataStreamTransportManagement('', '') - dataStreamTransportManagement.setCharacteristic(Characteristic.SupportedDataStreamTransportConfiguration, this.supportedDataStreamTransportConfiguration); - dataStreamTransportManagement.setCharacteristic(Characteristic.Version, DataStreamServer.version); + dataStreamTransportManagement.setCharacteristic(Characteristic.SupportedDataStreamTransportConfiguration, this.supportedDataStreamTransportConfiguration) + dataStreamTransportManagement.setCharacteristic(Characteristic.Version, DataStreamServer.version) - return dataStreamTransportManagement; + return dataStreamTransportManagement } private setupServiceHandlers() { @@ -227,13 +235,12 @@ export class DataStreamManagement { .onGet(() => this.lastSetupDataStreamTransportResponse) .on(CharacteristicEventTypes.SET, (value, callback, context, connection) => { if (!connection) { - debug("Set event handler for SetupDataStreamTransport cannot be called from plugin! Connection undefined!"); - callback(HAPStatus.INVALID_VALUE_IN_REQUEST); - return; + debug('Set event handler for SetupDataStreamTransport cannot be called from plugin! Connection undefined!') + callback(HAPStatus.INVALID_VALUE_IN_REQUEST) + return } - this.handleSetupDataStreamTransportWrite(value, callback, connection); + this.handleSetupDataStreamTransportWrite(value, callback, connection) }) - .updateValue(this.lastSetupDataStreamTransportResponse); + .updateValue(this.lastSetupDataStreamTransportResponse) } - } diff --git a/src/lib/datastream/DataStreamParser.ts b/src/lib/datastream/DataStreamParser.ts index 3ec48710c..9fa77c056 100644 --- a/src/lib/datastream/DataStreamParser.ts +++ b/src/lib/datastream/DataStreamParser.ts @@ -1,32 +1,33 @@ -import * as uuid from "../util/uuid"; -import * as hapCrypto from "../util/hapCrypto"; -import assert from "assert"; -import createDebug from "debug"; +import assert from 'node:assert' +import { Buffer } from 'node:buffer' + +import createDebug from 'debug' + +import { writeUInt64LE } from '../util/hapCrypto.js' +import { isValid, unparse, write } from '../util/uuid.js' // welcome to hell :) // in this file lies madness and frustration. and It's not only about HDS. Also, JavaScript is hell -const debug = createDebug("HAP-NodeJS:DataStream:Parser"); +const debug = createDebug('HAP-NodeJS:DataStream:Parser') class Magics { - static readonly TERMINATOR = { type: "terminator" }; + static readonly TERMINATOR = { type: 'terminator' } } /** * @group HomeKit Data Streams (HDS) */ export class ValueWrapper { // basically used to differentiate between different sized integers when encoding (to force certain encoding) - - value: T; + value: T constructor(value: T) { - this.value = value; + this.value = value } - public equals(obj: ValueWrapper) : boolean { - return this.constructor.name === obj.constructor.name && obj.value === this.value; + public equals(obj: ValueWrapper): boolean { + return this.constructor.name === obj.constructor.name && obj.value === this.value } - } /** @@ -67,17 +68,16 @@ export class SecondsSince2001 extends ValueWrapper {} * @group HomeKit Data Streams (HDS) */ export class UUID extends ValueWrapper { - constructor(value: string) { - assert(uuid.isValid(value), "invalid uuid format"); - super(value); + assert(isValid(value), 'invalid uuid format') + super(value) } - } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum DataFormatTags { INVALID = 0x00, @@ -132,203 +132,201 @@ export const enum DataFormatTags { * @group HomeKit Data Streams (HDS) */ export class DataStreamParser { - // eslint-disable-next-line @typescript-eslint/no-explicit-any public static decode(buffer: DataStreamReader): any { - const tag = buffer.readTag(); + const tag = buffer.readTag() if (tag === DataFormatTags.INVALID) { - throw new Error("HDSDecoder: zero tag detected on index " + buffer.readerIndex); + throw new Error(`HDSDecoder: zero tag detected on index ${buffer.readerIndex}`) } else if (tag === DataFormatTags.TRUE) { - return buffer.readTrue(); + return buffer.readTrue() } else if (tag === DataFormatTags.FALSE) { - return buffer.readFalse(); + return buffer.readFalse() } else if (tag === DataFormatTags.TERMINATOR) { - return Magics.TERMINATOR; + return Magics.TERMINATOR } else if (tag === DataFormatTags.NULL) { - return null; + return null } else if (tag === DataFormatTags.UUID) { - return buffer.readUUID(); + return buffer.readUUID() } else if (tag === DataFormatTags.DATE) { - return buffer.readSecondsSince2001_01_01(); + return buffer.readSecondsSince2001_01_01() } else if (tag === DataFormatTags.INTEGER_MINUS_ONE) { - return buffer.readNegOne(); + return buffer.readNegOne() } else if (tag >= DataFormatTags.INTEGER_RANGE_START_0 && tag <= DataFormatTags.INTEGER_RANGE_STOP_39) { - return buffer.readIntRange(tag); // integer values from 0-39 + return buffer.readIntRange(tag) // integer values from 0-39 } else if (tag === DataFormatTags.INT8) { - return buffer.readInt8(); + return buffer.readInt8() } else if (tag === DataFormatTags.INT16LE) { - return buffer.readInt16LE(); + return buffer.readInt16LE() } else if (tag === DataFormatTags.INT32LE) { - return buffer.readInt32LE(); + return buffer.readInt32LE() } else if (tag === DataFormatTags.INT64LE) { - return buffer.readInt64LE(); + return buffer.readInt64LE() } else if (tag === DataFormatTags.FLOAT32LE) { - return buffer.readFloat32LE(); + return buffer.readFloat32LE() } else if (tag === DataFormatTags.FLOAT64LE) { - return buffer.readFloat64LE(); + return buffer.readFloat64LE() } else if (tag >= DataFormatTags.UTF8_LENGTH_START && tag <= DataFormatTags.UTF8_LENGTH_STOP) { - const length = tag - DataFormatTags.UTF8_LENGTH_START; - return buffer.readUTF8(length); + const length = tag - DataFormatTags.UTF8_LENGTH_START + return buffer.readUTF8(length) } else if (tag === DataFormatTags.UTF8_LENGTH8) { - return buffer.readUTF8_Length8(); + return buffer.readUTF8_Length8() } else if (tag === DataFormatTags.UTF8_LENGTH16LE) { - return buffer.readUTF8_Length16LE(); + return buffer.readUTF8_Length16LE() } else if (tag === DataFormatTags.UTF8_LENGTH32LE) { - return buffer.readUTF8_Length32LE(); + return buffer.readUTF8_Length32LE() } else if (tag === DataFormatTags.UTF8_LENGTH64LE) { - return buffer.readUTF8_Length64LE(); + return buffer.readUTF8_Length64LE() } else if (tag === DataFormatTags.UTF8_NULL_TERMINATED) { - return buffer.readUTF8_NULL_terminated(); + return buffer.readUTF8_NULL_terminated() } else if (tag >= DataFormatTags.DATA_LENGTH_START && tag <= DataFormatTags.DATA_LENGTH_STOP) { - const length = tag - DataFormatTags.DATA_LENGTH_START; - buffer.readData(length); + const length = tag - DataFormatTags.DATA_LENGTH_START + buffer.readData(length) } else if (tag === DataFormatTags.DATA_LENGTH8) { - return buffer.readData_Length8(); + return buffer.readData_Length8() } else if (tag === DataFormatTags.DATA_LENGTH16LE) { - return buffer.readData_Length16LE(); + return buffer.readData_Length16LE() } else if (tag === DataFormatTags.DATA_LENGTH32LE) { - return buffer.readData_Length32LE(); + return buffer.readData_Length32LE() } else if (tag === DataFormatTags.DATA_LENGTH64LE) { - return buffer.readData_Length64LE(); + return buffer.readData_Length64LE() } else if (tag === DataFormatTags.DATA_TERMINATED) { - return buffer.readData_terminated(); + return buffer.readData_terminated() } else if (tag >= DataFormatTags.COMPRESSION_START && tag <= DataFormatTags.COMPRESSION_STOP) { - const index = tag - DataFormatTags.COMPRESSION_START; - return buffer.decompressData(index); + const index = tag - DataFormatTags.COMPRESSION_START + return buffer.decompressData(index) } else if (tag >= DataFormatTags.ARRAY_LENGTH_START && tag <= DataFormatTags.ARRAY_LENGTH_STOP) { - const length = tag - DataFormatTags.ARRAY_LENGTH_START; - const array = []; + const length = tag - DataFormatTags.ARRAY_LENGTH_START + const array = [] for (let i = 0; i < length; i++) { - array.push(this.decode(buffer)); + array.push(this.decode(buffer)) } - return array; + return array } else if (tag === DataFormatTags.ARRAY_TERMINATED) { - const array = []; + const array = [] - let element; - while ((element = this.decode(buffer)) !== Magics.TERMINATOR) { - array.push(element); + let element = this.decode(buffer) + while (element !== Magics.TERMINATOR) { + array.push(element) + element = this.decode(buffer) } - return array; + return array } else if (tag >= DataFormatTags.DICTIONARY_LENGTH_START && tag <= DataFormatTags.DICTIONARY_LENGTH_STOP) { - const length = tag - DataFormatTags.DICTIONARY_LENGTH_START; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dictionary: Record = {}; + const length = tag - DataFormatTags.DICTIONARY_LENGTH_START + const dictionary: Record = {} for (let i = 0; i < length; i++) { - const key = this.decode(buffer); - dictionary[key] = this.decode(buffer); + const key = this.decode(buffer) + dictionary[key] = this.decode(buffer) } - return dictionary; + return dictionary } else if (tag === DataFormatTags.DICTIONARY_TERMINATED) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dictionary: Record = {}; + const dictionary: Record = {} - let key; - while ((key = this.decode(buffer)) !== Magics.TERMINATOR) { - dictionary[key] = this.decode(buffer); // decode value + let key = this.decode(buffer) + while (key !== Magics.TERMINATOR) { + dictionary[key] = this.decode(buffer) // decode value + key = this.decode(buffer) } - return dictionary; + return dictionary } else { - throw new Error("HDSDecoder: encountered unknown tag on index " + buffer.readerIndex + ": " + tag.toString(16)); + throw new Error(`HDSDecoder: encountered unknown tag on index ${buffer.readerIndex}: ${tag.toString(16)}`) } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any public static encode(data: any, buffer: DataStreamWriter): void { if (data === undefined) { - throw new Error("HDSEncoder: cannot encode undefined"); + throw new Error('HDSEncoder: cannot encode undefined') } if (data === null) { - buffer.writeTag(DataFormatTags.NULL); - return; + buffer.writeTag(DataFormatTags.NULL) + return } switch (typeof data) { - case "boolean": - if (data) { - buffer.writeTrue(); - } else { - buffer.writeFalse(); - } - break; - case "number": - if (Number.isInteger(data)) { - buffer.writeNumber(data); - } else { - buffer.writeFloat64LE(new Float64(data)); - } - break; - case "string": - buffer.writeUTF8(data); - break; - case "object": - if (Array.isArray(data)) { - const length = data.length; - - if (length <= 12) { - buffer.writeTag(DataFormatTags.ARRAY_LENGTH_START + length); + case 'boolean': + if (data) { + buffer.writeTrue() } else { - buffer.writeTag(DataFormatTags.ARRAY_TERMINATED); + buffer.writeFalse() } - - data.forEach(element => { - this.encode(element, buffer); - }); - - if (length > 12) { - buffer.writeTag(DataFormatTags.TERMINATOR); - } - } else if (data instanceof ValueWrapper) { - if (data instanceof Int8) { - buffer.writeInt8(data); - } else if (data instanceof Int16) { - buffer.writeInt16LE(data); - } else if (data instanceof Int32) { - buffer.writeInt32LE(data); - } else if (data instanceof Int64) { - buffer.writeInt64LE(data); - } else if (data instanceof Float32) { - buffer.writeFloat32LE(data); - } else if (data instanceof Float64) { - buffer.writeFloat64LE(data); - } else if (data instanceof SecondsSince2001) { - buffer.writeSecondsSince2001_01_01(data); - } else if (data instanceof UUID) { - buffer.writeUUID(data.value); + break + case 'number': + if (Number.isInteger(data)) { + buffer.writeNumber(data) } else { - throw new Error("Unknown wrapped object 'ValueWrapper' of class " + data.constructor.name); + buffer.writeFloat64LE(new Float64(data)) } - } else if (data instanceof Buffer) { - buffer.writeData(data); - } else { // object is treated as dictionary - const entries = Object.entries(data) - .filter(entry => entry[1] !== undefined); // explicitly setting undefined will result in an entry here - - if (entries.length <= 14) { - buffer.writeTag(DataFormatTags.DICTIONARY_LENGTH_START + entries.length); - } else { - buffer.writeTag(DataFormatTags.DICTIONARY_TERMINATED); - } - - entries.forEach(entry => { - this.encode(entry[0], buffer); // encode key - this.encode(entry[1], buffer); // encode value - }); - - if (entries.length > 14) { - buffer.writeTag(DataFormatTags.TERMINATOR); + break + case 'string': + buffer.writeUTF8(data) + break + case 'object': + if (Array.isArray(data)) { + const length = data.length + + if (length <= 12) { + buffer.writeTag(DataFormatTags.ARRAY_LENGTH_START + length) + } else { + buffer.writeTag(DataFormatTags.ARRAY_TERMINATED) + } + + data.forEach((element) => { + this.encode(element, buffer) + }) + + if (length > 12) { + buffer.writeTag(DataFormatTags.TERMINATOR) + } + } else if (data instanceof ValueWrapper) { + if (data instanceof Int8) { + buffer.writeInt8(data) + } else if (data instanceof Int16) { + buffer.writeInt16LE(data) + } else if (data instanceof Int32) { + buffer.writeInt32LE(data) + } else if (data instanceof Int64) { + buffer.writeInt64LE(data) + } else if (data instanceof Float32) { + buffer.writeFloat32LE(data) + } else if (data instanceof Float64) { + buffer.writeFloat64LE(data) + } else if (data instanceof SecondsSince2001) { + buffer.writeSecondsSince2001_01_01(data) + } else if (data instanceof UUID) { + buffer.writeUUID(data.value) + } else { + throw new TypeError(`Unknown wrapped object 'ValueWrapper' of class ${data.constructor.name}`) + } + } else if (data instanceof Buffer) { + buffer.writeData(data) + } else { // object is treated as dictionary + const entries = Object.entries(data) + .filter(entry => entry[1] !== undefined) // explicitly setting undefined will result in an entry here + + if (entries.length <= 14) { + buffer.writeTag(DataFormatTags.DICTIONARY_LENGTH_START + entries.length) + } else { + buffer.writeTag(DataFormatTags.DICTIONARY_TERMINATED) + } + + entries.forEach((entry) => { + this.encode(entry[0], buffer) // encode key + this.encode(entry[1], buffer) // encode value + }) + + if (entries.length > 14) { + buffer.writeTag(DataFormatTags.TERMINATOR) + } } - } - break; - default: - throw new Error("HDSEncoder: no idea how to encode value of type '" + (typeof data) +"': " + data); + break + default: + throw new Error(`HDSEncoder: no idea how to encode value of type '${typeof data}': ${data}`) } } } @@ -337,277 +335,272 @@ export class DataStreamParser { * @group HomeKit Data Streams (HDS) */ export class DataStreamReader { + private readonly data: Buffer + readerIndex: number - private readonly data: Buffer; - readerIndex: number; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private trackedCompressedData: any[] = []; + private trackedCompressedData: any[] = [] constructor(data: Buffer) { - this.data = data; - this.readerIndex = 0; + this.data = data + this.readerIndex = 0 } finished(): void { if (this.readerIndex < this.data.length) { - const remainingHex = this.data.slice(this.readerIndex, this.data.length).toString("hex"); - debug("WARNING Finished reading HDS stream, but there are still %d bytes remaining () %s", this.data.length - this.readerIndex, remainingHex); + const remainingHex = this.data.subarray(this.readerIndex, this.data.length).toString('hex') + debug('WARNING Finished reading HDS stream, but there are still %d bytes remaining () %s', this.data.length - this.readerIndex, remainingHex) } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any decompressData(index: number): any { if (index >= this.trackedCompressedData.length) { - throw new Error("HDSDecoder: Tried decompression of data for an index out of range (index " + index + - " and got " + this.trackedCompressedData.length + " elements)"); + throw new Error(`HDSDecoder: Tried decompression of data for an index out of range (index ${index + } and got ${this.trackedCompressedData.length} elements)`) } - return this.trackedCompressedData[index]; + return this.trackedCompressedData[index] } private trackData(data: T): T { - this.trackedCompressedData.push(data); - return data; + this.trackedCompressedData.push(data) + return data } private ensureLength(bytes: number) { if (this.readerIndex + bytes > this.data.length) { - const remaining = this.data.length - this.readerIndex; - throw new Error("HDSDecoder: End of data stream. Tried reading " + bytes + " bytes however got only " + remaining + " remaining!"); + const remaining = this.data.length - this.readerIndex + throw new Error(`HDSDecoder: End of data stream. Tried reading ${bytes} bytes however got only ${remaining} remaining!`) } } readTag(): number { - this.ensureLength(1); - return this.data.readUInt8(this.readerIndex++); + this.ensureLength(1) + return this.data.readUInt8(this.readerIndex++) } readTrue(): true { - return this.trackData(true); // do those tag encoded values get cached? + return this.trackData(true) // do those tag encoded values get cached? } readFalse(): false { - return this.trackData(false); + return this.trackData(false) } readNegOne(): -1 { - return this.trackData(-1); + return this.trackData(-1) } readIntRange(tag: number): number { - return this.trackData(tag - DataFormatTags.INTEGER_RANGE_START_0); // integer values from 0-39 + return this.trackData(tag - DataFormatTags.INTEGER_RANGE_START_0) // integer values from 0-39 } readInt8(): number { - this.ensureLength(1); - return this.trackData(this.data.readInt8(this.readerIndex++)); + this.ensureLength(1) + return this.trackData(this.data.readInt8(this.readerIndex++)) } readInt16LE(): number { - this.ensureLength(2); - const value = this.data.readInt16LE(this.readerIndex); - this.readerIndex += 2; - return this.trackData(value); + this.ensureLength(2) + const value = this.data.readInt16LE(this.readerIndex) + this.readerIndex += 2 + return this.trackData(value) } readInt32LE(): number { - this.ensureLength(4); - const value = this.data.readInt32LE(this.readerIndex); - this.readerIndex += 4; - return this.trackData(value); + this.ensureLength(4) + const value = this.data.readInt32LE(this.readerIndex) + this.readerIndex += 4 + return this.trackData(value) } readInt64LE(): number { - this.ensureLength(8); + this.ensureLength(8) - const low = this.data.readInt32LE(this.readerIndex); - let value = this.data.readInt32LE(this.readerIndex + 4) * 0x100000000 + low; + const low = this.data.readInt32LE(this.readerIndex) + let value = this.data.readInt32LE(this.readerIndex + 4) * 0x100000000 + low if (low < 0) { - value += 0x100000000; + value += 0x100000000 } - this.readerIndex += 8; - return this.trackData(value); + this.readerIndex += 8 + return this.trackData(value) } readFloat32LE(): number { - this.ensureLength(4); - const value = this.data.readFloatLE(this.readerIndex); - this.readerIndex += 4; - return this.trackData(value); + this.ensureLength(4) + const value = this.data.readFloatLE(this.readerIndex) + this.readerIndex += 4 + return this.trackData(value) } readFloat64LE(): number { - this.ensureLength(8); - const value = this.data.readDoubleLE(this.readerIndex); - return this.trackData(value); + this.ensureLength(8) + const value = this.data.readDoubleLE(this.readerIndex) + return this.trackData(value) } private readLength8(): number { - this.ensureLength(1); - return this.data.readUInt8(this.readerIndex++); + this.ensureLength(1) + return this.data.readUInt8(this.readerIndex++) } private readLength16LE(): number { - this.ensureLength(2); - const value = this.data.readUInt16LE(this.readerIndex); - this.readerIndex += 2; - return value; + this.ensureLength(2) + const value = this.data.readUInt16LE(this.readerIndex) + this.readerIndex += 2 + return value } private readLength32LE(): number { - this.ensureLength(4); - const value = this.data.readUInt32LE(this.readerIndex); - this.readerIndex += 4; - return value; + this.ensureLength(4) + const value = this.data.readUInt32LE(this.readerIndex) + this.readerIndex += 4 + return value } private readLength64LE(): number { - this.ensureLength(8); + this.ensureLength(8) - const low = this.data.readUInt32LE(this.readerIndex); - const value = this.data.readUInt32LE(this.readerIndex + 4) * 0x100000000 + low; + const low = this.data.readUInt32LE(this.readerIndex) + const value = this.data.readUInt32LE(this.readerIndex + 4) * 0x100000000 + low - this.readerIndex += 8; - return value; + this.readerIndex += 8 + return value } readUTF8(length: number): string { - this.ensureLength(length); - const value = this.data.toString("utf8", this.readerIndex, this.readerIndex + length); - this.readerIndex += length; - return this.trackData(value); + this.ensureLength(length) + const value = this.data.toString('utf8', this.readerIndex, this.readerIndex + length) + this.readerIndex += length + return this.trackData(value) } readUTF8_Length8(): string { - const length = this.readLength8(); - return this.readUTF8(length); + const length = this.readLength8() + return this.readUTF8(length) } readUTF8_Length16LE(): string { - const length = this.readLength16LE(); - return this.readUTF8(length); + const length = this.readLength16LE() + return this.readUTF8(length) } readUTF8_Length32LE(): string { - const length = this.readLength32LE(); - return this.readUTF8(length); + const length = this.readLength32LE() + return this.readUTF8(length) } readUTF8_Length64LE(): string { - const length = this.readLength64LE(); - return this.readUTF8(length); + const length = this.readLength64LE() + return this.readUTF8(length) } readUTF8_NULL_terminated(): string { - let offset = this.readerIndex; - let nextByte; + let offset = this.readerIndex + let nextByte for (;;) { - nextByte = this.data[offset]; + nextByte = this.data[offset] if (nextByte === undefined) { - throw new Error("HDSDecoder: Reached end of data stream while reading NUL terminated string!"); - } else if (nextByte === 0) { - break; + throw new Error('HDSDecoder: Reached end of data stream while reading NUL terminated string!') + } else if (nextByte === 0) { + break } else { - offset++; + offset++ } } - const value = this.data.toString("utf8", this.readerIndex, offset); - this.readerIndex = offset + 1; - return this.trackData(value); + const value = this.data.toString('utf8', this.readerIndex, offset) + this.readerIndex = offset + 1 + return this.trackData(value) } readData(length: number): Buffer { - this.ensureLength(length); - const value = this.data.slice(this.readerIndex, this.readerIndex + length); - this.readerIndex += length; + this.ensureLength(length) + const value = this.data.subarray(this.readerIndex, this.readerIndex + length) + this.readerIndex += length - return this.trackData(value); + return this.trackData(value) } readData_Length8(): Buffer { - const length = this.readLength8(); - return this.readData(length); + const length = this.readLength8() + return this.readData(length) } readData_Length16LE(): Buffer { - const length = this.readLength16LE(); - return this.readData(length); + const length = this.readLength16LE() + return this.readData(length) } readData_Length32LE(): Buffer { - const length = this.readLength32LE(); - return this.readData(length); + const length = this.readLength32LE() + return this.readData(length) } readData_Length64LE(): Buffer { - const length = this.readLength64LE(); - return this.readData(length); + const length = this.readLength64LE() + return this.readData(length) } readData_terminated(): Buffer { - let offset = this.readerIndex; - let nextByte; + let offset = this.readerIndex + let nextByte for (;;) { - nextByte = this.data[offset]; + nextByte = this.data[offset] if (nextByte === undefined) { - throw new Error("HDSDecoder: Reached end of data stream while reading terminated data!"); - } else if (nextByte === DataFormatTags.TERMINATOR) { - break; + throw new Error('HDSDecoder: Reached end of data stream while reading terminated data!') + } else if (nextByte === DataFormatTags.TERMINATOR) { + break } else { - offset++; + offset++ } } - const value = this.data.slice(this.readerIndex, offset); - this.readerIndex = offset + 1; - return this.trackData(value); + const value = this.data.subarray(this.readerIndex, offset) + this.readerIndex = offset + 1 + return this.trackData(value) } readSecondsSince2001_01_01(): number { // second since 2001-01-01 00:00:00 - return this.readFloat64LE(); + return this.readFloat64LE() } readUUID(): string { // big endian - this.ensureLength(16); - const value = uuid.unparse(this.data, this.readerIndex); - this.readerIndex += 16; - return this.trackData(value); + this.ensureLength(16) + const value = unparse(this.data, this.readerIndex) + this.readerIndex += 16 + return this.trackData(value) } - } class WrittenDataList { // wrapper class since javascript doesn't really have a way to override === operator - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private writtenData: any[] = []; + private writtenData: any[] = [] push(data: T) { - this.writtenData.push(data); + this.writtenData.push(data) } indexOf(data: T) { for (let i = 0; i < this.writtenData.length; i++) { - const data0 = this.writtenData[i]; + const data0 = this.writtenData[i] if (data === data0) { - return i; + return i } if (data instanceof ValueWrapper && data0 instanceof ValueWrapper) { if (data.equals(data0)) { - return i; + return i } } } - return -1; + return -1 } } @@ -615,336 +608,335 @@ class WrittenDataList { // wrapper class since javascript doesn't really have a * @group HomeKit Data Streams (HDS) */ export class DataStreamWriter { + private static readonly chunkSize = 128 // seems to be a good default - private static readonly chunkSize = 128; // seems to be a good default - - private data: Buffer; - private writerIndex: number; + private data: Buffer + private writerIndex: number - private writtenData = new WrittenDataList(); + private writtenData = new WrittenDataList() constructor() { - this.data = Buffer.alloc(DataStreamWriter.chunkSize); - this.writerIndex = 0; + this.data = Buffer.alloc(DataStreamWriter.chunkSize) + this.writerIndex = 0 } length(): number { - return this.writerIndex; // since writerIndex points to the next FREE index it also represents the length + return this.writerIndex // since writerIndex points to the next FREE index it also represents the length } getData(): Buffer { - return this.data.slice(0, this.writerIndex); + return this.data.subarray(0, this.writerIndex) } private ensureLength(bytes: number) { - const neededBytes = (this.writerIndex + bytes) - this.data.length; + const neededBytes = (this.writerIndex + bytes) - this.data.length if (neededBytes > 0) { - const chunks = Math.ceil(neededBytes / DataStreamWriter.chunkSize); + const chunks = Math.ceil(neededBytes / DataStreamWriter.chunkSize) // don't know if it's best for performance to immediately concatenate the buffers. That way it's // the easiest way to handle writing though. - this.data = Buffer.concat([this.data, Buffer.alloc(chunks * DataStreamWriter.chunkSize)]); + this.data = Buffer.concat([this.data, Buffer.alloc(chunks * DataStreamWriter.chunkSize)]) } } private compressDataIfPossible(data: T): boolean { - const index = this.writtenData.indexOf(data); + const index = this.writtenData.indexOf(data) if (index < 0) { // data is not present yet - this.writtenData.push(data); - return false; + this.writtenData.push(data) + return false } else if (index <= DataFormatTags.COMPRESSION_STOP - DataFormatTags.COMPRESSION_START) { // data was already written and the index is in the applicable range => shorten the payload - this.writeTag(DataFormatTags.COMPRESSION_START + index); - return true; + this.writeTag(DataFormatTags.COMPRESSION_START + index) + return true } - return false; + return false } writeTag(tag: DataFormatTags): void { - this.ensureLength(1); - this.data.writeUInt8(tag, this.writerIndex++); + this.ensureLength(1) + this.data.writeUInt8(tag, this.writerIndex++) } writeTrue(): void { - this.writeTag(DataFormatTags.TRUE); + this.writeTag(DataFormatTags.TRUE) } writeFalse(): void { - this.writeTag(DataFormatTags.FALSE); + this.writeTag(DataFormatTags.FALSE) } writeNumber(number: number): void { if (number === -1) { - this.writeTag(DataFormatTags.INTEGER_MINUS_ONE); + this.writeTag(DataFormatTags.INTEGER_MINUS_ONE) } else if (number >= 0 && number <= 39) { - this.writeTag(DataFormatTags.INTEGER_RANGE_START_0 + number); + this.writeTag(DataFormatTags.INTEGER_RANGE_START_0 + number) } else if (number >= -128 && number <= 127) { - this.writeInt8(new Int8(number)); + this.writeInt8(new Int8(number)) } else if (number >= -32768 && number <= 32767) { - this.writeInt16LE(new Int16(number)); + this.writeInt16LE(new Int16(number)) } else if (number >= -2147483648 && number <= -2147483648) { - this.writeInt32LE(new Int32(number)); + this.writeInt32LE(new Int32(number)) } else if (number >= Number.MIN_SAFE_INTEGER && number <= Number.MAX_SAFE_INTEGER) { // use correct uin64 restriction when we convert to bigint - this.writeInt64LE(new Int64(number)); + this.writeInt64LE(new Int64(number)) } else { - throw new Error("Tried writing unrepresentable number (" + number + ")"); + throw new Error(`Tried writing unrepresentable number (${number})`) } } writeInt8(int8: Int8): void { if (this.compressDataIfPossible(int8)) { - return; + return } - this.ensureLength(2); - this.writeTag(DataFormatTags.INT8); - this.data.writeInt8(int8.value, this.writerIndex++); + this.ensureLength(2) + this.writeTag(DataFormatTags.INT8) + this.data.writeInt8(int8.value, this.writerIndex++) } writeInt16LE(int16: Int16): void { if (this.compressDataIfPossible(int16)) { - return; + return } - this.ensureLength(3); - this.writeTag(DataFormatTags.INT16LE); - this.data.writeInt16LE(int16.value, this.writerIndex); - this.writerIndex += 2; + this.ensureLength(3) + this.writeTag(DataFormatTags.INT16LE) + this.data.writeInt16LE(int16.value, this.writerIndex) + this.writerIndex += 2 } writeInt32LE(int32: Int32): void { if (this.compressDataIfPossible(int32)) { - return; + return } - this.ensureLength(5); - this.writeTag(DataFormatTags.INT32LE); - this.data.writeInt32LE(int32.value, this.writerIndex); - this.writerIndex += 4; + this.ensureLength(5) + this.writeTag(DataFormatTags.INT32LE) + this.data.writeInt32LE(int32.value, this.writerIndex) + this.writerIndex += 4 } writeInt64LE(int64: Int64): void { if (this.compressDataIfPossible(int64)) { - return; + return } - this.ensureLength(9); - this.writeTag(DataFormatTags.INT64LE); - this.data.writeUInt32LE(int64.value, this.writerIndex);// TODO correctly implement int64; currently it's basically an int32 - this.data.writeUInt32LE(0, this.writerIndex + 4); - this.writerIndex += 8; + this.ensureLength(9) + this.writeTag(DataFormatTags.INT64LE) + this.data.writeUInt32LE(int64.value, this.writerIndex)// TODO correctly implement int64; currently it's basically an int32 + this.data.writeUInt32LE(0, this.writerIndex + 4) + this.writerIndex += 8 } writeFloat32LE(float32: Float32): void { if (this.compressDataIfPossible(float32)) { - return; + return } - this.ensureLength(5); - this.writeTag(DataFormatTags.FLOAT32LE); - this.data.writeFloatLE(float32.value, this.writerIndex); - this.writerIndex += 4; + this.ensureLength(5) + this.writeTag(DataFormatTags.FLOAT32LE) + this.data.writeFloatLE(float32.value, this.writerIndex) + this.writerIndex += 4 } writeFloat64LE(float64: Float64): void { if (this.compressDataIfPossible(float64)) { - return; + return } - this.ensureLength(9); - this.writeTag(DataFormatTags.FLOAT64LE); - this.data.writeDoubleLE(float64.value, this.writerIndex); - this.writerIndex += 8; + this.ensureLength(9) + this.writeTag(DataFormatTags.FLOAT64LE) + this.data.writeDoubleLE(float64.value, this.writerIndex) + this.writerIndex += 8 } private writeLength8(length: number): void { - this.ensureLength(1); - this.data.writeUInt8(length, this.writerIndex++); + this.ensureLength(1) + this.data.writeUInt8(length, this.writerIndex++) } private writeLength16LE(length: number): void { - this.ensureLength(2); - this.data.writeUInt16LE(length, this.writerIndex); - this.writerIndex += 2; + this.ensureLength(2) + this.data.writeUInt16LE(length, this.writerIndex) + this.writerIndex += 2 } private writeLength32LE(length: number): void { - this.ensureLength(4); - this.data.writeUInt32LE(length, this.writerIndex); - this.writerIndex += 4; + this.ensureLength(4) + this.data.writeUInt32LE(length, this.writerIndex) + this.writerIndex += 4 } private writeLength64LE(length: number): void { - this.ensureLength(8); - hapCrypto.writeUInt64LE(length, this.data, this.writerIndex); - this.writerIndex += 8; + this.ensureLength(8) + writeUInt64LE(length, this.data, this.writerIndex) + this.writerIndex += 8 } writeUTF8(utf8: string): void { if (this.compressDataIfPossible(utf8)) { - return; + return } - const length = Buffer.byteLength(utf8); + const length = Buffer.byteLength(utf8) if (length <= 32) { - this.ensureLength(1 + length); - this.writeTag(DataFormatTags.UTF8_LENGTH_START + utf8.length); - this._writeUTF8(utf8); + this.ensureLength(1 + length) + this.writeTag(DataFormatTags.UTF8_LENGTH_START + utf8.length) + this._writeUTF8(utf8) } else if (length <= 255) { - this.writeUTF8_Length8(utf8); + this.writeUTF8_Length8(utf8) } else if (length <= 65535) { - this.writeUTF8_Length16LE(utf8); + this.writeUTF8_Length16LE(utf8) } else if (length <= 4294967295) { - this.writeUTF8_Length32LE(utf8); + this.writeUTF8_Length32LE(utf8) } else if (length <= Number.MAX_SAFE_INTEGER) { // use correct uin64 restriction when we convert to bigint - this.writeUTF8_Length64LE(utf8); + this.writeUTF8_Length64LE(utf8) } else { - this.writeUTF8_NULL_terminated(utf8); + this.writeUTF8_NULL_terminated(utf8) } } private _writeUTF8(utf8: string): void { // utility method - const byteLength = Buffer.byteLength(utf8); - this.ensureLength(byteLength); + const byteLength = Buffer.byteLength(utf8) + this.ensureLength(byteLength) - this.data.write(utf8, this.writerIndex, byteLength, "utf8"); - this.writerIndex += byteLength; + this.data.write(utf8, this.writerIndex, byteLength, 'utf8') + this.writerIndex += byteLength } private writeUTF8_Length8(utf8: string): void { - const length = Buffer.byteLength(utf8); - this.ensureLength(2 + length); + const length = Buffer.byteLength(utf8) + this.ensureLength(2 + length) - this.writeTag(DataFormatTags.UTF8_LENGTH8); - this.writeLength8(length); - this._writeUTF8(utf8); + this.writeTag(DataFormatTags.UTF8_LENGTH8) + this.writeLength8(length) + this._writeUTF8(utf8) } private writeUTF8_Length16LE(utf8: string): void { - const length = Buffer.byteLength(utf8); - this.ensureLength(3 + length); + const length = Buffer.byteLength(utf8) + this.ensureLength(3 + length) - this.writeTag(DataFormatTags.UTF8_LENGTH16LE); - this.writeLength16LE(length); - this._writeUTF8(utf8); + this.writeTag(DataFormatTags.UTF8_LENGTH16LE) + this.writeLength16LE(length) + this._writeUTF8(utf8) } private writeUTF8_Length32LE(utf8: string): void { - const length = Buffer.byteLength(utf8); - this.ensureLength(5 + length); + const length = Buffer.byteLength(utf8) + this.ensureLength(5 + length) - this.writeTag(DataFormatTags.UTF8_LENGTH32LE); - this.writeLength32LE(length); - this._writeUTF8(utf8); + this.writeTag(DataFormatTags.UTF8_LENGTH32LE) + this.writeLength32LE(length) + this._writeUTF8(utf8) } private writeUTF8_Length64LE(utf8: string): void { - const length = Buffer.byteLength(utf8); - this.ensureLength(9 + length); + const length = Buffer.byteLength(utf8) + this.ensureLength(9 + length) - this.writeTag(DataFormatTags.UTF8_LENGTH64LE); - this.writeLength64LE(length); - this._writeUTF8(utf8); + this.writeTag(DataFormatTags.UTF8_LENGTH64LE) + this.writeLength64LE(length) + this._writeUTF8(utf8) } private writeUTF8_NULL_terminated(utf8: string): void { - this.ensureLength(1 + Buffer.byteLength(utf8) + 1); + this.ensureLength(1 + Buffer.byteLength(utf8) + 1) - this.writeTag(DataFormatTags.UTF8_NULL_TERMINATED); - this._writeUTF8(utf8); - this.data.writeUInt8(0, this.writerIndex++); + this.writeTag(DataFormatTags.UTF8_NULL_TERMINATED) + this._writeUTF8(utf8) + this.data.writeUInt8(0, this.writerIndex++) } writeData(data: Buffer): void { if (this.compressDataIfPossible(data)) { - return; + return } if (data.length <= 32) { - this.writeTag(DataFormatTags.DATA_LENGTH_START + data.length); - this._writeData(data); + this.writeTag(DataFormatTags.DATA_LENGTH_START + data.length) + this._writeData(data) } else if (data.length <= 255) { - this.writeData_Length8(data); + this.writeData_Length8(data) } else if (data.length <= 65535) { - this.writeData_Length16LE(data); + this.writeData_Length16LE(data) } else if (data.length <= 4294967295) { - this.writeData_Length32LE(data); + this.writeData_Length32LE(data) } else if (data.length <= Number.MAX_SAFE_INTEGER) { - this.writeData_Length64LE(data); + this.writeData_Length64LE(data) } else { - this.writeData_terminated(data); + this.writeData_terminated(data) } } private _writeData(data: Buffer): void { // utility method - this.ensureLength(data.length); + this.ensureLength(data.length) for (let i = 0; i < data.length; i++) { - this.data[this.writerIndex++] = data[i]; + this.data[this.writerIndex++] = data[i] } } private writeData_Length8(data: Buffer): void { - this.ensureLength(2 + data.length); + this.ensureLength(2 + data.length) - this.writeTag(DataFormatTags.DATA_LENGTH8); - this.writeLength8(data.length); - this._writeData(data); + this.writeTag(DataFormatTags.DATA_LENGTH8) + this.writeLength8(data.length) + this._writeData(data) } private writeData_Length16LE(data: Buffer): void { - this.ensureLength(3 + data.length); + this.ensureLength(3 + data.length) - this.writeTag(DataFormatTags.DATA_LENGTH16LE); - this.writeLength16LE(data.length); - this._writeData(data); + this.writeTag(DataFormatTags.DATA_LENGTH16LE) + this.writeLength16LE(data.length) + this._writeData(data) } private writeData_Length32LE(data: Buffer): void { - this.ensureLength(5 + data.length); + this.ensureLength(5 + data.length) - this.writeTag(DataFormatTags.DATA_LENGTH32LE); - this.writeLength32LE(data.length); - this._writeData(data); + this.writeTag(DataFormatTags.DATA_LENGTH32LE) + this.writeLength32LE(data.length) + this._writeData(data) } private writeData_Length64LE(data: Buffer): void { - this.ensureLength(9 + data.length); + this.ensureLength(9 + data.length) - this.writeTag(DataFormatTags.DATA_LENGTH64LE); - this.writeLength64LE(data.length); - this._writeData(data); + this.writeTag(DataFormatTags.DATA_LENGTH64LE) + this.writeLength64LE(data.length) + this._writeData(data) } private writeData_terminated(data: Buffer): void { - this.ensureLength(1 + data.length + 1); + this.ensureLength(1 + data.length + 1) - this.writeTag(DataFormatTags.DATA_TERMINATED); - this._writeData(data); - this.writeTag(DataFormatTags.TERMINATOR); + this.writeTag(DataFormatTags.DATA_TERMINATED) + this._writeData(data) + this.writeTag(DataFormatTags.TERMINATOR) } writeSecondsSince2001_01_01(seconds: SecondsSince2001): void { if (this.compressDataIfPossible(seconds)) { - return; + return } - this.ensureLength(9); - this.writeTag(DataFormatTags.DATE); - this.data.writeDoubleLE(seconds.value, this.writerIndex); - this.writerIndex += 8; + this.ensureLength(9) + this.writeTag(DataFormatTags.DATE) + this.data.writeDoubleLE(seconds.value, this.writerIndex) + this.writerIndex += 8 } writeUUID(uuid_string: string): void { - assert(uuid.isValid(uuid_string), "supplied uuid is invalid"); + assert(isValid(uuid_string), 'supplied uuid is invalid') if (this.compressDataIfPossible(new UUID(uuid_string))) { - return; + return } - this.ensureLength(17); - this.writeTag(DataFormatTags.UUID); - uuid.write(uuid_string, this.data, this.writerIndex); - this.writerIndex += 16; + this.ensureLength(17) + this.writeTag(DataFormatTags.UUID) + write(uuid_string, this.data, this.writerIndex) + this.writerIndex += 16 } } diff --git a/src/lib/datastream/DataStreamServer.ts b/src/lib/datastream/DataStreamServer.ts index f5fb37dc1..02acd8932 100644 --- a/src/lib/datastream/DataStreamServer.ts +++ b/src/lib/datastream/DataStreamServer.ts @@ -1,104 +1,121 @@ -import assert from "assert"; -import crypto from "crypto"; -import createDebug from "debug"; -import { EventEmitter, EventEmitter as NodeEventEmitter } from "events"; -import net, { Socket } from "net"; -import { HAPConnection, HAPConnectionEvent } from "../util/eventedhttp"; +/* global NodeJS */ +import type { Server, Socket } from 'node:net' -import * as hapCrypto from "../util/hapCrypto"; -import { DataStreamParser, DataStreamReader, DataStreamWriter, Int64 } from "./DataStreamParser"; +import type { HAPConnection } from '../util/eventedhttp' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' +import { EventEmitter, EventEmitter as NodeEventEmitter } from 'node:events' +import { createServer } from 'node:net' -const debug = createDebug("HAP-NodeJS:DataStream:Server"); +import createDebug from 'debug' + +import { HAPConnectionEvent } from '../util/eventedhttp.js' +import { + chacha20_poly1305_decryptAndVerify, + chacha20_poly1305_encryptAndSeal, + HKDF, + writeUInt64LE, +} from '../util/hapCrypto.js' +import { DataStreamParser, DataStreamReader, DataStreamWriter, Int64 } from './DataStreamParser.js' + +const debug = createDebug('HAP-NodeJS:DataStream:Server') /** * @group HomeKit Data Streams (HDS) */ -export type PreparedDataStreamSession = { +export interface PreparedDataStreamSession { - connection: HAPConnection, // reference to the hap session which created the request + connection: HAPConnection // reference to the hap session which created the request - accessoryToControllerEncryptionKey: Buffer, - controllerToAccessoryEncryptionKey: Buffer, - accessoryKeySalt: Buffer, + accessoryToControllerEncryptionKey: Buffer + controllerToAccessoryEncryptionKey: Buffer + accessoryKeySalt: Buffer - port?: number, + port?: number - connectTimeout?: NodeJS.Timeout, // 10s timer + connectTimeout?: NodeJS.Timeout // 10s timer } /** * @group HomeKit Data Streams (HDS) */ -export type PrepareSessionCallback = (error?: Error, preparedSession?: PreparedDataStreamSession) => void; +export type PrepareSessionCallback = (error?: Error, preparedSession?: PreparedDataStreamSession) => void /** * @group HomeKit Data Streams (HDS) */ -export type EventHandler = (message: Record) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +export type EventHandler = (message: Record) => void /** * @group HomeKit Data Streams (HDS) */ -export type RequestHandler = (id: number, message: Record) => void; // eslint-disable-line @typescript-eslint/no-explicit-any +export type RequestHandler = (id: number, message: Record) => void /** * @group HomeKit Data Streams (HDS) */ export type ResponseHandler = ( error: Error | undefined, status: HDSStatus | undefined, - message: Record) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + message: Record +) => void /** * @group HomeKit Data Streams (HDS) */ export type GlobalEventHandler = ( connection: DataStreamConnection, - message: Record) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + message: Record +) => void /** * @group HomeKit Data Streams (HDS) */ export type GlobalRequestHandler = ( - connection: DataStreamConnection, id: number, - message: Record) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + connection: DataStreamConnection, + id: number, + message: Record +) => void /** * @group HomeKit Data Streams (HDS) */ export interface DataStreamProtocolHandler { - eventHandler?: Record; - requestHandler?: Record; + eventHandler?: Record + requestHandler?: Record } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum Protocols { // a collection of currently known protocols - CONTROL = "control", - TARGET_CONTROL = "targetControl", - DATA_SEND = "dataSend", + CONTROL = 'control', + TARGET_CONTROL = 'targetControl', + DATA_SEND = 'dataSend', } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum Topics { // a collection of currently known topics grouped by their protocol // control - HELLO = "hello", + HELLO = 'hello', // targetControl - WHOAMI = "whoami", + WHOAMI = 'whoami', // dataSend - OPEN = "open", - DATA = "data", - ACK = "ack", - CLOSE = "close", + OPEN = 'open', + DATA = 'data', + ACK = 'ack', + CLOSE = 'close', } /** * @group HomeKit Data Streams (HDS) */ + export enum HDSStatus { - // noinspection JSUnusedGlobalSymbols SUCCESS = 0, OUT_OF_MEMORY = 1, TIMEOUT = 2, @@ -111,8 +128,8 @@ export enum HDSStatus { /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum HDSProtocolSpecificErrorReason { // close reason used in the dataSend protocol - // noinspection JSUnusedGlobalSymbols NORMAL = 0, NOT_ALLOWED = 1, BUSY = 2, @@ -131,21 +148,21 @@ export const enum HDSProtocolSpecificErrorReason { // close reason used in the d * @group HomeKit Data Streams (HDS) */ export class HDSProtocolError extends Error { - reason: HDSProtocolSpecificErrorReason; + reason: HDSProtocolSpecificErrorReason /** * Initializes a new `HDSProtocolError` * @param reason - The {@link HDSProtocolSpecificErrorReason}. - * Values MUST NOT be {@link HDSProtocolSpecificErrorReason.NORMAL}. + * Values MUST NOT be {@link HDSProtocolSpecificErrorReason.NORMAL}. */ constructor(reason: HDSProtocolSpecificErrorReason) { - super("HDSProtocolError: " + reason); - assert(reason !== HDSProtocolSpecificErrorReason.NORMAL, "Cannot initialize a HDSProtocolError with NORMAL!"); - this.reason = reason; + super(`HDSProtocolError: ${reason}`) + assert(reason !== HDSProtocolSpecificErrorReason.NORMAL, 'Cannot initialize a HDSProtocolError with NORMAL!') + this.reason = reason } } - +// eslint-disable-next-line no-restricted-syntax const enum ServerState { UNINITIALIZED, // server socket hasn't been created BINDING, // server is created and is currently trying to bind @@ -153,6 +170,7 @@ const enum ServerState { CLOSING, } +// eslint-disable-next-line no-restricted-syntax const enum ConnectionState { UNIDENTIFIED, EXPECTING_HELLO, @@ -164,16 +182,17 @@ const enum ConnectionState { /** * @group HomeKit Data Streams (HDS) */ -export type HDSFrame = { - header: Buffer, - cipheredPayload: Buffer, - authTag: Buffer, - plaintextPayload?: Buffer, +export interface HDSFrame { + header: Buffer + cipheredPayload: Buffer + authTag: Buffer + plaintextPayload?: Buffer } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum MessageType { EVENT = 1, REQUEST = 2, @@ -183,70 +202,70 @@ export const enum MessageType { /** * @group HomeKit Data Streams (HDS) */ -export type DataStreamMessage = { - type: MessageType, +export interface DataStreamMessage { + type: MessageType - protocol: string, - topic: string, - id?: number, // for requests and responses - status?: HDSStatus, // for responses + protocol: string + topic: string + id?: number // for requests and responses + status?: HDSStatus // for responses - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: Record, + message: Record } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum DataStreamServerEvent { /** * This event is emitted when a new client socket is received. At this point we have no idea to what * hap session this connection will be matched. */ - CONNECTION_OPENED = "connection-opened", + CONNECTION_OPENED = 'connection-opened', /** * This event is emitted when the socket of a connection gets closed. */ - CONNECTION_CLOSED = "connection-closed", + CONNECTION_CLOSED = 'connection-closed', } /** * @group HomeKit Data Streams (HDS) */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface DataStreamServer { - on(event: "connection-opened", listener: (connection: DataStreamConnection) => void): this; - on(event: "connection-closed", listener: (connection: DataStreamConnection) => void): this; - - emit(event: "connection-opened", connection: DataStreamConnection): boolean; - emit(event: "connection-closed", connection: DataStreamConnection): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'connection-opened', listener: (connection: DataStreamConnection) => void): this + on(event: 'connection-closed', listener: (connection: DataStreamConnection) => void): this + emit(event: 'connection-opened', connection: DataStreamConnection): boolean + emit(event: 'connection-closed', connection: DataStreamConnection): boolean + /* eslint-enable ts/method-signature-style */ } /** * DataStreamServer which listens for incoming tcp connections and handles identification of new connections * @group HomeKit Data Streams (HDS) */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class DataStreamServer extends EventEmitter { + static readonly version = '1.0' - static readonly version = "1.0"; - - private state: ServerState = ServerState.UNINITIALIZED; + private state: ServerState = ServerState.UNINITIALIZED - private static accessoryToControllerInfo = Buffer.from("HDS-Read-Encryption-Key"); - private static controllerToAccessoryInfo = Buffer.from("HDS-Write-Encryption-Key"); + private static accessoryToControllerInfo = Buffer.from('HDS-Read-Encryption-Key') + private static controllerToAccessoryInfo = Buffer.from('HDS-Write-Encryption-Key') - private tcpServer?: net.Server; - private tcpPort?: number; + private tcpServer?: Server + private tcpPort?: number - preparedSessions: PreparedDataStreamSession[] = []; - private readonly connections: DataStreamConnection[] = []; - private removeListenersOnceClosed = false; + preparedSessions: PreparedDataStreamSession[] = [] + private readonly connections: DataStreamConnection[] = [] + private removeListenersOnceClosed = false - private readonly internalEventEmitter: NodeEventEmitter = new NodeEventEmitter(); // used for message event and message request handlers + private readonly internalEventEmitter: NodeEventEmitter = new NodeEventEmitter() // used for message event and message request handlers public constructor() { - super(); + super() } /** @@ -259,8 +278,8 @@ export class DataStreamServer extends EventEmitter { * @param handler - function to be called for every occurring event */ public onEventMessage(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this { - this.internalEventEmitter.on(protocol + "-e-" + event, handler); - return this; + this.internalEventEmitter.on(`${protocol}-e-${event}`, handler) + return this } /** @@ -271,8 +290,8 @@ export class DataStreamServer extends EventEmitter { * @param handler - registered event handler */ public removeEventHandler(protocol: string | Protocols, event: string | Topics, handler: GlobalEventHandler): this { - this.internalEventEmitter.removeListener(protocol + "-e-" + event, handler); - return this; + this.internalEventEmitter.removeListener(`${protocol}-e-${event}`, handler) + return this } /** @@ -285,8 +304,8 @@ export class DataStreamServer extends EventEmitter { * @param handler - function to be called for every occurring request */ public onRequestMessage(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this { - this.internalEventEmitter.on(protocol + "-r-" + request, handler); - return this; + this.internalEventEmitter.on(`${protocol}-r-${request}`, handler) + return this } /** @@ -297,194 +316,195 @@ export class DataStreamServer extends EventEmitter { * @param handler - registered request handler */ public removeRequestHandler(protocol: string | Protocols, request: string | Topics, handler: GlobalRequestHandler): this { - this.internalEventEmitter.removeListener(protocol + "-r-" + request, handler); - return this; + this.internalEventEmitter.removeListener(`${protocol}-r-${request}`, handler) + return this } public prepareSession(connection: HAPConnection, controllerKeySalt: Buffer, callback: PrepareSessionCallback): void { - debug("Preparing for incoming HDS connection from %s", connection.sessionID); - const accessoryKeySalt = crypto.randomBytes(32); - const salt = Buffer.concat([controllerKeySalt, accessoryKeySalt]); + debug('Preparing for incoming HDS connection from %s', connection.sessionID) + const accessoryKeySalt = randomBytes(32) + const salt = Buffer.concat([controllerKeySalt, accessoryKeySalt]) - const accessoryToControllerEncryptionKey = hapCrypto.HKDF( - "sha512", + const accessoryToControllerEncryptionKey = HKDF( + 'sha512', salt, connection.encryption!.sharedSecret, DataStreamServer.accessoryToControllerInfo, 32, - ); - const controllerToAccessoryEncryptionKey = hapCrypto.HKDF( - "sha512", + ) + const controllerToAccessoryEncryptionKey = HKDF( + 'sha512', salt, connection.encryption!.sharedSecret, DataStreamServer.controllerToAccessoryInfo, 32, - ); + ) const preparedSession: PreparedDataStreamSession = { - connection: connection, - accessoryToControllerEncryptionKey: accessoryToControllerEncryptionKey, - controllerToAccessoryEncryptionKey: controllerToAccessoryEncryptionKey, - accessoryKeySalt: accessoryKeySalt, + connection, + accessoryToControllerEncryptionKey, + controllerToAccessoryEncryptionKey, + accessoryKeySalt, connectTimeout: setTimeout(() => this.timeoutPreparedSession(preparedSession), 10000), - }; - preparedSession.connectTimeout!.unref(); - this.preparedSessions.push(preparedSession); + } + preparedSession.connectTimeout!.unref() + this.preparedSessions.push(preparedSession) this.checkTCPServerEstablished(preparedSession, (error) => { if (error) { - callback(error); + callback(error) } else { - callback(undefined, preparedSession); + callback(undefined, preparedSession) } - }); + }) } private timeoutPreparedSession(preparedSession: PreparedDataStreamSession) { - debug("Prepared HDS session timed out out since no connection was opened for 10 seconds (%s)", preparedSession.connection.sessionID); - const index = this.preparedSessions.indexOf(preparedSession); + debug('Prepared HDS session timed out out since no connection was opened for 10 seconds (%s)', preparedSession.connection.sessionID) + const index = this.preparedSessions.indexOf(preparedSession) if (index >= 0) { - this.preparedSessions.splice(index, 1); + this.preparedSessions.splice(index, 1) } - this.checkCloseable(); + this.checkCloseable() } private checkTCPServerEstablished(preparedSession: PreparedDataStreamSession, callback: (error?: Error) => void) { switch (this.state) { - case ServerState.UNINITIALIZED: - debug("Starting up TCP server."); - this.tcpServer = net.createServer(); - - this.tcpServer.once("listening", this.listening.bind(this, preparedSession, callback)); - this.tcpServer.on("connection", this.onConnection.bind(this)); - this.tcpServer.on("close", this.closed.bind(this)); - - this.tcpServer.listen(); - this.state = ServerState.BINDING; - break; - case ServerState.BINDING: - debug("TCP server already running. Waiting for it to bind."); - this.tcpServer!.once("listening", this.listening.bind(this, preparedSession, callback)); - break; - case ServerState.LISTENING: - debug("Instructing client to connect to already running TCP server"); - preparedSession.port = this.tcpPort; - callback(); - break; - case ServerState.CLOSING: - debug("TCP socket is currently closing. Trying again when server is fully closed and opening a new one then."); - this.tcpServer!.once("close", () => setTimeout(() => this.checkTCPServerEstablished(preparedSession, callback), 10)); - break; + case ServerState.UNINITIALIZED: + debug('Starting up TCP server.') + this.tcpServer = createServer() + + this.tcpServer.once('listening', this.listening.bind(this, preparedSession, callback)) + this.tcpServer.on('connection', this.onConnection.bind(this)) + this.tcpServer.on('close', this.closed.bind(this)) + + this.tcpServer.listen() + this.state = ServerState.BINDING + break + case ServerState.BINDING: + debug('TCP server already running. Waiting for it to bind.') + this.tcpServer!.once('listening', this.listening.bind(this, preparedSession, callback)) + break + case ServerState.LISTENING: + debug('Instructing client to connect to already running TCP server') + preparedSession.port = this.tcpPort + callback() + break + case ServerState.CLOSING: + debug('TCP socket is currently closing. Trying again when server is fully closed and opening a new one then.') + this.tcpServer!.once('close', () => setTimeout(() => this.checkTCPServerEstablished(preparedSession, callback), 10)) + break } } private listening(preparedSession: PreparedDataStreamSession, callback: (error?: Error) => void) { - this.state = ServerState.LISTENING; + this.state = ServerState.LISTENING - const address = this.tcpServer!.address(); - if (address && typeof address !== "string") { // address is only typeof string when listening to a pipe or unix socket - this.tcpPort = address.port; - preparedSession.port = address.port; + const address = this.tcpServer!.address() + if (address && typeof address !== 'string') { // address is only typeof string when listening to a pipe or unix socket + this.tcpPort = address.port + preparedSession.port = address.port - debug("TCP server is now listening for new data stream connections on port %s", address.port); - callback(); + debug('TCP server is now listening for new data stream connections on port %s', address.port) + callback() } } private onConnection(socket: Socket) { - debug("[%s] New DataStream connection was established", socket.remoteAddress); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const connection = new DataStreamConnection(socket); + debug('[%s] New DataStream connection was established', socket.remoteAddress) + const connection = new DataStreamConnection(socket) // eslint-disable-line ts/no-use-before-define - connection.on(DataStreamConnectionEvent.IDENTIFICATION, this.handleSessionIdentification.bind(this, connection)); - connection.on(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, this.handleMessageGlobally.bind(this, connection)); - connection.on(DataStreamConnectionEvent.CLOSED, this.connectionClosed.bind(this, connection)); + /* eslint-disable ts/no-use-before-define */ + connection.on(DataStreamConnectionEvent.IDENTIFICATION, this.handleSessionIdentification.bind(this, connection)) + connection.on(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, this.handleMessageGlobally.bind(this, connection)) + connection.on(DataStreamConnectionEvent.CLOSED, this.connectionClosed.bind(this, connection)) + /* eslint-enable ts/no-use-before-define */ - this.connections.push(connection); + this.connections.push(connection) - this.emit(DataStreamServerEvent.CONNECTION_OPENED, connection); + this.emit(DataStreamServerEvent.CONNECTION_OPENED, connection) } private handleSessionIdentification(connection: DataStreamConnection, firstFrame: HDSFrame, callback: IdentificationCallback) { - let identifiedSession: PreparedDataStreamSession | undefined = undefined; + let identifiedSession: PreparedDataStreamSession | undefined for (let i = 0; i < this.preparedSessions.length; i++) { - const preparedSession = this.preparedSessions[i]; + const preparedSession = this.preparedSessions[i] // if we successfully decrypt the first frame with this key we know to which session this connection belongs if (connection.decryptHDSFrame(firstFrame, preparedSession.controllerToAccessoryEncryptionKey)) { - identifiedSession = preparedSession; - break; + identifiedSession = preparedSession + break } } - callback(identifiedSession); + callback(identifiedSession) if (identifiedSession) { - debug("[%s] Connection was successfully identified (linked with sessionId: %s)", connection.remoteAddress, identifiedSession.connection.sessionID); - const index = this.preparedSessions.indexOf(identifiedSession); + debug('[%s] Connection was successfully identified (linked with sessionId: %s)', connection.remoteAddress, identifiedSession.connection.sessionID) + const index = this.preparedSessions.indexOf(identifiedSession) if (index >= 0) { - this.preparedSessions.splice(index, 1); + this.preparedSessions.splice(index, 1) } - clearTimeout(identifiedSession.connectTimeout!); - identifiedSession.connectTimeout = undefined; + clearTimeout(identifiedSession.connectTimeout!) + identifiedSession.connectTimeout = undefined // we have currently no experience with data stream connections, maybe it would be good to index active connections // by their hap sessionId in order to clear out old but still open connections when the controller opens a new one // on the other hand the keepAlive should handle that also :thinking: } else { // we looped through all session and didn't find anything - debug("[%s] Could not identify connection. Terminating.", connection.remoteAddress); - connection.close(); // disconnecting since first message was not a valid hello + debug('[%s] Could not identify connection. Terminating.', connection.remoteAddress) + connection.close() // disconnecting since first message was not a valid hello } } private handleMessageGlobally(connection: DataStreamConnection, message: DataStreamMessage) { - assert.notStrictEqual(message.type, MessageType.RESPONSE); // responses can't physically get here + assert.notStrictEqual(message.type, MessageType.RESPONSE) // responses can't physically get here - let separator = ""; - const args = []; + let separator = '' + const args = [] if (message.type === MessageType.EVENT) { - separator = "-e-"; + separator = '-e-' } else if (message.type === MessageType.REQUEST) { - separator = "-r-"; - args.push(message.id!); + separator = '-r-' + args.push(message.id!) } - args.push(message.message); + args.push(message.message) - let hadListeners; + let hadListeners try { - hadListeners = this.internalEventEmitter.emit(message.protocol + separator + message.topic, connection, ...args); + hadListeners = this.internalEventEmitter.emit(message.protocol + separator + message.topic, connection, ...args) } catch (error) { - hadListeners = true; - debug("[%s] Error occurred while dispatching handler for HDS message: %o", connection.remoteAddress, message); - debug(error.stack); + hadListeners = true + debug('[%s] Error occurred while dispatching handler for HDS message: %o', connection.remoteAddress, message) + debug(error.stack) } if (!hadListeners) { - debug("[%s] WARNING no handler was found for message: %o", connection.remoteAddress, message); + debug('[%s] WARNING no handler was found for message: %o', connection.remoteAddress, message) } } private connectionClosed(connection: DataStreamConnection) { - debug("[%s] DataStream connection closed", connection.remoteAddress); + debug('[%s] DataStream connection closed', connection.remoteAddress) - this.connections.splice(this.connections.indexOf(connection), 1); - this.emit(DataStreamServerEvent.CONNECTION_CLOSED, connection); + this.connections.splice(this.connections.indexOf(connection), 1) + this.emit(DataStreamServerEvent.CONNECTION_CLOSED, connection) - this.checkCloseable(); + this.checkCloseable() if (this.state === ServerState.CLOSING && this.removeListenersOnceClosed && this.connections.length === 0) { - this.removeAllListeners(); // see this.destroy() + this.removeAllListeners() // see this.destroy() } } private checkCloseable() { if (this.connections.length === 0 && this.preparedSessions.length === 0 && this.state < ServerState.CLOSING) { - debug("Last connection disconnected. Closing the server now."); + debug('Last connection disconnected. Closing the server now.') - this.state = ServerState.CLOSING; - this.tcpServer!.close(); + this.state = ServerState.CLOSING + this.tcpServer!.close() } } @@ -493,35 +513,35 @@ export class DataStreamServer extends EventEmitter { */ public destroy(): void { if (this.state > ServerState.UNINITIALIZED && this.state < ServerState.CLOSING) { - this.tcpServer!.close(); + this.tcpServer!.close() for (const connection of this.connections) { - connection.close(); + connection.close() } } - this.state = ServerState.CLOSING; + this.state = ServerState.CLOSING - this.removeListenersOnceClosed = true; - this.internalEventEmitter.removeAllListeners(); + this.removeListenersOnceClosed = true + this.internalEventEmitter.removeAllListeners() } private closed() { - this.tcpServer = undefined; - this.tcpPort = undefined; + this.tcpServer = undefined + this.tcpPort = undefined - this.state = ServerState.UNINITIALIZED; + this.state = ServerState.UNINITIALIZED } - } /** * @group HomeKit Data Streams (HDS) */ -export type IdentificationCallback = (identifiedSession?: PreparedDataStreamSession) => void; +export type IdentificationCallback = (identifiedSession?: PreparedDataStreamSession) => void /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum DataStreamConnectionEvent { /** * This event is emitted when the first HDSFrame is received from a new connection. @@ -529,34 +549,37 @@ export const enum DataStreamConnectionEvent { * If identification was successful the PreparedDataStreamSession should be supplied to the callback, * otherwise undefined should be supplied. */ - IDENTIFICATION = "identification", + IDENTIFICATION = 'identification', /** * This event is emitted when no handler could be found for the given protocol of an event or request message. */ - HANDLE_MESSAGE_GLOBALLY = "handle-message-globally", + HANDLE_MESSAGE_GLOBALLY = 'handle-message-globally', /** * This event is emitted when the socket of the connection was closed. */ - CLOSED = "closed", + CLOSED = 'closed', } /** * @group HomeKit Data Streams (HDS) */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface DataStreamConnection { - on(event: "identification", listener: (frame: HDSFrame, callback: IdentificationCallback) => void): this; - on(event: "handle-message-globally", listener: (message: DataStreamMessage) => void): this; - on(event: "closed", listener: () => void): this; - - emit(event: "identification", frame: HDSFrame, callback: IdentificationCallback): boolean; - emit(event: "handle-message-globally", message: DataStreamMessage): boolean; - emit(event: "closed"): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'identification', listener: (frame: HDSFrame, callback: IdentificationCallback) => void): this + on(event: 'handle-message-globally', listener: (message: DataStreamMessage) => void): this + on(event: 'closed', listener: () => void): this + + emit(event: 'identification', frame: HDSFrame, callback: IdentificationCallback): boolean + emit(event: 'handle-message-globally', message: DataStreamMessage): boolean + emit(event: 'closed'): boolean + /* eslint-enable ts/method-signature-style */ } /** * @group HomeKit Data Streams (HDS) */ +// eslint-disable-next-line no-restricted-syntax export const enum HDSConnectionErrorType { ILLEGAL_STATE = 1, CLOSED_SOCKET = 2, @@ -567,11 +590,11 @@ export const enum HDSConnectionErrorType { * @group HomeKit Data Streams (HDS) */ export class HDSConnectionError extends Error { - readonly type: HDSConnectionErrorType; + readonly type: HDSConnectionErrorType constructor(message: string, type: HDSConnectionErrorType) { - super(message); - this.type = type; + super(message) + this.type = type } } @@ -581,89 +604,87 @@ export class HDSConnectionError extends Error { * * @group HomeKit Data Streams (HDS) */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class DataStreamConnection extends EventEmitter { + private static readonly MAX_PAYLOAD_LENGTH = 0b11111111111111111111 - private static readonly MAX_PAYLOAD_LENGTH = 0b11111111111111111111; - - private socket: Socket; - private connection?: HAPConnection; // reference to the hap connection. is present when state > UNIDENTIFIED - readonly remoteAddress: string; + private socket: Socket + private connection?: HAPConnection // reference to the hap connection. is present when state > UNIDENTIFIED + readonly remoteAddress: string /* Since our DataStream server does only listen on one port and this port is supplied to every client which wants to connect, we do not really know which client is who when we receive a tcp connection. Thus, we find the correct PreparedDataStreamSession object by testing the encryption keys of all available prepared sessions. Then we can reference this hds connection with the correct hap connection and mark it as identified. */ - private state: ConnectionState = ConnectionState.UNIDENTIFIED; + private state: ConnectionState = ConnectionState.UNIDENTIFIED - private accessoryToControllerEncryptionKey?: Buffer; - private controllerToAccessoryEncryptionKey?: Buffer; + private accessoryToControllerEncryptionKey?: Buffer + private controllerToAccessoryEncryptionKey?: Buffer - private accessoryToControllerNonce: number; - private readonly accessoryToControllerNonceBuffer: Buffer; - private controllerToAccessoryNonce: number; - private readonly controllerToAccessoryNonceBuffer: Buffer; + private accessoryToControllerNonce: number + private readonly accessoryToControllerNonceBuffer: Buffer + private controllerToAccessoryNonce: number + private readonly controllerToAccessoryNonceBuffer: Buffer - private frameBuffer?: Buffer; // used to store incomplete HDS frames + private frameBuffer?: Buffer // used to store incomplete HDS frames - private readonly hapConnectionClosedListener: () => void; - private protocolHandlers: Record = {}; // used to store protocolHandlers identified by their protocol name + private readonly hapConnectionClosedListener: () => void + private protocolHandlers: Record = {} // used to store protocolHandlers identified by their protocol name - private responseHandlers: Record = {}; // used to store responseHandlers indexed by their respective requestId - private responseTimers: Record = {}; // used to store response timeouts indexed by their respective requestId + private responseHandlers: Record = {} // used to store responseHandlers indexed by their respective requestId + private responseTimers: Record = {} // used to store response timeouts indexed by their respective requestId - private helloTimer?: NodeJS.Timeout; + private helloTimer?: NodeJS.Timeout constructor(socket: Socket) { - super(); - this.socket = socket; - this.remoteAddress = socket.remoteAddress!; + super() + this.socket = socket + this.remoteAddress = socket.remoteAddress! - this.socket.setNoDelay(true); // disable Nagle algorithm - this.socket.setKeepAlive(true); + this.socket.setNoDelay(true) // disable Nagle algorithm + this.socket.setKeepAlive(true) - this.accessoryToControllerNonce = 0; - this.accessoryToControllerNonceBuffer = Buffer.alloc(8); - this.controllerToAccessoryNonce = 0; - this.controllerToAccessoryNonceBuffer = Buffer.alloc(8); + this.accessoryToControllerNonce = 0 + this.accessoryToControllerNonceBuffer = Buffer.alloc(8) + this.controllerToAccessoryNonce = 0 + this.controllerToAccessoryNonceBuffer = Buffer.alloc(8) - this.hapConnectionClosedListener = this.onHAPSessionClosed.bind(this); + this.hapConnectionClosedListener = this.onHAPSessionClosed.bind(this) this.addProtocolHandler(Protocols.CONTROL, { requestHandler: { [Topics.HELLO]: this.handleHello.bind(this), }, - }); + }) this.helloTimer = setTimeout(() => { - debug("[%s] Hello message did not arrive in time. Killing the connection", this.remoteAddress); - this.close(); - }, 10000); + debug('[%s] Hello message did not arrive in time. Killing the connection', this.remoteAddress) + this.close() + }, 10000) - this.socket.on("data", this.onSocketData.bind(this)); - this.socket.on("error", this.onSocketError.bind(this)); - this.socket.on("close", this.onSocketClose.bind(this)); + this.socket.on('data', this.onSocketData.bind(this)) + this.socket.on('error', this.onSocketError.bind(this)) + this.socket.on('close', this.onSocketClose.bind(this)) // this is to mitigate the event emitter "memory leak warning". // e.g. with HSV there might be multiple cameras subscribing to the CLOSE event. one subscription for // every active recording stream on a camera. The default limit of 10 might be easily reached. // Setting a high limit isn't the prefect solution, but will avoid false positives but ensures that // a warning is still be printed if running long enough. - this.setMaxListeners(100); + this.setMaxListeners(100) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleHello(id: number, message: Record) { // that hello is indeed the _first_ message received is verified in onSocketData(...) - debug("[%s] Received hello message from client: %o", this.remoteAddress, message); + debug('[%s] Received hello message from client: %o', this.remoteAddress, message) - clearTimeout(this.helloTimer!); - this.helloTimer = undefined; + clearTimeout(this.helloTimer!) + this.helloTimer = undefined - this.state = ConnectionState.READY; + this.state = ConnectionState.READY - this.sendResponse(Protocols.CONTROL, Topics.HELLO, id); + this.sendResponse(Protocols.CONTROL, Topics.HELLO, id) } /** @@ -675,11 +696,11 @@ export class DataStreamConnection extends EventEmitter { */ addProtocolHandler(protocol: string | Protocols, protocolHandler: DataStreamProtocolHandler): boolean { if (this.protocolHandlers[protocol] !== undefined) { - return false; + return false } - this.protocolHandlers[protocol] = protocolHandler; - return true; + this.protocolHandlers[protocol] = protocolHandler + return true } /** @@ -689,10 +710,10 @@ export class DataStreamConnection extends EventEmitter { * @param protocolHandler - object which will be unregistered */ removeProtocolHandler(protocol: string | Protocols, protocolHandler: DataStreamProtocolHandler): void { - const current = this.protocolHandlers[protocol]; + const current = this.protocolHandlers[protocol] if (current === protocolHandler) { - delete this.protocolHandlers[protocol]; + delete this.protocolHandlers[protocol] } } @@ -703,15 +724,13 @@ export class DataStreamConnection extends EventEmitter { * @param event - name of the event (also referred to as topic. See {@link Topics} for some known ones) * @param message - message dictionary which gets sent along the event */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any sendEvent(protocol: string | Protocols, event: string | Topics, message: Record = {}): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const header: Record = {}; - header.protocol = protocol; - header.event = event; + const header: Record = {} + header.protocol = protocol + header.event = event if (this.state === ConnectionState.READY) { - this.sendHDSFrame(header, message); + this.sendHDSFrame(header, message) } } @@ -722,37 +741,35 @@ export class DataStreamConnection extends EventEmitter { * @param request - name of the request (also referred to as topic. See {@link Topics} for some known ones) * @param message - message dictionary which gets sent along the request * @param callback - handler which gets supplied with an error object if the response didn't - * arrive in time or the status and the message dictionary from the response + * arrive in time or the status and the message dictionary from the response */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any sendRequest(protocol: string | Protocols, request: string | Topics, message: Record = {}, callback: ResponseHandler): void { - let requestId: number; + let requestId: number do { // generate unused requestId // currently writing int64 to data stream is not really supported, so 32-bit int will be the max - requestId = Math.floor(Math.random() * 4294967295); - } while (this.responseHandlers[requestId] !== undefined); + requestId = Math.floor(Math.random() * 4294967295) + } while (this.responseHandlers[requestId] !== undefined) - this.responseHandlers[requestId] = callback; + this.responseHandlers[requestId] = callback this.responseTimers[requestId] = setTimeout(() => { // we did not receive a response => close socket - this.close(); + this.close() - const handler = this.responseHandlers[requestId]; + const handler = this.responseHandlers[requestId] - delete this.responseHandlers[requestId]; - delete this.responseTimers[requestId]; + delete this.responseHandlers[requestId] + delete this.responseTimers[requestId] // handler should be able to clean up their stuff - handler(new Error("timeout"), undefined, {}); - }, 10000); // 10s timer + handler(new Error('timeout'), undefined, {}) + }, 10000) // 10s timer - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const header: Record = {}; - header.protocol = protocol; - header.request = request; - header.id = new Int64(requestId); + const header: Record = {} + header.protocol = protocol + header.request = request + header.id = new Int64(requestId) - this.sendHDSFrame(header, message); + this.sendHDSFrame(header, message) } /** @@ -769,334 +786,326 @@ export class DataStreamConnection extends EventEmitter { response: string | Topics, id: number, status: HDSStatus = HDSStatus.SUCCESS, - // eslint-disable-next-line @typescript-eslint/no-explicit-any message: Record = {}, ): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const header: Record = {}; - header.protocol = protocol; - header.response = response; - header.id = new Int64(id); - header.status = new Int64(status); - - this.sendHDSFrame(header, message); + const header: Record = {} + header.protocol = protocol + header.response = response + header.id = new Int64(id) + header.status = new Int64(status) + + this.sendHDSFrame(header, message) } private onSocketData(data: Buffer) { if (this.state >= ConnectionState.CLOSING) { - return; + return } - let frameIndex = 0; - const frames: HDSFrame[] = this.decodeHDSFrames(data); + let frameIndex = 0 + const frames: HDSFrame[] = this.decodeHDSFrames(data) if (frames.length === 0) { // not enough data - return; + return } if (this.state === ConnectionState.UNIDENTIFIED) { // at the beginning we are only interested in trying to decrypt the first frame in order to test decryption keys - const firstFrame = frames[frameIndex++]; + const firstFrame = frames[frameIndex++] this.emit(DataStreamConnectionEvent.IDENTIFICATION, firstFrame, (identifiedSession?: PreparedDataStreamSession) => { if (identifiedSession) { // horray, we found our connection - this.connection = identifiedSession.connection; - this.accessoryToControllerEncryptionKey = identifiedSession.accessoryToControllerEncryptionKey; - this.controllerToAccessoryEncryptionKey = identifiedSession.controllerToAccessoryEncryptionKey; - this.state = ConnectionState.EXPECTING_HELLO; + this.connection = identifiedSession.connection + this.accessoryToControllerEncryptionKey = identifiedSession.accessoryToControllerEncryptionKey + this.controllerToAccessoryEncryptionKey = identifiedSession.controllerToAccessoryEncryptionKey + this.state = ConnectionState.EXPECTING_HELLO // below listener is removed in .close() - this.connection.setMaxListeners(this.connection.getMaxListeners() + 1); - this.connection.on(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener); // register close listener + this.connection.setMaxListeners(this.connection.getMaxListeners() + 1) + this.connection.on(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener) // register close listener - debug("[%s] Registering CLOSED handler to HAP connection. Connection currently has %d close handlers!", - this.remoteAddress, this.connection.listeners(HAPConnectionEvent.CLOSED).length); + debug('[%s] Registering CLOSED handler to HAP connection. Connection currently has %d close handlers!', this.remoteAddress, this.connection.listeners(HAPConnectionEvent.CLOSED).length) } - }); + }) if (this.state === ConnectionState.UNIDENTIFIED) { // did not find a prepared connection, server already closed this connection; nothing to do here - return; + return } } for (; frameIndex < frames.length; frameIndex++) { // decrypt all remaining frames if (!this.decryptHDSFrame(frames[frameIndex])) { - debug("[%s] HDS frame decryption or authentication failed. Connection will be terminated!", this.remoteAddress); - this.close(); - return; + debug('[%s] HDS frame decryption or authentication failed. Connection will be terminated!', this.remoteAddress) + this.close() + return } } - const messages: DataStreamMessage[] = this.decodePayloads(frames); // decode contents of payload + const messages: DataStreamMessage[] = this.decodePayloads(frames) // decode contents of payload if (this.state === ConnectionState.EXPECTING_HELLO) { - const firstMessage = messages[0]; + const firstMessage = messages[0] if (firstMessage.protocol !== Protocols.CONTROL || firstMessage.type !== MessageType.REQUEST || firstMessage.topic !== Topics.HELLO) { // first message is not the expected hello request - debug("[%s] First message received was not the expected hello message. Instead got: %o", this.remoteAddress, firstMessage); - this.close(); - return; + debug('[%s] First message received was not the expected hello message. Instead got: %o', this.remoteAddress, firstMessage) + this.close() + return } } - messages.forEach(message => { + messages.forEach((message) => { if (message.type === MessageType.RESPONSE) { // protocol and topic are currently not tested here; just assumed they are correct; // probably they are as the requestId is unique per connection no matter what protocol is used - const responseHandler = this.responseHandlers[message.id!]; - const responseTimer = this.responseTimers[message.id!]; + const responseHandler = this.responseHandlers[message.id!] + const responseTimer = this.responseTimers[message.id!] if (responseTimer) { - clearTimeout(responseTimer); - delete this.responseTimers[message.id!]; + clearTimeout(responseTimer) + delete this.responseTimers[message.id!] } if (!responseHandler) { // we got a response to a request we did not send; we ignore it for now, since nobody will be hurt - debug("WARNING we received a response to a request we have not sent: %o", message); - return; + debug('WARNING we received a response to a request we have not sent: %o', message) + return } try { - responseHandler(undefined, message.status!, message.message); + responseHandler(undefined, message.status!, message.message) } catch (error) { - debug("[%s] Error occurred while dispatching response handler for HDS message: %o", this.remoteAddress, message); - debug(error.stack); + debug('[%s] Error occurred while dispatching response handler for HDS message: %o', this.remoteAddress, message) + debug(error.stack) } - delete this.responseHandlers[message.id!]; + delete this.responseHandlers[message.id!] } else { - const handler = this.protocolHandlers[message.protocol]; + const handler = this.protocolHandlers[message.protocol] if (handler === undefined) { // send message to the server to check if there are some global handlers for it - this.emit(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, message); - return; + this.emit(DataStreamConnectionEvent.HANDLE_MESSAGE_GLOBALLY, message) + return } if (message.type === MessageType.EVENT) { - let eventHandler: EventHandler; - if (!handler.eventHandler || !(eventHandler = handler.eventHandler[message.topic])) { - debug("[%s] WARNING no event handler was found for message: %o", this.remoteAddress, message); - return; + if (!handler.eventHandler || !handler.eventHandler[message.topic]) { + debug('[%s] WARNING no event handler was found for message: %o', this.remoteAddress, message) + return } + const eventHandler: EventHandler = handler.eventHandler[message.topic] try { - eventHandler(message.message); + eventHandler(message.message) } catch (error) { - debug("[%s] Error occurred while dispatching event handler for HDS message: %o", this.remoteAddress, message); - debug(error.stack); + debug('[%s] Error occurred while dispatching event handler for HDS message: %o', this.remoteAddress, message) + debug(error.stack) } } else if (message.type === MessageType.REQUEST) { - let requestHandler: RequestHandler; - if (!handler.requestHandler || !(requestHandler = handler.requestHandler[message.topic])) { - debug("[%s] WARNING no request handler was found for message: %o", this.remoteAddress, message); - return; + if (!handler.requestHandler || !handler.requestHandler[message.topic]) { + debug('[%s] WARNING no request handler was found for message: %o', this.remoteAddress, message) + return } + const requestHandler: RequestHandler = handler.requestHandler[message.topic] try { - requestHandler(message.id!, message.message); + requestHandler(message.id!, message.message) } catch (error) { - debug("[%s] Error occurred while dispatching request handler for HDS message: %o", this.remoteAddress, message); - debug(error.stack); + debug('[%s] Error occurred while dispatching request handler for HDS message: %o', this.remoteAddress, message) + debug(error.stack) } } else { - debug("[%s] Encountered unknown message type with id %d", this.remoteAddress, message.type); + debug('[%s] Encountered unknown message type with id %d', this.remoteAddress, message.type) } } - }); + }) } private decodeHDSFrames(data: Buffer) { if (this.frameBuffer !== undefined) { - data = Buffer.concat([this.frameBuffer, data]); - this.frameBuffer = undefined; + data = Buffer.concat([this.frameBuffer, data]) + this.frameBuffer = undefined } - const totalBufferLength = data.length; - const frames: HDSFrame[] = []; + const totalBufferLength = data.length + const frames: HDSFrame[] = [] for (let frameBegin = 0; frameBegin < totalBufferLength;) { if (frameBegin + 4 > totalBufferLength) { // we don't have enough data in the buffer for the next header - this.frameBuffer = data.slice(frameBegin); - break; + this.frameBuffer = data.subarray(frameBegin) + break } - const payloadType = data.readUInt8(frameBegin); // type defining structure of payload; 8-bit; currently expected to be 1 - const payloadLength = data.readUIntBE(frameBegin + 1, 3); // read 24-bit big-endian uint length field + const payloadType = data.readUInt8(frameBegin) // type defining structure of payload; 8-bit; currently expected to be 1 + const payloadLength = data.readUIntBE(frameBegin + 1, 3) // read 24-bit big-endian uint length field if (payloadLength > DataStreamConnection.MAX_PAYLOAD_LENGTH) { - debug("[%s] Connection send payload with size bigger than the maximum allow for data stream", this.remoteAddress); - this.close(); - return []; + debug('[%s] Connection send payload with size bigger than the maximum allow for data stream', this.remoteAddress) + this.close() + return [] } - const remainingBufferLength = totalBufferLength - frameBegin - 4; // subtract 4 for payloadType (1-byte) and payloadLength (3-byte) + const remainingBufferLength = totalBufferLength - frameBegin - 4 // subtract 4 for payloadType (1-byte) and payloadLength (3-byte) // check if the data from this frame is already there (payload + 16-byte authTag) if (payloadLength + 16 > remainingBufferLength) { // Frame is fragmented, so we wait until we receive more - this.frameBuffer = data.slice(frameBegin); - break; + this.frameBuffer = data.subarray(frameBegin) + break } - const payloadBegin = frameBegin + 4; - const authTagBegin = payloadBegin + payloadLength; + const payloadBegin = frameBegin + 4 + const authTagBegin = payloadBegin + payloadLength - const header = data.slice(frameBegin, payloadBegin); // header is also authenticated using authTag - const cipheredPayload = data.slice(payloadBegin, authTagBegin); - const plaintextPayload = Buffer.alloc(payloadLength); - const authTag = data.slice(authTagBegin, authTagBegin + 16); + const header = data.subarray(frameBegin, payloadBegin) // header is also authenticated using authTag + const cipheredPayload = data.subarray(payloadBegin, authTagBegin) + const plaintextPayload = Buffer.alloc(payloadLength) + const authTag = data.subarray(authTagBegin, authTagBegin + 16) - frameBegin = authTagBegin + 16; // move to next frame + frameBegin = authTagBegin + 16 // move to next frame if (payloadType === 1) { const hdsFrame: HDSFrame = { - header: header, - cipheredPayload: cipheredPayload, - authTag: authTag, - }; - frames.push(hdsFrame); + header, + cipheredPayload, + authTag, + } + frames.push(hdsFrame) } else { - debug("[%s] Encountered unknown payload type %d for payload: %s", this.remoteAddress, plaintextPayload.toString("hex")); + debug('[%s] Encountered unknown payload type %d for payload: %s', this.remoteAddress, plaintextPayload.toString('hex')) } } - return frames; + return frames } /** - * @private file-private API + * @private */ decryptHDSFrame(frame: HDSFrame, keyOverwrite?: Buffer): boolean { - hapCrypto.writeUInt64LE(this.controllerToAccessoryNonce, this.controllerToAccessoryNonceBuffer, 0); // update nonce buffer + writeUInt64LE(this.controllerToAccessoryNonce, this.controllerToAccessoryNonceBuffer, 0) // update nonce buffer - const key = keyOverwrite || this.controllerToAccessoryEncryptionKey!; + const key = keyOverwrite || this.controllerToAccessoryEncryptionKey! try { - frame.plaintextPayload = hapCrypto.chacha20_poly1305_decryptAndVerify(key, this.controllerToAccessoryNonceBuffer, - frame.header, frame.cipheredPayload, frame.authTag); - this.controllerToAccessoryNonce++; // we had a successful encryption, increment the nonce - return true; + frame.plaintextPayload = chacha20_poly1305_decryptAndVerify(key, this.controllerToAccessoryNonceBuffer, frame.header, frame.cipheredPayload, frame.authTag) + this.controllerToAccessoryNonce++ // we had a successful encryption, increment the nonce + return true } catch (error) { // frame decryption or authentication failed. Could happen when our guess for a PreparedDataStreamSession is wrong - return false; + return false } } private decodePayloads(frames: HDSFrame[]) { - const messages: DataStreamMessage[] = []; + const messages: DataStreamMessage[] = [] - frames.forEach(frame => { - const payload = frame.plaintextPayload; + frames.forEach((frame) => { + const payload = frame.plaintextPayload if (!payload) { - throw new HDSConnectionError("Reached illegal state. Encountered HDSFrame with wasn't decrypted yet!", HDSConnectionErrorType.ILLEGAL_STATE); + throw new HDSConnectionError('Reached illegal state. Encountered HDSFrame with wasn\'t decrypted yet!', HDSConnectionErrorType.ILLEGAL_STATE) } - const headerLength = payload.readUInt8(0); - const messageLength = payload.length - headerLength - 1; + const headerLength = payload.readUInt8(0) + const messageLength = payload.length - headerLength - 1 - const headerBegin = 1; - const messageBegin = headerBegin + headerLength; + const headerBegin = 1 + const messageBegin = headerBegin + headerLength - const headerPayload = new DataStreamReader(payload.slice(headerBegin, headerBegin + headerLength)); - const messagePayload = new DataStreamReader(payload.slice(messageBegin, messageBegin + messageLength)); + const headerPayload = new DataStreamReader(payload.subarray(headerBegin, headerBegin + headerLength)) + const messagePayload = new DataStreamReader(payload.subarray(messageBegin, messageBegin + messageLength)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let headerDictionary: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let messageDictionary: Record; + let headerDictionary: Record + let messageDictionary: Record try { - headerDictionary = DataStreamParser.decode(headerPayload); - headerPayload.finished(); + headerDictionary = DataStreamParser.decode(headerPayload) + headerPayload.finished() } catch (error) { - debug("[%s] Failed to decode header payload: %s", this.remoteAddress, error.message); - return; + debug('[%s] Failed to decode header payload: %s', this.remoteAddress, error.message) + return } try { - messageDictionary = DataStreamParser.decode(messagePayload); - messagePayload.finished(); + messageDictionary = DataStreamParser.decode(messagePayload) + messagePayload.finished() } catch (error) { - debug("[%s] Failed to decode message payload: %s (header: %o)", this.remoteAddress, error.message, headerDictionary); - return; + debug('[%s] Failed to decode message payload: %s (header: %o)', this.remoteAddress, error.message, headerDictionary) + return } - let type: MessageType; - const protocol: string = headerDictionary.protocol; - let topic: string; - let id: number | undefined = undefined; - let status: HDSStatus | undefined = undefined; + let type: MessageType + const protocol: string = headerDictionary.protocol + let topic: string + let id: number | undefined + let status: HDSStatus | undefined if (headerDictionary.event !== undefined) { - type = MessageType.EVENT; - topic = headerDictionary.event; + type = MessageType.EVENT + topic = headerDictionary.event } else if (headerDictionary.request !== undefined) { - type = MessageType.REQUEST; - topic = headerDictionary.request; - id = headerDictionary.id; + type = MessageType.REQUEST + topic = headerDictionary.request + id = headerDictionary.id } else if (headerDictionary.response !== undefined) { - type = MessageType.RESPONSE; - topic = headerDictionary.response; - id = headerDictionary.id; - status = headerDictionary.status; + type = MessageType.RESPONSE + topic = headerDictionary.response + id = headerDictionary.id + status = headerDictionary.status } else { - debug("[%s] Encountered unknown payload header format: %o (message: %o)", this.remoteAddress, headerDictionary, messageDictionary); - return; + debug('[%s] Encountered unknown payload header format: %o (message: %o)', this.remoteAddress, headerDictionary, messageDictionary) + return } const message: DataStreamMessage = { - type: type, - protocol: protocol, - topic: topic, - id: id, - status: status, + type, + protocol, + topic, + id, + status, message: messageDictionary, - }; - messages.push(message); - }); + } + messages.push(message) + }) - return messages; + return messages } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private sendHDSFrame(header: Record, message: Record) { if (this.state >= ConnectionState.CLOSING) { - throw new HDSConnectionError("Cannot send message on closing/closed socket!", HDSConnectionErrorType.CLOSED_SOCKET); + throw new HDSConnectionError('Cannot send message on closing/closed socket!', HDSConnectionErrorType.CLOSED_SOCKET) } - const headerWriter = new DataStreamWriter(); - const messageWriter = new DataStreamWriter(); + const headerWriter = new DataStreamWriter() + const messageWriter = new DataStreamWriter() - DataStreamParser.encode(header, headerWriter); - DataStreamParser.encode(message, messageWriter); + DataStreamParser.encode(header, headerWriter) + DataStreamParser.encode(message, messageWriter) - - const payloadHeaderBuffer = Buffer.alloc(1); - payloadHeaderBuffer.writeUInt8(headerWriter.length(), 0); - const payloadBuffer = Buffer.concat([payloadHeaderBuffer, headerWriter.getData(), messageWriter.getData()]); + const payloadHeaderBuffer = Buffer.alloc(1) + payloadHeaderBuffer.writeUInt8(headerWriter.length(), 0) + const payloadBuffer = Buffer.concat([payloadHeaderBuffer, headerWriter.getData(), messageWriter.getData()]) if (payloadBuffer.length > DataStreamConnection.MAX_PAYLOAD_LENGTH) { throw new HDSConnectionError( - "Tried sending payload with length larger than the maximum allowed for data stream", + 'Tried sending payload with length larger than the maximum allowed for data stream', HDSConnectionErrorType.MAX_PAYLOAD_LENGTH, - ); + ) } - const frameTypeBuffer = Buffer.alloc(1); - frameTypeBuffer.writeUInt8(1, 0); - let frameLengthBuffer = Buffer.alloc(4); - frameLengthBuffer.writeUInt32BE(payloadBuffer.length, 0); - frameLengthBuffer = frameLengthBuffer.slice(1, 4); // a bit hacky but the only real way to write 24-bit int in node + const frameTypeBuffer = Buffer.alloc(1) + frameTypeBuffer.writeUInt8(1, 0) + let frameLengthBuffer = Buffer.alloc(4) + frameLengthBuffer.writeUInt32BE(payloadBuffer.length, 0) + frameLengthBuffer = frameLengthBuffer.subarray(1, 4) // a bit hacky but the only real way to write 24-bit int in node - const frameHeader = Buffer.concat([frameTypeBuffer, frameLengthBuffer]); + const frameHeader = Buffer.concat([frameTypeBuffer, frameLengthBuffer]) - hapCrypto.writeUInt64LE(this.accessoryToControllerNonce++, this.accessoryToControllerNonceBuffer); - const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal( + writeUInt64LE(this.accessoryToControllerNonce++, this.accessoryToControllerNonceBuffer) + const encrypted = chacha20_poly1305_encryptAndSeal( this.accessoryToControllerEncryptionKey!, this.accessoryToControllerNonceBuffer, frameHeader, payloadBuffer, - ); + ) - this.socket.write(Buffer.concat([frameHeader, encrypted.ciphertext, encrypted.authTag])); + this.socket.write(Buffer.concat([frameHeader, encrypted.ciphertext, encrypted.authTag])) /* Useful for debugging outgoing packages and detecting encoding errors console.log("SENT DATA: " + payloadBuffer.toString("hex")); @@ -1108,41 +1117,40 @@ export class DataStreamConnection extends EventEmitter { }; const sentMessage = this.decodePayloads([frame])[0]; console.log("Sent message: " + JSON.stringify(sentMessage, null, 4)); - //*/ + // */ } close(): void { // closing socket by sending FIN packet; incoming data will be ignored from that point on if (this.state >= ConnectionState.CLOSING) { - return; // connection is already closing/closed + return // connection is already closing/closed } - this.state = ConnectionState.CLOSING; - this.socket.end(); + this.state = ConnectionState.CLOSING + this.socket.end() } isConsideredClosed(): boolean { - return this.state >= ConnectionState.CLOSING; + return this.state >= ConnectionState.CLOSING } private onHAPSessionClosed() { // If the hap connection is closed it is probably also a good idea to close the data stream connection - debug("[%s] HAP connection disconnected. Also closing DataStream connection now.", this.remoteAddress); - this.close(); + debug('[%s] HAP connection disconnected. Also closing DataStream connection now.', this.remoteAddress) + this.close() } private onSocketError(error: Error) { - debug("[%s] Encountered socket error: %s", this.remoteAddress, error.message); + debug('[%s] Encountered socket error: %s', this.remoteAddress, error.message) // onSocketClose will be called next } private onSocketClose() { // this instance is now considered completely dead - this.state = ConnectionState.CLOSED; - this.emit(DataStreamConnectionEvent.CLOSED); + this.state = ConnectionState.CLOSED + this.emit(DataStreamConnectionEvent.CLOSED) - this.connection?.removeListener(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener); - this.connection?.setMaxListeners(this.connection.getMaxListeners() - 1); - this.removeAllListeners(); + this.connection?.removeListener(HAPConnectionEvent.CLOSED, this.hapConnectionClosedListener) + this.connection?.setMaxListeners(this.connection.getMaxListeners() - 1) + this.removeAllListeners() } - } diff --git a/src/lib/datastream/index.ts b/src/lib/datastream/index.ts index b4e0ca8e9..0d4e34d0f 100644 --- a/src/lib/datastream/index.ts +++ b/src/lib/datastream/index.ts @@ -1,3 +1,3 @@ -export * from "./DataStreamManagement"; -export * from "./DataStreamServer"; -export * from "./DataStreamParser"; +export * from './DataStreamManagement.js' +export * from './DataStreamParser.js' +export * from './DataStreamServer.js' diff --git a/src/lib/dbus/align.ts b/src/lib/dbus/align.ts new file mode 100644 index 000000000..e61342d49 --- /dev/null +++ b/src/lib/dbus/align.ts @@ -0,0 +1,12 @@ +import { Buffer } from 'safe-buffer' + +export default function align(ps: any, n: number) { + const pad = n - (ps._offset % n) + if (pad === 0 || pad === n) { + return + } + // TODO: write8(0) in a loop (3 to 7 times here) could be more efficient + const padBuff = Buffer.alloc(pad) + ps.put(Buffer.from(padBuff)) + ps._offset += pad +} diff --git a/src/lib/dbus/bus.ts b/src/lib/dbus/bus.ts new file mode 100644 index 000000000..e22e68def --- /dev/null +++ b/src/lib/dbus/bus.ts @@ -0,0 +1,241 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { EventEmitter } from 'node:events' + +import createDebug from 'debug' + +import constants from './constants.js' +import { introspectBus } from './introspect.js' +import stdDbusIfaces from './stdifaces.js' + +const debug = createDebug('HAP-NodeJS:DBus') + +export default function MessageBus(conn, opts) { + if (!(this instanceof MessageBus)) { + return new MessageBus(conn) + } + if (!opts) { + opts = {} + } + + const self = this // eslint-disable-line ts/no-this-alias + this.connection = conn + this.serial = 1 + this.cookies = {} // TODO: rename to methodReturnHandlers + this.methodCallHandlers = {} + this.signals = new EventEmitter() + this.exportedObjects = {} + + this.invoke = function (msg, callback) { + if (!msg.type) { + msg.type = constants.messageType.methodCall + } + msg.serial = self.serial++ + this.cookies[msg.serial] = callback + self.connection.message(msg) + } + + this.invokeDbus = function (msg, callback) { + if (!msg.path) { + msg.path = '/org/freedesktop/DBus' + } + if (!msg.destination) { + msg.destination = 'org.freedesktop.DBus' + } + if (!msg.interface) { + msg.interface = 'org.freedesktop.DBus' + } + self.invoke(msg, callback) + } + + this.mangle = function (path, iface, member) { + const obj = {} + if (typeof path === 'object') { + // handle one argument case mangle(msg) + obj.path = path.path + obj.interface = path.interface + obj.member = path.member + } else { + obj.path = path + obj.interface = iface + obj.member = member + } + return JSON.stringify(obj) + } + + // Warning: errorName must respect the same rules as interface names (must contain a dot) + this.sendError = function (msg, errorName, errorText) { + const reply = { + type: constants.messageType.error, + serial: self.serial++, + replySerial: msg.serial, + destination: msg.sender, + errorName, + signature: 's', + body: [errorText], + } + this.connection.message(reply) + } + + // route reply/error + this.connection.on('message', (msg) => { + function invoke(impl, func, resultSignature) { + Promise.resolve() + .then(() => { + return func.apply(impl, (msg.body || []).concat(msg)) + }) + .then( + (methodReturnResult) => { + const methodReturnReply = { + type: constants.messageType.methodReturn, + serial: self.serial++, + destination: msg.sender, + replySerial: msg.serial, + } + if (methodReturnResult !== null) { + methodReturnReply.signature = resultSignature + methodReturnReply.body = [methodReturnResult] + } + self.connection.message(methodReturnReply) + }, + (e) => { + self.sendError(msg, e.dbusName || 'org.freedesktop.DBus.Error.Failed', e.message || '') + }, + ) + } + + let handler + if (msg.type === constants.messageType.methodReturn || msg.type === constants.messageType.error) { + handler = self.cookies[msg.replySerial] + if (handler) { + delete self.cookies[msg.replySerial] + const props = { + connection: self.connection, + bus: self, + message: msg, + signature: msg.signature, + } + let args = msg.body || [] + if (msg.type === constants.messageType.methodReturn) { + args = [null].concat(args) // first argument - no errors, null + handler.apply(props, args) // body as array of arguments + } else { + handler.call(props, { name: msg.errorName, message: args }) // body as first argument + } + } + } else if (msg.type === constants.messageType.signal) { + self.signals.emit(self.mangle(msg), msg.body, msg.signature) + } else { + // methodCall + if (stdDbusIfaces(msg, self)) { + return + } + + // exported interfaces handlers + const obj = self.exportedObjects[msg.path] + let iface + + if (obj) { + iface = obj[msg.interface] + if (iface) { + // now we are ready to serve msg.member + const impl = iface[1] + const func = impl[msg.member] + if (!func) { + self.sendError(msg, 'org.freedesktop.DBus.Error.UnknownMethod', `Method "${msg.member}" on interface "${msg.interface}" doesn't exist`) + return + } + // TODO safety check here + const resultSignature = iface[0].methods[msg.member][1] + invoke(impl, func, resultSignature) + return + } else { + // TODO: respond with standard dbus error + debug(`Interface ${msg.interface} is not supported`) + } + } + + // setMethodCall handlers + handler = self.methodCallHandlers[self.mangle(msg)] + if (handler) { + invoke(null, handler[0], handler[1]) + } else { + self.sendError(msg, 'org.freedesktop.DBus.Error.UnknownService', 'Uh oh oh') + } + } + }) + + // register name + if (opts.direct !== true) { + this.invokeDbus({ member: 'Hello' }, (err, name) => { + if (err) { + throw new Error(err) + } + self.name = name + }) + } else { + self.name = null + } + + // eslint-disable-next-line unicorn/consistent-function-scoping + function DBusObject(name, service) { + this.name = name + this.service = service + this.as = function (name) { + return this.proxy[name] + } + } + + function DBusService(name, bus) { + this.name = name + this.bus = bus + this.getObject = function (name, callback) { + if (name === undefined) { + return callback(new Error('Object name is null or undefined')) + } + const obj = new DBusObject(name, this) + introspectBus(obj, (err, ifaces, nodes) => { + if (err) { + return callback(err) + } + obj.proxy = ifaces + obj.nodes = nodes + callback(null, obj) + }) + } + + this.getInterface = function (objName, ifaceName, callback) { + this.getObject(objName, (err, obj) => { + if (err) { + return callback(err) + } + callback(null, obj.as(ifaceName)) + }) + } + } + + this.getService = function (name) { + return new DBusService(name, this) + } + + this.getObject = function (path, name, callback) { + const service = this.getService(path) + return service.getObject(name, callback) + } + + this.getInterface = function (path, objname, name, callback) { + return this.getObject(path, objname, (err, obj) => { + if (err) { + return callback(err) + } + callback(null, obj.as(name)) + }) + } + + this.removeMatch = function (match, callback) { + this.invokeDbus( + { member: 'RemoveMatch', signature: 's', body: [match] }, + callback, + ) + } +} diff --git a/src/lib/dbus/constants.ts b/src/lib/dbus/constants.ts new file mode 100644 index 000000000..a641afc9d --- /dev/null +++ b/src/lib/dbus/constants.ts @@ -0,0 +1,54 @@ +export default { + messageType: { + invalid: 0, + methodCall: 1, + methodReturn: 2, + error: 3, + signal: 4, + }, + + headerTypeName: [ + null, + 'path', + 'interface', + 'member', + 'errorName', + 'replySerial', + 'destination', + 'sender', + 'signature', + ], + + // TODO: merge to single hash? e.g path -> [1, 'o'] + fieldSignature: { + path: 'o', + interface: 's', + member: 's', + errorName: 's', + replySerial: 'u', + destination: 's', + sender: 's', + signature: 'g', + }, + headerTypeId: { + path: 1, + interface: 2, + member: 3, + errorName: 4, + replySerial: 5, + destination: 6, + sender: 7, + signature: 8, + }, + protocolVersion: 1, + flags: { + noReplyExpected: 1, + noAutoStart: 2, + }, + endianness: { + le: 108, + be: 66, + }, + messageSignature: 'yyyyuua(yv)', + defaultAuthMethods: ['EXTERNAL', 'DBUS_COOKIE_SHA1', 'ANONYMOUS'], +} diff --git a/src/lib/dbus/dbus-buffer.ts b/src/lib/dbus/dbus-buffer.ts new file mode 100644 index 000000000..2c8bba33e --- /dev/null +++ b/src/lib/dbus/dbus-buffer.ts @@ -0,0 +1,192 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import long from 'long' + +import parseSignature from './signature.js' + +const { fromBits } = long + +// Buffer + position + global start position ( used in alignment ) +export default function DBusBuffer(buffer, startPos, options) { + if (typeof options !== 'object') { + options = { ayBuffer: true, ReturnLongjs: false } + } else if (options.ayBuffer === undefined) { + // default settings object + options.ayBuffer = true // enforce truthy default props + } + this.options = options + this.buffer = buffer + this.startPos = startPos || 0 + this.pos = 0 +} + +DBusBuffer.prototype.align = function (power) { + const allBits = (1 << power) - 1 + const paddedOffset = ((this.pos + this.startPos + allBits) >> power) << power + this.pos = paddedOffset - this.startPos +} + +DBusBuffer.prototype.readInt8 = function () { + this.pos++ + return this.buffer[this.pos - 1] +} + +DBusBuffer.prototype.readSInt16 = function () { + this.align(1) + const res = this.buffer.readInt16LE(this.pos) + this.pos += 2 + return res +} + +DBusBuffer.prototype.readInt16 = function () { + this.align(1) + const res = this.buffer.readUInt16LE(this.pos) + this.pos += 2 + return res +} + +DBusBuffer.prototype.readSInt32 = function () { + this.align(2) + const res = this.buffer.readInt32LE(this.pos) + this.pos += 4 + return res +} + +DBusBuffer.prototype.readInt32 = function () { + this.align(2) + const res = this.buffer.readUInt32LE(this.pos) + this.pos += 4 + return res +} + +DBusBuffer.prototype.readDouble = function () { + this.align(3) + const res = this.buffer.readDoubleLE(this.pos) + this.pos += 8 + return res +} + +DBusBuffer.prototype.readString = function (len) { + if (len === 0) { + this.pos++ + return '' + } + const res = this.buffer.toString('utf8', this.pos, this.pos + len) + this.pos += len + 1 // dbus strings are always zero-terminated ('s' and 'g' types) + return res +} + +DBusBuffer.prototype.readTree = function readTree(tree) { + switch (tree.type) { + case '(': + case '{': + case 'r': + this.align(3) + return this.readStruct(tree.child) + case 'a': { + if (!tree.child || tree.child.length !== 1) { + throw new Error('Incorrect array element signature') + } + const arrayBlobLength = this.readInt32() + return this.readArray(tree.child[0], arrayBlobLength) + } + case 'v': + return this.readVariant() + default: + return this.readSimpleType(tree.type) + } +} + +DBusBuffer.prototype.read = function read(signature) { + const tree = parseSignature(signature) + return this.readStruct(tree) +} + +DBusBuffer.prototype.readVariant = function readVariant() { + const signature = this.readSimpleType('g') + const tree = parseSignature(signature) + return [tree, this.readStruct(tree)] +} + +DBusBuffer.prototype.readStruct = function readStruct(struct) { + const result = [] + for (let i = 0; i < struct.length; ++i) { + result.push(this.readTree(struct[i])) + } + return result +} + +DBusBuffer.prototype.readArray = function readArray(eleType, arrayBlobSize) { + const start = this.pos + + // special case: treat ay as Buffer + if (eleType.type === 'y' && this.options.ayBuffer) { + this.pos += arrayBlobSize + return this.buffer.slice(start, this.pos) + } + + // end of array is start of first element + array size + // we need to add 4 bytes if not on 8-byte boundary + // and array element needs 8 byte alignment + if (['x', 't', 'd', '{', '(', 'r'].includes(eleType.type)) { + this.align(3) + } + const end = this.pos + arrayBlobSize + const result = [] + while (this.pos < end) result.push(this.readTree(eleType)) + return result +} + +DBusBuffer.prototype.readSimpleType = function readSimpleType(t) { + let data, len, word0, word1 + switch (t) { + case 'y': + return this.readInt8() + case 'b': + // TODO: spec says that true is strictly 1 and false is strictly 0 + // shold we error (or warn?) when non 01 values? + return !!this.readInt32() + case 'n': + return this.readSInt16() + case 'q': + return this.readInt16() + case 'u': + return this.readInt32() + case 'i': + return this.readSInt32() + case 'g': + len = this.readInt8() + return this.readString(len) + case 's': + case 'o': + len = this.readInt32() + return this.readString(len) + // TODO: validate object path here + // if (t === 'o' && !isValidObjectPath(str)) + // throw new Error('string is not a valid object path')); + case 'x': + // signed + this.align(3) + word0 = this.readInt32() + word1 = this.readInt32() + data = fromBits(word0, word1, false) + if (this.options.ReturnLongjs) { + return data + } + return data.toNumber() // convert to number (good up to 53 bits) + case 't': + // unsigned + this.align(3) + word0 = this.readInt32() + word1 = this.readInt32() + data = fromBits(word0, word1, true) + if (this.options.ReturnLongjs) { + return data + } + return data.toNumber() // convert to number (good up to 53 bits) + case 'd': + return this.readDouble() + default: + throw new Error(`Unsupported type: ${t}`) + } +} diff --git a/src/lib/dbus/handshake.ts b/src/lib/dbus/handshake.ts new file mode 100644 index 000000000..9e30b80f4 --- /dev/null +++ b/src/lib/dbus/handshake.ts @@ -0,0 +1,152 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { createHash } from 'node:crypto' +import { readFile, stat } from 'node:fs' +import { join } from 'node:path' +import process from 'node:process' + +import createDebug from 'debug' +import { Buffer } from 'safe-buffer' + +import constants from './constants.js' +import readLine from './readline.js' + +const debug = createDebug('HAP-NodeJS:DBus') + +function sha1(input) { + const shasum = createHash('sha1') + shasum.update(input) + return shasum.digest('hex') +} + +function getUserHome() { + return process.env[process.platform.match(/\$win/) ? 'USERPROFILE' : 'HOME'] +} + +function getCookie(context, id, cb) { + // http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha + const dirname = join(getUserHome(), '.dbus-keyrings') + // > There is a default context, "org_freedesktop_general" that's used by servers that do not specify otherwise. + if (context.length === 0) { + context = 'org_freedesktop_general' + } + + const filename = join(dirname, context) + // check it's not writable by others and readable by user + stat(dirname, (err, stat) => { + if (err) { + return cb(err) + } + if (stat.mode & 0o22) { + return cb( + new Error( + 'User keyrings directory is writeable by other users. Aborting authentication', + ), + ) + } + // eslint-disable-next-line no-prototype-builtins + if (process.hasOwnProperty('getuid') && stat.uid !== process.getuid()) { + return cb( + new Error( + 'Keyrings directory is not owned by the current user. Aborting authentication!', + ), + ) + } + readFile(filename, 'ascii', (err, keyrings) => { + if (err) { + return cb(err) + } + const lines = keyrings.split('\n') + for (let l = 0; l < lines.length; ++l) { + const data = lines[l].split(' ') + if (id === data[0]) { + return cb(null, data[2]) + } + } + return cb(new Error('cookie not found')) + }) + }) +} + +function hexlify(input) { + return Buffer.from(input.toString(), 'ascii').toString('hex') +} + +export default function auth(stream, opts, cb) { + // filter used to make a copy so we don't accidentally change opts data + let authMethods + if (opts.authMethods) { + authMethods = opts.authMethods + } else { + authMethods = constants.defaultAuthMethods + } + stream.write('\0') + tryAuth(stream, authMethods.slice(), cb) +} + +function tryAuth(stream, methods, cb) { + if (methods.length === 0) { + return cb(new Error('No authentication methods left to try')) + } + + const authMethod = methods.shift() + // eslint-disable-next-line no-prototype-builtins + const uid = process.hasOwnProperty('getuid') ? process.getuid() : 0 + const id = hexlify(uid) + + function beginOrNextAuth() { + readLine(stream, (line) => { + const ok = line.toString('ascii').match(/^([A-Z]+) (.*)/i) + if (ok && ok[1] === 'OK') { + stream.write('BEGIN\r\n') + return cb(null, ok[2]) // ok[2] = guid. Do we need it? + } else { + // TODO: parse error! + if (!methods.empty) { + tryAuth(stream, methods, cb) + } else { + return cb(line) + } + } + }) + } + + switch (authMethod) { + case 'EXTERNAL': + stream.write(`AUTH ${authMethod} ${id}\r\n`) + beginOrNextAuth() + break + case 'DBUS_COOKIE_SHA1': + stream.write(`AUTH ${authMethod} ${id}\r\n`) + readLine(stream, (line) => { + const data = Buffer.from(line.toString().split(' ')[1].trim(), 'hex') + .toString() + .split(' ') + const cookieContext = data[0] + const cookieId = data[1] + const serverChallenge = data[2] + // any random 16 bytes should work, sha1(rnd) to make it simpler + const clientChallenge = crypto.randomBytes(16).toString('hex') + getCookie(cookieContext, cookieId, (err, cookie) => { + if (err) { + return cb(err) + } + const response = sha1( + [serverChallenge, clientChallenge, cookie].join(':'), + ) + const reply = hexlify(clientChallenge + response) + stream.write(`DATA ${reply}\r\n`) + beginOrNextAuth() + }) + }) + break + case 'ANONYMOUS': + stream.write('AUTH ANONYMOUS \r\n') + beginOrNextAuth() + break + default: + debug(`Unsupported auth method: ${authMethod}`) + beginOrNextAuth() + break + } +} diff --git a/src/lib/dbus/index.ts b/src/lib/dbus/index.ts new file mode 100644 index 000000000..f40d1ce56 --- /dev/null +++ b/src/lib/dbus/index.ts @@ -0,0 +1,148 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { EventEmitter } from 'node:events' +import { createConnection as netCreateConnection } from 'node:net' +import process from 'node:process' + +import createDebug from 'debug' + +import MessageBus from './bus.js' +import clientHandshake from './handshake.js' +import { marshalMessage, unmarshalMessages } from './message.js' + +const debug = createDebug('HAP-NodeJS:DBus') + +function createStream(opts) { + if (opts.stream) { + return opts.stream + } + let host = opts.host + let port = opts.port + const socket = opts.socket + if (socket) { + return netCreateConnection(socket) + } + if (port) { + return netCreateConnection(port, host) + } + + const busAddress = opts.busAddress || process.env.DBUS_SESSION_BUS_ADDRESS + if (!busAddress) { + throw new Error('unknown bus address') + } + + const addresses = busAddress.split(';') + for (let i = 0; i < addresses.length; ++i) { + const address = addresses[i] + const familyParams = address.split(':') + const family = familyParams[0] + const params = {} + familyParams[1].split(',').map((p) => { // eslint-disable-line array-callback-return + const keyVal = p.split('=') + params[keyVal[0]] = keyVal[1] + }) + + try { + switch (family.toLowerCase()) { + case 'tcp': + host = params.host || 'localhost' + port = params.port + return netCreateConnection(port, host) + case 'unix': + if (params.socket) { + return netCreateConnection(params.socket) + } + if (params.path) { + return netCreateConnection(params.path) + } + throw new Error( + 'not enough parameters for \'unix\' connection - you need to specify \'socket\' or \'abstract\' or \'path\' parameter', + ) + default: + throw new Error(`unknown address type:${family}`) + } + } catch (e) { + if (i < addresses.length - 1) { + debug(e.message) + } else { + throw e + } + } + } +} + +function createConnection(opts) { + const self = new EventEmitter() + if (!opts) { + opts = {} + } + const stream = (self.stream = createStream(opts)) + stream.setNoDelay() + + stream.on('error', (err) => { + // forward network and stream errors + self.emit('error', err) + }) + + stream.on('end', () => { + self.emit('end') + self.message = function () { + debug('Didn\'t write bytes to closed stream') + } + }) + + self.end = function () { + stream.end() + return self + } + + clientHandshake(stream, opts, (error, guid) => { + if (error) { + return self.emit('error', error) + } + self.guid = guid + self.emit('connect') + unmarshalMessages( + stream, + (message) => { + self.emit('message', message) + }, + opts, + ) + }) + + self._messages = [] + + // pre-connect version, buffers all messages. replaced after connect + self.message = function (msg) { + self._messages.push(msg) + } + + self.once('connect', () => { + self.state = 'connected' + for (let i = 0; i < self._messages.length; ++i) { + stream.write(marshalMessage(self._messages[i])) + } + self._messages.length = 0 + + // no need to buffer once connected + self.message = function (msg) { + stream.write(marshalMessage(msg)) + } + }) + + return self +} + +function createClient(params) { + const connection = createConnection(params || {}) + return new MessageBus(connection, params || {}) +} + +export function systemBus() { + return createClient({ + busAddress: + process.env.DBUS_SYSTEM_BUS_ADDRESS + || 'unix:path=/var/run/dbus/system_bus_socket', + }) +} diff --git a/src/lib/dbus/introspect.ts b/src/lib/dbus/introspect.ts new file mode 100644 index 000000000..d35424b34 --- /dev/null +++ b/src/lib/dbus/introspect.ts @@ -0,0 +1,205 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { Parser } from 'xml2js' + +export function introspectBus(obj, callback) { + const bus = obj.service.bus + bus.invoke( + { + destination: obj.service.name, + path: obj.name, + interface: 'org.freedesktop.DBus.Introspectable', + member: 'Introspect', + }, + (err, xml) => { + processXML(err, xml, obj, callback) + }, + ) +} + +export function processXML(err, xml, obj, callback) { + if (err) { + return callback(err) + } + const parser = new Parser() + parser.parseString(xml, (err, result) => { + if (err) { + return callback(err) + } + if (!result.node) { + throw new Error('No root XML node') + } + result = result.node // unwrap the root node + // If no interface, try first sub node? + if (!result.interface) { + if (result.node && result.node.length > 0 && result.node[0].$) { + const subObj = Object.assign(obj, {}) + if (subObj.name.slice(-1) !== '/') { + subObj.name += '/' + } + subObj.name += result.node[0].$.name + return module.exports.introspectBus(subObj, callback) + } + return callback(new Error('No such interface found')) + } + const proxy = {} + const nodes = [] + let ifaceName, method, property, iface, arg, signature, currentIface + const ifaces = result.interface + const xmlnodes = result.node || [] + + for (let n = 1; n < xmlnodes.length; ++n) { + // Start at 1 because we want to skip the root node + nodes.push(xmlnodes[n].$.name) + } + + for (let i = 0; i < ifaces.length; ++i) { + iface = ifaces[i] + ifaceName = iface.$.name + currentIface = proxy[ifaceName] = new DBusInterface(obj, ifaceName) + + for (let m = 0; iface.method && m < iface.method.length; ++m) { + method = iface.method[m] + signature = '' + const methodName = method.$.name + for (let a = 0; method.arg && a < method.arg.length; ++a) { + arg = method.arg[a].$ + if (arg.direction === 'in') { + signature += arg.type + } + } + // add method + currentIface.$createMethod(methodName, signature) + } + for (let p = 0; iface.property && p < iface.property.length; ++p) { + property = iface.property[p] + currentIface.$createProp( + property.$.name, + property.$.type, + property.$.access, + ) + } + // TODO: introspect signals + } + callback(null, proxy, nodes) + }) +} + +function DBusInterface(parent_obj, ifname) { + // Since methods and props presently get added directly to the object, to avoid collision with existing names we must use $ naming convention as $ is invalid for dbus member names + // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names + this.$parent = parent_obj // parent DbusObject + this.$name = ifname // string interface name +} + +DBusInterface.prototype.$getSigHandler = function (callback) { + let index = this.$callbacks.indexOf(callback) + if (index === -1) { + index = this.$callbacks.push(callback) - 1 + this.$sigHandlers[index] = function (messageBody) { + callback(...messageBody) + } + } + return this.$sigHandlers[index] +} + +DBusInterface.prototype.removeListener = DBusInterface.prototype.off = function (signame, callback) { + const bus = this.$parent.service.bus + const signalFullName = bus.mangle(this.$parent.name, this.$name, signame) + bus.signals.removeListener(signalFullName, this.$getSigHandler(callback)) + if (!bus.signals.listeners(signalFullName).length) { + // There is no event handlers for this match + const match = getMatchRule(this.$parent.name, this.$name, signame) + bus.removeMatch( + match, + (err) => { + if (err) { + throw new Error(err) + } + // Now it is safe to empty these arrays + this.$callbacks.length = 0 + this.$sigHandlers.length = 0 + }, + ) + } +} + +DBusInterface.prototype.$createMethod = function (mName, signature) { + this.$methods[mName] = signature + this[mName] = function (...args) { + this.$callMethod(mName, args) + } +} + +DBusInterface.prototype.$callMethod = function (mName, args) { + const bus = this.$parent.service.bus + if (!Array.isArray(args)) { + args = Array.from(args) // Array.prototype.slice.apply(args) + } + const callback + = typeof args[args.length - 1] === 'function' ? args.pop() : function () {} + const msg = { + destination: this.$parent.service.name, + path: this.$parent.name, + interface: this.$name, + member: mName, + } + if (this.$methods[mName] !== '') { + msg.signature = this.$methods[mName] + msg.body = args + } + bus.invoke(msg, callback) +} + +DBusInterface.prototype.$createProp = function (propName, propType, propAccess) { + this.$properties[propName] = { type: propType, access: propAccess } + Object.defineProperty(this, propName, { + enumerable: true, + get: () => callback => this.$readProp(propName, callback), // eslint-disable-line unicorn/consistent-function-scoping + set(val) { + this.$writeProp(propName, val) + }, + }) +} + +DBusInterface.prototype.$readProp = function (propName, callback) { + const bus = this.$parent.service.bus + bus.invoke( + { + destination: this.$parent.service.name, + path: this.$parent.name, + interface: 'org.freedesktop.DBus.Properties', + member: 'Get', + signature: 'ss', + body: [this.$name, propName], + }, + (err, val) => { + if (err) { + callback(err) + } else { + const signature = val[0] + if (signature.length === 1) { + callback(err, val[1][0]) + } else { + callback(err, val[1]) + } + } + }, + ) +} + +DBusInterface.prototype.$writeProp = function (propName, val) { + const bus = this.$parent.service.bus + bus.invoke({ + destination: this.$parent.service.name, + path: this.$parent.name, + interface: 'org.freedesktop.DBus.Properties', + member: 'Set', + signature: 'ssv', + body: [this.$name, propName, [this.$properties[propName].type, val]], + }) +} + +function getMatchRule(objName, ifName, signame) { + return `type='signal',path='${objName}',interface='${ifName}',member='${signame}'` +} diff --git a/src/lib/dbus/marshall.ts b/src/lib/dbus/marshall.ts new file mode 100644 index 000000000..9da217ead --- /dev/null +++ b/src/lib/dbus/marshall.ts @@ -0,0 +1,110 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import assert from 'node:assert' +import { Buffer } from 'node:buffer' + +import align from './align.js' +import MakeSimpleMarshaller from './marshallers.js' +import put from './put.js' +import parseSignature from './signature.js' + +export default function marshall(signature, data, offset = 0) { + const tree = parseSignature(signature) + if (!Array.isArray(data) || data.length !== tree.length) { + throw new Error( + `message body does not match message signature. Body:${JSON.stringify( + data, + )}, signature:${signature}`, + ) + } + const putstream = put() + putstream._offset = offset + return writeStruct(putstream, tree, data).buffer() +} + +function writeStruct(ps, tree, data) { + if (tree.length !== data.length) { + throw new Error('Invalid struct data') + } + for (let i = 0; i < tree.length; ++i) { + write(ps, tree[i], data[i]) + } + return ps +} + +function write(ps, ele, data) { + switch (ele.type) { + case '(': + case '{': + align(ps, 8) + writeStruct(ps, ele.child, data) + break + case 'a': { + // array serialisation: + // length of array body aligned at 4 byte boundary + // (optional 4 bytes to align first body element on 8-byte boundary if element + // body + const arrPut = put() + arrPut._offset = ps._offset + const _offset = arrPut._offset + writeSimple(arrPut, 'u', 0) // array length placeholder + const lengthOffset = arrPut._offset - 4 - _offset + // we need to align here because alignment is not included in array length + if (['x', 't', 'd', '{', '('].includes(ele.child[0].type)) { + align(arrPut, 8) + } + const startOffset = arrPut._offset + for (let i = 0; i < data.length; ++i) { + write(arrPut, ele.child[0], data[i]) + } + const arrBuff = arrPut.buffer() + const length = arrPut._offset - startOffset + // lengthOffset in the range 0 to 3 depending on number of align bytes padded _before_ arrayLength + arrBuff.writeUInt32LE(length, lengthOffset) + ps.put(arrBuff) + ps._offset += arrBuff.length + break + } + case 'v': { + // TODO: allow serialisation of simple types as variants, e. g 123 -> ['u', 123], true -> ['b', 1], 'abc' -> ['s', 'abc'] + assert.equal(data.length, 2, 'variant data should be [signature, data]') + const signatureEle = { + type: 'g', + child: [], + } + write(ps, signatureEle, data[0]) + const tree = parseSignature(data[0]) + assert(tree.length === 1) + write(ps, tree[0], data[1]) + break + } + default: + return writeSimple(ps, ele.type, data) + } +} + +const stringTypes = ['g', 'o', 's'] + +function writeSimple(ps, type, data) { + if (typeof data === 'undefined') { + throw new TypeError('Serialisation of JS \'undefined\' type is not supported by d-bus') + } + if (data === null) { + throw new Error('Serialisation of null value is not supported by d-bus') + } + + if (Buffer.isBuffer(data)) { + data = data.toString()// encoding? + } + if (stringTypes.includes(type) && typeof data !== 'string') { + throw new Error( + `Expected string or buffer argument, got ${JSON.stringify( + data, + )} of type '${type}'`, + ) + } + + const simpleMarshaller = MakeSimpleMarshaller(type) + simpleMarshaller.marshall(ps, data) + return ps +} diff --git a/src/lib/dbus/marshallers.ts b/src/lib/dbus/marshallers.ts new file mode 100644 index 000000000..ce1cb82ad --- /dev/null +++ b/src/lib/dbus/marshallers.ts @@ -0,0 +1,342 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import Long from 'long' +import { Buffer } from 'safe-buffer' + +import align from './align.js' +import parseSignature from './signature.js' + +function checkRange(minValue, maxValue, data) { + if (data > maxValue || data < minValue) { + throw new Error('Number outside range') + } +} + +function checkInteger(data) { + if (typeof data !== 'number') { + throw new TypeError(`Data: ${data} was not of type number`) + } + if (Math.floor(data) !== data) { + throw new Error(`Data: ${data} was not an integer`) + } +} + +function checkBoolean(data) { + if (!(typeof data === 'boolean' || data === 0 || data === 1)) { + throw new Error(`Data: ${data} was not of type boolean`) + } +} + +// This is essentially a tweaked version of 'fromValue' from Long.js with error checking. +// This can take number or string of decimal characters or 'Long' instance (or Long-style object with props low,high,unsigned). +function makeLong(val, signed) { + if (val instanceof Long) { + return val + } + if (val instanceof Number) { + val = val.valueOf() + } + if (typeof val === 'number') { + try { + // Long.js won't alert you to precision loss in passing more than 53 bit int through a double number, so we check here + checkInteger(val) + if (signed) { + checkRange(-0x1FFFFFFFFFFFFF, 0x1FFFFFFFFFFFFF, val) + } else { + checkRange(0, 0x1FFFFFFFFFFFFF, val) + } + } catch (e) { + e.message += ' (Number type can only carry 53 bit integer)' + throw e + } + try { + return Long.fromNumber(val, !signed) + } catch (e) { + e.message = `Error converting number to 64bit integer "${e.message}"` + throw e + } + } + if (typeof val === 'string' || val instanceof String) { + let radix = 10 + val = val.trim().toUpperCase() // remove extra whitespace and make uppercase (for hex) + if (val.substring(0, 2) === '0X') { + radix = 16 + val = val.substring(2) + } else if (val.substring(0, 3) === '-0X') { + // unusual, but just in case? + radix = 16 + val = `-${val.substring(3)}` + } + val = val.replace(/^0+(?=\d)/, '') // dump leading zeroes + let data + try { + data = Long.fromString(val, !signed, radix) + } catch (e) { + e.message = `Error converting string to 64bit integer '${e.message}'` + throw e + } + // If string represents a number outside 64 bit range, it can quietly overflow. + // We assume if things converted correctly the string coming out of Long should match what went into it. + if (data.toString(radix).toUpperCase() !== val) { + throw new Error( + `Data: '${val}' did not convert correctly to ${ + signed ? 'signed' : 'unsigned' + } 64 bit`, + ) + } + return data + } + // Throws for non-objects, converts non-instanceof Long: + try { + return Long.fromBits(val.low, val.high, val.unsigned) + } catch (e) { + e.message = `Error converting object to 64bit integer '${e.message}'` + throw e + } +} + +function checkLong(data, signed) { + if (!Long.isLong(data)) { + data = makeLong(data, signed) + } + + // Do we enforce that Long.js object unsigned/signed match the field even if it is still in range? + // Probably, might help users avoid unintended bugs? + if (signed) { + if (data.unsigned) { + throw new Error( + 'Longjs object is unsigned, but marshalling into signed 64 bit field', + ) + } + if (data.gt(Long.MAX_VALUE) || data.lt(Long.MIN_VALUE)) { + throw new Error(`Data: ${data} was out of range (64-bit signed)`) + } + } else { + if (!data.unsigned) { + throw new Error( + 'Longjs object is signed, but marshalling into unsigned 64 bit field', + ) + } + // NOTE: data.gt(Long.MAX_UNSIGNED_VALUE) will catch if Long.js object is a signed value but is still within unsigned range! + // Since we are enforcing signed type matching between Long.js object and field, this note should not matter. + if (data.gt(Long.MAX_UNSIGNED_VALUE) || data.lt(0)) { + throw new Error(`Data: ${data} was out of range (64-bit unsigned)`) + } + } + return data +} + +function checkValidSignature(data) { + if (data.length > 0xFF) { + throw new Error( + `Data: ${data} is too long for signature type (${data.length} > 255)`, + ) + } + + let parenCount = 0 + for (let ii = 0; ii < data.length; ++ii) { + if (parenCount > 32) { + throw new Error( + `Maximum container type nesting exceeded in signature type:${data}`, + ) + } + switch (data[ii]) { + case '(': + ++parenCount + break + case ')': + --parenCount + break + default: + /* no-op */ + break + } + } + parseSignature(data) +} + +function checkValidString(data) { + if (typeof data !== 'string') { + throw new TypeError(`Data: ${data} was not of type string`) + } else if (data.includes('\0')) { + throw new Error('String contains null byte') + } +} + +/** + * MakeSimpleMarshaller + * @param signature - the signature of the data you want to check + * @returns a simple marshaller with the "check" method + * + * check returns nothing - it only raises errors if the data is + * invalid for the signature + */ +export default function MakeSimpleMarshaller(signature) { + const marshaller = {} + + switch (signature) { + case 'o': + // object path + // TODO: verify object path here? + case 's': // eslint-disable-line no-fallthrough + // STRING + marshaller.check = function (data) { + checkValidString(data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + // utf8 string + align(ps, 4) + const buff = Buffer.from(data, 'utf8') + ps.word32le(buff.length).put(buff).word8(0) + ps._offset += 5 + buff.length + } + break + case 'g': + // SIGNATURE + marshaller.check = function (data) { + checkValidString(data) + checkValidSignature(data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + // signature + const buff = Buffer.from(data, 'ascii') + ps.word8(data.length).put(buff).word8(0) + ps._offset += 2 + buff.length + } + break + case 'y': + // BYTE + marshaller.check = function (data) { + checkInteger(data) + checkRange(0x00, 0xFF, data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + ps.word8(data) + ps._offset++ + } + break + case 'b': + // BOOLEAN + marshaller.check = function (data) { + checkBoolean(data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + // booleans serialised as 0/1 unsigned 32 bit int + data = data ? 1 : 0 + align(ps, 4) + ps.word32le(data) + ps._offset += 4 + } + break + case 'n': + // INT16 + marshaller.check = function (data) { + checkInteger(data) + checkRange(-0x7FFF - 1, 0x7FFF, data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + align(ps, 2) + const buff = Buffer.alloc(2) + buff.writeInt16LE(Number.parseInt(data), 0) + ps.put(buff) + ps._offset += 2 + } + break + case 'q': + // UINT16 + marshaller.check = function (data) { + checkInteger(data) + checkRange(0, 0xFFFF, data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + align(ps, 2) + ps.word16le(data) + ps._offset += 2 + } + break + case 'i': + // INT32 + marshaller.check = function (data) { + checkInteger(data) + checkRange(-0x7FFFFFFF - 1, 0x7FFFFFFF, data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + align(ps, 4) + const buff = Buffer.alloc(4) + buff.writeInt32LE(Number.parseInt(data), 0) + ps.put(buff) + ps._offset += 4 + } + break + case 'u': + // UINT32 + marshaller.check = function (data) { + checkInteger(data) + checkRange(0, 0xFFFFFFFF, data) + } + marshaller.marshall = function (ps, data) { + this.check(data) + // 32 t unsigned int + align(ps, 4) + ps.word32le(data) + ps._offset += 4 + } + break + case 't': + // UINT64 + marshaller.check = function (data) { + return checkLong(data, false) + } + marshaller.marshall = function (ps, data) { + data = this.check(data) + align(ps, 8) + ps.word32le(data.low) + ps.word32le(data.high) + ps._offset += 8 + } + break + case 'x': + // INT64 + marshaller.check = function (data) { + return checkLong(data, true) + } + marshaller.marshall = function (ps, data) { + data = this.check(data) + align(ps, 8) + ps.word32le(data.low) + ps.word32le(data.high) + ps._offset += 8 + } + break + case 'd': + // DOUBLE + marshaller.check = function (data) { + if (typeof data !== 'number') { + throw new TypeError(`Data: ${data} was not of type number`) + } else if (Number.isNaN(data)) { + throw new TypeError(`Data: ${data} was not a number`) + } else if (!Number.isFinite(data)) { + throw new TypeError('Number outside range') + } + } + marshaller.marshall = function (ps, data) { + this.check(data) + align(ps, 8) + const buff = Buffer.alloc(8) + buff.writeDoubleLE(Number.parseFloat(data), 0) + ps.put(buff) + ps._offset += 8 + } + break + default: + throw new Error(`Unknown data type format: ${signature}`) + } + return marshaller +} diff --git a/src/lib/dbus/message.ts b/src/lib/dbus/message.ts new file mode 100644 index 000000000..c09531e3b --- /dev/null +++ b/src/lib/dbus/message.ts @@ -0,0 +1,126 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { Buffer } from 'safe-buffer' + +import constants from './constants.js' +import DBusBuffer from './dbus-buffer.js' +import marshall from './marshall.js' + +const headerSignature = [ + { + type: 'a', + child: [ + { + type: '(', + child: [ + { + type: 'y', + child: [], + }, + { + type: 'v', + child: [], + }, + ], + }, + ], + }, +] + +export function unmarshalMessages(stream, onMessage, opts) { + let state = 0 // 0: header, 1: fields + body + let header, fieldsAndBody + let fieldsLength, fieldsLengthPadded + let fieldsAndBodyLength = 0 + let bodyLength = 0 + stream.on('readable', () => { + while (1) { + if (state === 0) { + header = stream.read(16) + if (!header) { + break + } + state = 1 + + fieldsLength = header.readUInt32LE(12) + fieldsLengthPadded = ((fieldsLength + 7) >> 3) << 3 + bodyLength = header.readUInt32LE(4) + fieldsAndBodyLength = fieldsLengthPadded + bodyLength + } else { + fieldsAndBody = stream.read(fieldsAndBodyLength) + if (!fieldsAndBody) { + break + } + state = 0 + + const messageBuffer = new DBusBuffer(fieldsAndBody, undefined, opts) + const unmarshalledHeader = messageBuffer.readArray( + headerSignature[0].child[0], + fieldsLength, + ) + messageBuffer.align(3) + let headerName + const message = {} + message.serial = header.readUInt32LE(8) + + for (let i = 0; i < unmarshalledHeader.length; ++i) { + headerName = constants.headerTypeName[unmarshalledHeader[i][0]] + message[headerName] = unmarshalledHeader[i][1][1][0] + } + + message.type = header[1] + message.flags = header[2] + + if (bodyLength > 0 && message.signature) { + message.body = messageBuffer.read(message.signature) + } + onMessage(message) + } + } + }) +} + +export function marshalMessage(message) { + if (!message.serial) { + throw new Error('Missing or invalid serial') + } + const flags = message.flags || 0 + const type = message.type || constants.messageType.methodCall + let bodyLength = 0 + let bodyBuff + if (message.signature && message.body) { + bodyBuff = marshall(message.signature, message.body) + bodyLength = bodyBuff.length + } + const header = [ + constants.endianness.le, + type, + flags, + constants.protocolVersion, + bodyLength, + message.serial, + ] + const headerBuff = marshall('yyyyuu', header) + const fields = [] + constants.headerTypeName.forEach((fieldName) => { + const fieldVal = message[fieldName] + if (fieldVal) { + fields.push([ + constants.headerTypeId[fieldName], + [constants.fieldSignature[fieldName], fieldVal], + ]) + } + }) + const fieldsBuff = marshall('a(yv)', [fields], 12) + const headerLenAligned + = ((headerBuff.length + fieldsBuff.length + 7) >> 3) << 3 + const messageLen = headerLenAligned + bodyLength + const messageBuff = Buffer.alloc(messageLen) + headerBuff.copy(messageBuff) + fieldsBuff.copy(messageBuff, headerBuff.length) + if (bodyLength > 0) { + bodyBuff.copy(messageBuff, headerLenAligned) + } + + return messageBuff +} diff --git a/src/lib/dbus/put.ts b/src/lib/dbus/put.ts new file mode 100644 index 000000000..e64afaf3a --- /dev/null +++ b/src/lib/dbus/put.ts @@ -0,0 +1,89 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { Buffer } from 'node:buffer' + +export default function Put() { + if (!(this instanceof Put)) { + return new Put() + } + + const words = [] + let len = 0 + + this.put = function (buf) { + words.push({ buffer: buf }) + len += buf.length + return this + } + + this.word8 = function (x) { + words.push({ bytes: 1, value: x }) + len += 1 + return this + } + + const bitSizes = [8, 16, 24, 32, 64] + for (let i = 0; i < bitSizes.length; i++) { + const bits = bitSizes[i] + this[`word${bits}be`] = function (x) { + words.push({ endian: 'big', bytes: bits / 8, value: x }) + len += bits / 8 + return this + } + + this[`word${bits}le`] = function (x) { + words.push({ endian: 'little', bytes: bits / 8, value: x }) + len += bits / 8 + return this + } + } + + this.length = function () { + return len + } + + this.buffer = function () { + const buf = Buffer.alloc(len) + let offset = 0 + words.forEach((word) => { + if (word.buffer) { + word.buffer.copy(buf, offset, 0) + offset += word.buffer.length + } else if (word.bytes === 'float') { + // s * f * 2^e + const v = Math.abs(word.value) + const s = (word.value >= 0) * 1 + const e = Math.ceil(Math.log(v) / Math.LN2) + const f = v / (1 << e) + + // s:1, e:7, f:23 + // [seeeeeee][efffffff][ffffffff][ffffffff] + buf[offset++] = (s << 7) & ~~(e / 2) + buf[offset++] = ((e & 1) << 7) & ~~(f / (1 << 16)) + buf[offset++] = 0 + buf[offset++] = 0 + offset += 4 + } else { + const big = word.endian === 'big' + const ix = big ? [(word.bytes - 1) * 8, -8] : [0, 8] + + for ( + let i = ix[0]; + big ? i >= 0 : i < word.bytes * 8; + i += ix[1] + ) { + if (i >= 32) { + buf[offset++] = Math.floor(word.value / 2 ** i) & 0xFF + } else { + buf[offset++] = (word.value >> i) & 0xFF + } + } + } + }) + return buf + } + + this.write = function (stream) { + stream.write(this.buffer()) + } +} diff --git a/src/lib/dbus/readline.ts b/src/lib/dbus/readline.ts new file mode 100644 index 000000000..6ae82c10f --- /dev/null +++ b/src/lib/dbus/readline.ts @@ -0,0 +1,27 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import { Buffer } from 'safe-buffer' + +export default function readOneLine(stream, cb) { + const bytes: any[] = [] + function readable() { + while (1) { + const buf = stream.read(1) + if (!buf) { + return + } + const b = buf[0] + if (b === 0x0A) { + try { + cb(Buffer.from(bytes)) + } catch (error) { + stream.emit('error', error) + } + stream.removeListener('readable', readable) + return + } + bytes.push(b) + } + } + stream.on('readable', readable) +} diff --git a/src/lib/dbus/signature.ts b/src/lib/dbus/signature.ts new file mode 100644 index 000000000..038435f8c --- /dev/null +++ b/src/lib/dbus/signature.ts @@ -0,0 +1,64 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +const match = { + '{': '}', + '(': ')', +} + +const knownTypes = {} +'(){}ybnqiuxtdsogarvehm*?@&^'.split('').forEach((c) => { + knownTypes[c] = true +}) + +function checkNotEnd(c) { + if (!c) { + throw new Error('Bad signature: unexpected end') + } + return c +} + +export default function parseSignature(signature) { + let index = 0 + function next() { + if (index < signature.length) { + const c = signature[index] + ++index + return c + } + return null + } + + function parseOne(c) { + if (!knownTypes[c]) { + throw new Error(`Unknown type: "${c}" in signature "${signature}"`) + } + + let ele + const res = { type: c, child: [] } + switch (c) { + case 'a': // array + ele = next() + checkNotEnd(ele) + res.child.push(parseOne(ele)) + return res + case '{': // dict entry + case '(': // struct + ele = next() + while (ele !== null && ele !== match[c]) { + res.child.push(parseOne(ele)) + ele = next() + } + checkNotEnd(ele) + return res + } + return res + } + + const ret = [] + let c = next() + while (c !== null) { + ret.push(parseOne(c)) + c = next() + } + return ret +} diff --git a/src/lib/dbus/stdifaces.ts b/src/lib/dbus/stdifaces.ts new file mode 100644 index 000000000..8d699295b --- /dev/null +++ b/src/lib/dbus/stdifaces.ts @@ -0,0 +1,205 @@ +// eslint-disable-next-line ts/ban-ts-comment +// @ts-nocheck +import constants from './constants.js' +import parseSignature from './signature.js' + +// TODO: use xmlbuilder + +const xmlHeader + = '' +let stdIfaces + +export default function (msg, bus) { + if (msg.interface === 'org.freedesktop.DBus.Introspectable' && msg.member === 'Introspect') { + if (msg.path === '/') { + msg.path = '' + } + + const resultXml = [xmlHeader] + const nodes = {} + // TODO: this is not very efficient for large number of exported objects + // need to build objects tree as they are exported and walk this tree on introspect request + for (const path in bus.exportedObjects) { + if (path.indexOf(msg.path) === 0) { + // objects path starts with requested + const introspectableObj = bus.exportedObjects[msg.path] + if (introspectableObj) { + nodes[msg.path] = introspectableObj + } else { + if (path[msg.path.length] !== '/') { + continue + } + const localPath = path.slice(msg.path.length) + const pathParts = localPath.split('/') + const localName = pathParts[1] + nodes[localName] = null + } + } + } + + const length = Object.keys(nodes).length + let obj + if (length === 0) { + resultXml.push('') + } else if (length === 1) { + obj = nodes[Object.keys(nodes)[0]] + if (obj) { + resultXml.push('') + for (const ifaceNode in obj) { + resultXml.push(interfaceToXML(obj[ifaceNode][0])) + } + resultXml.push(stdIfaces) + resultXml.push('') + } else { + resultXml.push( + `\n \n `, + ) + } + } else { + resultXml.push('') + for (const name in nodes) { + if (nodes[name] === null) { + resultXml.push(` `) + } else { + obj = nodes[name] + resultXml.push(` `) + for (const ifaceName in obj) { + resultXml.push(interfaceToXML(obj[ifaceName][0])) + } + resultXml.push(stdIfaces) + resultXml.push(' ') + } + } + resultXml.push('') + } + + const introspectableReply = { + type: constants.messageType.methodReturn, + serial: bus.serial++, + replySerial: msg.serial, + destination: msg.sender, + signature: 's', + body: [resultXml.join('\n')], + } + bus.connection.message(introspectableReply) + return 1 + } else if (msg.interface === 'org.freedesktop.DBus.Properties') { + const interfaceName = msg.body[0] + const propertiesObj = bus.exportedObjects[msg.path] + // TODO: !propertiesObj -> UnknownObject http://www.freedesktop.org/wiki/Software/DBusBindingErrors + if (!propertiesObj || !propertiesObj[interfaceName]) { + bus.sendError(msg, 'org.freedesktop.DBus.Error.UnknownMethod', 'Uh oh oh') + return 1 + } + const impl = propertiesObj[interfaceName][1] + + const propertiesReply = { + type: constants.messageType.methodReturn, + serial: bus.serial++, + replySerial: msg.serial, + destination: msg.sender, + } + if (msg.member === 'Get' || msg.member === 'Set') { + const propertyName = msg.body[1] + const propType = propertiesObj[interfaceName][0].properties[propertyName] + if (msg.member === 'Get') { + const propValue = impl[propertyName] + propertiesReply.signature = 'v' + propertiesReply.body = [[propType, propValue]] + } else { + impl[propertyName] = 1234 // TODO: read variant and set property value + } + } else if (msg.member === 'GetAll') { + propertiesReply.signature = 'a{sv}' + const props = [] + for (const p in propertiesObj[interfaceName][0].properties) { + const propertySignature = propertiesObj[interfaceName][0].properties[p] + props.push([p, [propertySignature, impl[p]]]) + } + propertiesReply.body = [props] + } + bus.connection.message(propertiesReply) + return 1 + } else if (msg.interface === 'org.freedesktop.DBus.Peer') { + // TODO: implement bus.replyTo(srcMsg, signature, body) method + const peerReply = { + type: constants.messageType.methodReturn, + serial: bus.serial++, + replySerial: msg.serial, + destination: msg.sender, + } + if (msg.member === 'Ping') { + // empty body + } else if (msg.member === 'GetMachineId') { + peerReply.signature = 's' + peerReply.body = ['This is a machine id. TODO: implement'] + } + bus.connection.message(peerReply) + return 1 + } + return 0 +} + +function interfaceToXML(iface) { + const result = [] + const dumpArgs = function (argsSignature, argsNames, direction) { + if (!argsSignature) { + return + } + const args = parseSignature(argsSignature) + args.forEach((arg, num) => { + const argName = argsNames ? argsNames[num] : direction + num + const dirStr = direction === 'signal' ? '' : `" direction="${direction}` + result.push( + ` `, + ) + }) + } + result.push(` `) + if (iface.methods) { + for (const methodName in iface.methods) { + const method = iface.methods[methodName] + result.push(` `) + dumpArgs(method[0], method[2], 'in') + dumpArgs(method[1], method[3], 'out') + result.push(' ') + } + } + if (iface.signals) { + for (const signalName in iface.signals) { + const signal = iface.signals[signalName] + result.push(` `) + dumpArgs(signal[0], signal.slice(1), 'signal') + result.push(' ') + } + } + if (iface.properties) { + for (const propertyName in iface.properties) { + // TODO: decide how to encode access + result.push( + ` `, + ) + } + } + result.push(' ') + return result.join('\n') +} + +function dumpSignature(s) { + const result = [] + s.forEach((sig) => { + result.push(sig.type + dumpSignature(sig.child)) + if (sig.type === '{') { + result.push('}') + } + if (sig.type === '(') { + result.push(')') + } + }) + return result.join('') +} +stdIfaces + = ' \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' diff --git a/src/lib/definitions/CharacteristicDefinitions.spec.ts b/src/lib/definitions/CharacteristicDefinitions.spec.ts index 0e2080a4c..34178aeb8 100644 --- a/src/lib/definitions/CharacteristicDefinitions.spec.ts +++ b/src/lib/definitions/CharacteristicDefinitions.spec.ts @@ -1,1440 +1,1442 @@ // THIS FILE IS AUTO-GENERATED - DO NOT MODIFY -import "./"; - -import { Characteristic } from "../Characteristic"; - -describe("CharacteristicDefinitions", () => { - describe("AccessCodeControlPoint", () => { - it("should be able to construct", () => { - new Characteristic.AccessCodeControlPoint(); - }); - }); - - describe("AccessCodeSupportedConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.AccessCodeSupportedConfiguration(); - }); - }); - - describe("AccessControlLevel", () => { - it("should be able to construct", () => { - new Characteristic.AccessControlLevel(); - }); - }); - - describe("AccessoryFlags", () => { - it("should be able to construct", () => { - new Characteristic.AccessoryFlags(); - }); - }); - - describe("AccessoryIdentifier", () => { - it("should be able to construct", () => { - new Characteristic.AccessoryIdentifier(); - }); - }); - - describe("Active", () => { - it("should be able to construct", () => { - new Characteristic.Active(); - }); - }); - - describe("ActiveIdentifier", () => { - it("should be able to construct", () => { - new Characteristic.ActiveIdentifier(); - }); - }); - - describe("ActivityInterval", () => { - it("should be able to construct", () => { - new Characteristic.ActivityInterval(); - }); - }); - - describe("AdministratorOnlyAccess", () => { - it("should be able to construct", () => { - new Characteristic.AdministratorOnlyAccess(); - }); - }); - - describe("AirParticulateDensity", () => { - it("should be able to construct", () => { - new Characteristic.AirParticulateDensity(); - }); - }); - - describe("AirParticulateSize", () => { - it("should be able to construct", () => { - new Characteristic.AirParticulateSize(); - }); - }); - - describe("AirPlayEnable", () => { - it("should be able to construct", () => { - new Characteristic.AirPlayEnable(); - }); - }); - - describe("AirQuality", () => { - it("should be able to construct", () => { - new Characteristic.AirQuality(); - }); - }); - - describe("AppMatchingIdentifier", () => { - it("should be able to construct", () => { - new Characteristic.AppMatchingIdentifier(); - }); - }); - - describe("AssetUpdateReadiness", () => { - it("should be able to construct", () => { - new Characteristic.AssetUpdateReadiness(); - }); - }); - - describe("AudioFeedback", () => { - it("should be able to construct", () => { - new Characteristic.AudioFeedback(); - }); - }); - - describe("BatteryLevel", () => { - it("should be able to construct", () => { - new Characteristic.BatteryLevel(); - }); - }); - - describe("Brightness", () => { - it("should be able to construct", () => { - new Characteristic.Brightness(); - }); - }); - - describe("ButtonEvent", () => { - it("should be able to construct", () => { - new Characteristic.ButtonEvent(); - }); - }); - - describe("CameraOperatingModeIndicator", () => { - it("should be able to construct", () => { - new Characteristic.CameraOperatingModeIndicator(); - }); - }); - - describe("CarbonDioxideDetected", () => { - it("should be able to construct", () => { - new Characteristic.CarbonDioxideDetected(); - }); - }); - - describe("CarbonDioxideLevel", () => { - it("should be able to construct", () => { - new Characteristic.CarbonDioxideLevel(); - }); - }); - - describe("CarbonDioxidePeakLevel", () => { - it("should be able to construct", () => { - new Characteristic.CarbonDioxidePeakLevel(); - }); - }); - - describe("CarbonMonoxideDetected", () => { - it("should be able to construct", () => { - new Characteristic.CarbonMonoxideDetected(); - }); - }); - - describe("CarbonMonoxideLevel", () => { - it("should be able to construct", () => { - new Characteristic.CarbonMonoxideLevel(); - }); - }); - - describe("CarbonMonoxidePeakLevel", () => { - it("should be able to construct", () => { - new Characteristic.CarbonMonoxidePeakLevel(); - }); - }); - - describe("CCAEnergyDetectThreshold", () => { - it("should be able to construct", () => { - new Characteristic.CCAEnergyDetectThreshold(); - }); - }); - - describe("CCASignalDetectThreshold", () => { - it("should be able to construct", () => { - new Characteristic.CCASignalDetectThreshold(); - }); - }); - - describe("CharacteristicValueActiveTransitionCount", () => { - it("should be able to construct", () => { - new Characteristic.CharacteristicValueActiveTransitionCount(); - }); - }); - - describe("CharacteristicValueTransitionControl", () => { - it("should be able to construct", () => { - new Characteristic.CharacteristicValueTransitionControl(); - }); - }); - - describe("ChargingState", () => { - it("should be able to construct", () => { - new Characteristic.ChargingState(); - }); - }); - - describe("ClosedCaptions", () => { - it("should be able to construct", () => { - new Characteristic.ClosedCaptions(); - }); - }); - - describe("ColorTemperature", () => { - it("should be able to construct", () => { - new Characteristic.ColorTemperature(); - }); - }); - - describe("ConfigurationState", () => { - it("should be able to construct", () => { - new Characteristic.ConfigurationState(); - }); - }); - - describe("ConfiguredName", () => { - it("should be able to construct", () => { - new Characteristic.ConfiguredName(); - }); - }); - - describe("ContactSensorState", () => { - it("should be able to construct", () => { - new Characteristic.ContactSensorState(); - }); - }); - - describe("CoolingThresholdTemperature", () => { - it("should be able to construct", () => { - new Characteristic.CoolingThresholdTemperature(); - }); - }); - - describe("CryptoHash", () => { - it("should be able to construct", () => { - new Characteristic.CryptoHash(); - }); - }); - - describe("CurrentAirPurifierState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentAirPurifierState(); - }); - }); - - describe("CurrentAmbientLightLevel", () => { - it("should be able to construct", () => { - new Characteristic.CurrentAmbientLightLevel(); - }); - }); - - describe("CurrentDoorState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentDoorState(); - }); - }); - - describe("CurrentFanState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentFanState(); - }); - }); - - describe("CurrentHeaterCoolerState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentHeaterCoolerState(); - }); - }); - - describe("CurrentHeatingCoolingState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentHeatingCoolingState(); - }); - }); - - describe("CurrentHorizontalTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.CurrentHorizontalTiltAngle(); - }); - }); - - describe("CurrentHumidifierDehumidifierState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentHumidifierDehumidifierState(); - }); - }); - - describe("CurrentMediaState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentMediaState(); - }); - }); - - describe("CurrentPosition", () => { - it("should be able to construct", () => { - new Characteristic.CurrentPosition(); - }); - }); - - describe("CurrentRelativeHumidity", () => { - it("should be able to construct", () => { - new Characteristic.CurrentRelativeHumidity(); - }); - }); - - describe("CurrentSlatState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentSlatState(); - }); - }); - - describe("CurrentTemperature", () => { - it("should be able to construct", () => { - new Characteristic.CurrentTemperature(); - }); - }); - - describe("CurrentTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.CurrentTiltAngle(); - }); - }); - - describe("CurrentTransport", () => { - it("should be able to construct", () => { - new Characteristic.CurrentTransport(); - }); - }); - - describe("CurrentVerticalTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.CurrentVerticalTiltAngle(); - }); - }); - - describe("CurrentVisibilityState", () => { - it("should be able to construct", () => { - new Characteristic.CurrentVisibilityState(); - }); - }); - - describe("DataStreamHAPTransport", () => { - it("should be able to construct", () => { - new Characteristic.DataStreamHAPTransport(); - }); - }); - - describe("DataStreamHAPTransportInterrupt", () => { - it("should be able to construct", () => { - new Characteristic.DataStreamHAPTransportInterrupt(); - }); - }); - - describe("DiagonalFieldOfView", () => { - it("should be able to construct", () => { - new Characteristic.DiagonalFieldOfView(); - }); - }); - - describe("DigitalZoom", () => { - it("should be able to construct", () => { - new Characteristic.DigitalZoom(); - }); - }); - - describe("DisplayOrder", () => { - it("should be able to construct", () => { - new Characteristic.DisplayOrder(); - }); - }); - - describe("EventRetransmissionMaximum", () => { - it("should be able to construct", () => { - new Characteristic.EventRetransmissionMaximum(); - }); - }); - - describe("EventSnapshotsActive", () => { - it("should be able to construct", () => { - new Characteristic.EventSnapshotsActive(); - }); - }); - - describe("EventTransmissionCounters", () => { - it("should be able to construct", () => { - new Characteristic.EventTransmissionCounters(); - }); - }); - - describe("FilterChangeIndication", () => { - it("should be able to construct", () => { - new Characteristic.FilterChangeIndication(); - }); - }); - - describe("FilterLifeLevel", () => { - it("should be able to construct", () => { - new Characteristic.FilterLifeLevel(); - }); - }); - - describe("FirmwareRevision", () => { - it("should be able to construct", () => { - new Characteristic.FirmwareRevision(); - }); - }); - - describe("FirmwareUpdateReadiness", () => { - it("should be able to construct", () => { - new Characteristic.FirmwareUpdateReadiness(); - }); - }); - - describe("FirmwareUpdateStatus", () => { - it("should be able to construct", () => { - new Characteristic.FirmwareUpdateStatus(); - }); - }); - - describe("HardwareFinish", () => { - it("should be able to construct", () => { - new Characteristic.HardwareFinish(); - }); - }); - - describe("HardwareRevision", () => { - it("should be able to construct", () => { - new Characteristic.HardwareRevision(); - }); - }); - - describe("HeartBeat", () => { - it("should be able to construct", () => { - new Characteristic.HeartBeat(); - }); - }); - - describe("HeatingThresholdTemperature", () => { - it("should be able to construct", () => { - new Characteristic.HeatingThresholdTemperature(); - }); - }); - - describe("HoldPosition", () => { - it("should be able to construct", () => { - new Characteristic.HoldPosition(); - }); - }); - - describe("HomeKitCameraActive", () => { - it("should be able to construct", () => { - new Characteristic.HomeKitCameraActive(); - }); - }); - - describe("Hue", () => { - it("should be able to construct", () => { - new Characteristic.Hue(); - }); - }); - - describe("Identifier", () => { - it("should be able to construct", () => { - new Characteristic.Identifier(); - }); - }); - - describe("Identify", () => { - it("should be able to construct", () => { - new Characteristic.Identify(); - }); - }); - - describe("ImageMirroring", () => { - it("should be able to construct", () => { - new Characteristic.ImageMirroring(); - }); - }); - - describe("ImageRotation", () => { - it("should be able to construct", () => { - new Characteristic.ImageRotation(); - }); - }); - - describe("InputDeviceType", () => { - it("should be able to construct", () => { - new Characteristic.InputDeviceType(); - }); - }); - - describe("InputSourceType", () => { - it("should be able to construct", () => { - new Characteristic.InputSourceType(); - }); - }); - - describe("InUse", () => { - it("should be able to construct", () => { - new Characteristic.InUse(); - }); - }); - - describe("IsConfigured", () => { - it("should be able to construct", () => { - new Characteristic.IsConfigured(); - }); - }); - - describe("LeakDetected", () => { - it("should be able to construct", () => { - new Characteristic.LeakDetected(); - }); - }); - - describe("ListPairings", () => { - it("should be able to construct", () => { - new Characteristic.ListPairings(); - }); - }); - - describe("LockControlPoint", () => { - it("should be able to construct", () => { - new Characteristic.LockControlPoint(); - }); - }); - - describe("LockCurrentState", () => { - it("should be able to construct", () => { - new Characteristic.LockCurrentState(); - }); - }); - - describe("LockLastKnownAction", () => { - it("should be able to construct", () => { - new Characteristic.LockLastKnownAction(); - }); - }); - - describe("LockManagementAutoSecurityTimeout", () => { - it("should be able to construct", () => { - new Characteristic.LockManagementAutoSecurityTimeout(); - }); - }); - - describe("LockPhysicalControls", () => { - it("should be able to construct", () => { - new Characteristic.LockPhysicalControls(); - }); - }); - - describe("LockTargetState", () => { - it("should be able to construct", () => { - new Characteristic.LockTargetState(); - }); - }); - - describe("Logs", () => { - it("should be able to construct", () => { - new Characteristic.Logs(); - }); - }); - - describe("MACRetransmissionMaximum", () => { - it("should be able to construct", () => { - new Characteristic.MACRetransmissionMaximum(); - }); - }); - - describe("MACTransmissionCounters", () => { - it("should be able to construct", () => { - new Characteristic.MACTransmissionCounters(); - }); - }); - - describe("ManagedNetworkEnable", () => { - it("should be able to construct", () => { - new Characteristic.ManagedNetworkEnable(); - }); - }); - - describe("ManuallyDisabled", () => { - it("should be able to construct", () => { - new Characteristic.ManuallyDisabled(); - }); - }); - - describe("Manufacturer", () => { - it("should be able to construct", () => { - new Characteristic.Manufacturer(); - }); - }); - - describe("MatterFirmwareRevisionNumber", () => { - it("should be able to construct", () => { - new Characteristic.MatterFirmwareRevisionNumber(); - }); - }); - - describe("MatterFirmwareUpdateStatus", () => { - it("should be able to construct", () => { - new Characteristic.MatterFirmwareUpdateStatus(); - }); - }); - - describe("MaximumTransmitPower", () => { - it("should be able to construct", () => { - new Characteristic.MaximumTransmitPower(); - }); - }); - - describe("MetricsBufferFullState", () => { - it("should be able to construct", () => { - new Characteristic.MetricsBufferFullState(); - }); - }); - - describe("Model", () => { - it("should be able to construct", () => { - new Characteristic.Model(); - }); - }); - - describe("MotionDetected", () => { - it("should be able to construct", () => { - new Characteristic.MotionDetected(); - }); - }); - - describe("MultifunctionButton", () => { - it("should be able to construct", () => { - new Characteristic.MultifunctionButton(); - }); - }); - - describe("Mute", () => { - it("should be able to construct", () => { - new Characteristic.Mute(); - }); - }); - - describe("Name", () => { - it("should be able to construct", () => { - new Characteristic.Name(); - }); - }); - - describe("NetworkAccessViolationControl", () => { - it("should be able to construct", () => { - new Characteristic.NetworkAccessViolationControl(); - }); - }); - - describe("NetworkClientProfileControl", () => { - it("should be able to construct", () => { - new Characteristic.NetworkClientProfileControl(); - }); - }); - - describe("NetworkClientStatusControl", () => { - it("should be able to construct", () => { - new Characteristic.NetworkClientStatusControl(); - }); - }); - - describe("NFCAccessControlPoint", () => { - it("should be able to construct", () => { - new Characteristic.NFCAccessControlPoint(); - }); - }); - - describe("NFCAccessSupportedConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.NFCAccessSupportedConfiguration(); - }); - }); - - describe("NightVision", () => { - it("should be able to construct", () => { - new Characteristic.NightVision(); - }); - }); - - describe("NitrogenDioxideDensity", () => { - it("should be able to construct", () => { - new Characteristic.NitrogenDioxideDensity(); - }); - }); - - describe("ObstructionDetected", () => { - it("should be able to construct", () => { - new Characteristic.ObstructionDetected(); - }); - }); - - describe("OccupancyDetected", () => { - it("should be able to construct", () => { - new Characteristic.OccupancyDetected(); - }); - }); - - describe("On", () => { - it("should be able to construct", () => { - new Characteristic.On(); - }); - }); - - describe("OperatingStateResponse", () => { - it("should be able to construct", () => { - new Characteristic.OperatingStateResponse(); - }); - }); - - describe("OpticalZoom", () => { - it("should be able to construct", () => { - new Characteristic.OpticalZoom(); - }); - }); - - describe("OutletInUse", () => { - it("should be able to construct", () => { - new Characteristic.OutletInUse(); - }); - }); - - describe("OzoneDensity", () => { - it("should be able to construct", () => { - new Characteristic.OzoneDensity(); - }); - }); - - describe("PairingFeatures", () => { - it("should be able to construct", () => { - new Characteristic.PairingFeatures(); - }); - }); - - describe("PairSetup", () => { - it("should be able to construct", () => { - new Characteristic.PairSetup(); - }); - }); - - describe("PairVerify", () => { - it("should be able to construct", () => { - new Characteristic.PairVerify(); - }); - }); - - describe("PasswordSetting", () => { - it("should be able to construct", () => { - new Characteristic.PasswordSetting(); - }); - }); - - describe("PeriodicSnapshotsActive", () => { - it("should be able to construct", () => { - new Characteristic.PeriodicSnapshotsActive(); - }); - }); - - describe("PictureMode", () => { - it("should be able to construct", () => { - new Characteristic.PictureMode(); - }); - }); - - describe("Ping", () => { - it("should be able to construct", () => { - new Characteristic.Ping(); - }); - }); - - describe("PM10Density", () => { - it("should be able to construct", () => { - new Characteristic.PM10Density(); - }); - }); - - describe("PM2_5Density", () => { - it("should be able to construct", () => { - new Characteristic.PM2_5Density(); - }); - }); - - describe("PositionState", () => { - it("should be able to construct", () => { - new Characteristic.PositionState(); - }); - }); - - describe("PowerModeSelection", () => { - it("should be able to construct", () => { - new Characteristic.PowerModeSelection(); - }); - }); - - describe("ProductData", () => { - it("should be able to construct", () => { - new Characteristic.ProductData(); - }); - }); - - describe("ProgrammableSwitchEvent", () => { - it("should be able to construct", () => { - new Characteristic.ProgrammableSwitchEvent(); - }); - }); - - describe("ProgrammableSwitchOutputState", () => { - it("should be able to construct", () => { - new Characteristic.ProgrammableSwitchOutputState(); - }); - }); - - describe("ProgramMode", () => { - it("should be able to construct", () => { - new Characteristic.ProgramMode(); - }); - }); - - describe("ReceivedSignalStrengthIndication", () => { - it("should be able to construct", () => { - new Characteristic.ReceivedSignalStrengthIndication(); - }); - }); - - describe("ReceiverSensitivity", () => { - it("should be able to construct", () => { - new Characteristic.ReceiverSensitivity(); - }); - }); - - describe("RecordingAudioActive", () => { - it("should be able to construct", () => { - new Characteristic.RecordingAudioActive(); - }); - }); - - describe("RelativeHumidityDehumidifierThreshold", () => { - it("should be able to construct", () => { - new Characteristic.RelativeHumidityDehumidifierThreshold(); - }); - }); - - describe("RelativeHumidityHumidifierThreshold", () => { - it("should be able to construct", () => { - new Characteristic.RelativeHumidityHumidifierThreshold(); - }); - }); - - describe("RemainingDuration", () => { - it("should be able to construct", () => { - new Characteristic.RemainingDuration(); - }); - }); - - describe("RemoteKey", () => { - it("should be able to construct", () => { - new Characteristic.RemoteKey(); - }); - }); - - describe("ResetFilterIndication", () => { - it("should be able to construct", () => { - new Characteristic.ResetFilterIndication(); - }); - }); - - describe("RotationDirection", () => { - it("should be able to construct", () => { - new Characteristic.RotationDirection(); - }); - }); - - describe("RotationSpeed", () => { - it("should be able to construct", () => { - new Characteristic.RotationSpeed(); - }); - }); - - describe("RouterStatus", () => { - it("should be able to construct", () => { - new Characteristic.RouterStatus(); - }); - }); - - describe("Saturation", () => { - it("should be able to construct", () => { - new Characteristic.Saturation(); - }); - }); - - describe("SecuritySystemAlarmType", () => { - it("should be able to construct", () => { - new Characteristic.SecuritySystemAlarmType(); - }); - }); - - describe("SecuritySystemCurrentState", () => { - it("should be able to construct", () => { - new Characteristic.SecuritySystemCurrentState(); - }); - }); - - describe("SecuritySystemTargetState", () => { - it("should be able to construct", () => { - new Characteristic.SecuritySystemTargetState(); - }); - }); - - describe("SelectedAudioStreamConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SelectedAudioStreamConfiguration(); - }); - }); - - describe("SelectedCameraRecordingConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SelectedCameraRecordingConfiguration(); - }); - }); - - describe("SelectedDiagnosticsModes", () => { - it("should be able to construct", () => { - new Characteristic.SelectedDiagnosticsModes(); - }); - }); - - describe("SelectedRTPStreamConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SelectedRTPStreamConfiguration(); - }); - }); - - describe("SelectedSleepConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SelectedSleepConfiguration(); - }); - }); - - describe("SerialNumber", () => { - it("should be able to construct", () => { - new Characteristic.SerialNumber(); - }); - }); - - describe("ServiceLabelIndex", () => { - it("should be able to construct", () => { - new Characteristic.ServiceLabelIndex(); - }); - }); - - describe("ServiceLabelNamespace", () => { - it("should be able to construct", () => { - new Characteristic.ServiceLabelNamespace(); - }); - }); - - describe("SetDuration", () => { - it("should be able to construct", () => { - new Characteristic.SetDuration(); - }); - }); - - describe("SetupDataStreamTransport", () => { - it("should be able to construct", () => { - new Characteristic.SetupDataStreamTransport(); - }); - }); - - describe("SetupEndpoints", () => { - it("should be able to construct", () => { - new Characteristic.SetupEndpoints(); - }); - }); - - describe("SetupTransferTransport", () => { - it("should be able to construct", () => { - new Characteristic.SetupTransferTransport(); - }); - }); - - describe("SignalToNoiseRatio", () => { - it("should be able to construct", () => { - new Characteristic.SignalToNoiseRatio(); - }); - }); - - describe("SiriEnable", () => { - it("should be able to construct", () => { - new Characteristic.SiriEnable(); - }); - }); - - describe("SiriEndpointSessionStatus", () => { - it("should be able to construct", () => { - new Characteristic.SiriEndpointSessionStatus(); - }); - }); - - describe("SiriEngineVersion", () => { - it("should be able to construct", () => { - new Characteristic.SiriEngineVersion(); - }); - }); - - describe("SiriInputType", () => { - it("should be able to construct", () => { - new Characteristic.SiriInputType(); - }); - }); - - describe("SiriLightOnUse", () => { - it("should be able to construct", () => { - new Characteristic.SiriLightOnUse(); - }); - }); - - describe("SiriListening", () => { - it("should be able to construct", () => { - new Characteristic.SiriListening(); - }); - }); - - describe("SiriTouchToUse", () => { - it("should be able to construct", () => { - new Characteristic.SiriTouchToUse(); - }); - }); - - describe("SlatType", () => { - it("should be able to construct", () => { - new Characteristic.SlatType(); - }); - }); - - describe("SleepDiscoveryMode", () => { - it("should be able to construct", () => { - new Characteristic.SleepDiscoveryMode(); - }); - }); - - describe("SleepInterval", () => { - it("should be able to construct", () => { - new Characteristic.SleepInterval(); - }); - }); - - describe("SmokeDetected", () => { - it("should be able to construct", () => { - new Characteristic.SmokeDetected(); - }); - }); - - describe("SoftwareRevision", () => { - it("should be able to construct", () => { - new Characteristic.SoftwareRevision(); - }); - }); - - describe("StagedFirmwareVersion", () => { - it("should be able to construct", () => { - new Characteristic.StagedFirmwareVersion(); - }); - }); - - describe("StatusActive", () => { - it("should be able to construct", () => { - new Characteristic.StatusActive(); - }); - }); - - describe("StatusFault", () => { - it("should be able to construct", () => { - new Characteristic.StatusFault(); - }); - }); - - describe("StatusJammed", () => { - it("should be able to construct", () => { - new Characteristic.StatusJammed(); - }); - }); - - describe("StatusLowBattery", () => { - it("should be able to construct", () => { - new Characteristic.StatusLowBattery(); - }); - }); - - describe("StatusTampered", () => { - it("should be able to construct", () => { - new Characteristic.StatusTampered(); - }); - }); - - describe("StreamingStatus", () => { - it("should be able to construct", () => { - new Characteristic.StreamingStatus(); - }); - }); - - describe("SulphurDioxideDensity", () => { - it("should be able to construct", () => { - new Characteristic.SulphurDioxideDensity(); - }); - }); - - describe("SupportedAssetTypes", () => { - it("should be able to construct", () => { - new Characteristic.SupportedAssetTypes(); - }); - }); - - describe("SupportedAudioRecordingConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedAudioRecordingConfiguration(); - }); - }); - - describe("SupportedAudioStreamConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedAudioStreamConfiguration(); - }); - }); - - describe("SupportedCameraRecordingConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedCameraRecordingConfiguration(); - }); - }); - - describe("SupportedCharacteristicValueTransitionConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedCharacteristicValueTransitionConfiguration(); - }); - }); - - describe("SupportedDataStreamTransportConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedDataStreamTransportConfiguration(); - }); - }); - - describe("SupportedDiagnosticsModes", () => { - it("should be able to construct", () => { - new Characteristic.SupportedDiagnosticsModes(); - }); - }); - - describe("SupportedDiagnosticsSnapshot", () => { - it("should be able to construct", () => { - new Characteristic.SupportedDiagnosticsSnapshot(); - }); - }); - - describe("SupportedFirmwareUpdateConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedFirmwareUpdateConfiguration(); - }); - }); - - describe("SupportedMetrics", () => { - it("should be able to construct", () => { - new Characteristic.SupportedMetrics(); - }); - }); - - describe("SupportedRouterConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedRouterConfiguration(); - }); - }); - - describe("SupportedRTPConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedRTPConfiguration(); - }); - }); - - describe("SupportedSleepConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedSleepConfiguration(); - }); - }); - - describe("SupportedTransferTransportConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedTransferTransportConfiguration(); - }); - }); - - describe("SupportedVideoRecordingConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedVideoRecordingConfiguration(); - }); - }); - - describe("SupportedVideoStreamConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.SupportedVideoStreamConfiguration(); - }); - }); - - describe("SwingMode", () => { - it("should be able to construct", () => { - new Characteristic.SwingMode(); - }); - }); - - describe("TapType", () => { - it("should be able to construct", () => { - new Characteristic.TapType(); - }); - }); - - describe("TargetAirPurifierState", () => { - it("should be able to construct", () => { - new Characteristic.TargetAirPurifierState(); - }); - }); - - describe("TargetControlList", () => { - it("should be able to construct", () => { - new Characteristic.TargetControlList(); - }); - }); - - describe("TargetControlSupportedConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.TargetControlSupportedConfiguration(); - }); - }); - - describe("TargetDoorState", () => { - it("should be able to construct", () => { - new Characteristic.TargetDoorState(); - }); - }); - - describe("TargetFanState", () => { - it("should be able to construct", () => { - new Characteristic.TargetFanState(); - }); - }); - - describe("TargetHeaterCoolerState", () => { - it("should be able to construct", () => { - new Characteristic.TargetHeaterCoolerState(); - }); - }); - - describe("TargetHeatingCoolingState", () => { - it("should be able to construct", () => { - new Characteristic.TargetHeatingCoolingState(); - }); - }); - - describe("TargetHorizontalTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.TargetHorizontalTiltAngle(); - }); - }); - - describe("TargetHumidifierDehumidifierState", () => { - it("should be able to construct", () => { - new Characteristic.TargetHumidifierDehumidifierState(); - }); - }); - - describe("TargetMediaState", () => { - it("should be able to construct", () => { - new Characteristic.TargetMediaState(); - }); - }); - - describe("TargetPosition", () => { - it("should be able to construct", () => { - new Characteristic.TargetPosition(); - }); - }); - - describe("TargetRelativeHumidity", () => { - it("should be able to construct", () => { - new Characteristic.TargetRelativeHumidity(); - }); - }); - - describe("TargetTemperature", () => { - it("should be able to construct", () => { - new Characteristic.TargetTemperature(); - }); - }); - - describe("TargetTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.TargetTiltAngle(); - }); - }); - - describe("TargetVerticalTiltAngle", () => { - it("should be able to construct", () => { - new Characteristic.TargetVerticalTiltAngle(); - }); - }); - - describe("TargetVisibilityState", () => { - it("should be able to construct", () => { - new Characteristic.TargetVisibilityState(); - }); - }); - - describe("TemperatureDisplayUnits", () => { - it("should be able to construct", () => { - new Characteristic.TemperatureDisplayUnits(); - }); - }); - - describe("ThirdPartyCameraActive", () => { - it("should be able to construct", () => { - new Characteristic.ThirdPartyCameraActive(); - }); - }); - - describe("ThreadControlPoint", () => { - it("should be able to construct", () => { - new Characteristic.ThreadControlPoint(); - }); - }); - - describe("ThreadNodeCapabilities", () => { - it("should be able to construct", () => { - new Characteristic.ThreadNodeCapabilities(); - }); - }); - - describe("ThreadOpenThreadVersion", () => { - it("should be able to construct", () => { - new Characteristic.ThreadOpenThreadVersion(); - }); - }); - - describe("ThreadStatus", () => { - it("should be able to construct", () => { - new Characteristic.ThreadStatus(); - }); - }); - - describe("Token", () => { - it("should be able to construct", () => { - new Characteristic.Token(); - }); - }); - - describe("TransmitPower", () => { - it("should be able to construct", () => { - new Characteristic.TransmitPower(); - }); - }); - - describe("ValveType", () => { - it("should be able to construct", () => { - new Characteristic.ValveType(); - }); - }); - - describe("Version", () => { - it("should be able to construct", () => { - new Characteristic.Version(); - }); - }); - - describe("VideoAnalysisActive", () => { - it("should be able to construct", () => { - new Characteristic.VideoAnalysisActive(); - }); - }); - - describe("VOCDensity", () => { - it("should be able to construct", () => { - new Characteristic.VOCDensity(); - }); - }); - - describe("Volume", () => { - it("should be able to construct", () => { - new Characteristic.Volume(); - }); - }); - - describe("VolumeControlType", () => { - it("should be able to construct", () => { - new Characteristic.VolumeControlType(); - }); - }); - - describe("VolumeSelector", () => { - it("should be able to construct", () => { - new Characteristic.VolumeSelector(); - }); - }); - - describe("WakeConfiguration", () => { - it("should be able to construct", () => { - new Characteristic.WakeConfiguration(); - }); - }); - - describe("WANConfigurationList", () => { - it("should be able to construct", () => { - new Characteristic.WANConfigurationList(); - }); - }); - - describe("WANStatusList", () => { - it("should be able to construct", () => { - new Characteristic.WANStatusList(); - }); - }); - - describe("WaterLevel", () => { - it("should be able to construct", () => { - new Characteristic.WaterLevel(); - }); - }); - - describe("WiFiCapabilities", () => { - it("should be able to construct", () => { - new Characteristic.WiFiCapabilities(); - }); - }); - - describe("WiFiConfigurationControl", () => { - it("should be able to construct", () => { - new Characteristic.WiFiConfigurationControl(); - }); - }); - - describe("WiFiSatelliteStatus", () => { - it("should be able to construct", () => { - new Characteristic.WiFiSatelliteStatus(); - }); - }); -}); +/* eslint-disable no-new */ +import { describe, it } from 'vitest' + +import { Characteristic } from '../Characteristic.js' +import './index.js' + +describe('characteristicDefinitions', () => { + describe('accessCodeControlPoint', () => { + it('should be able to construct', () => { + new Characteristic.AccessCodeControlPoint() + }) + }) + + describe('accessCodeSupportedConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.AccessCodeSupportedConfiguration() + }) + }) + + describe('accessControlLevel', () => { + it('should be able to construct', () => { + new Characteristic.AccessControlLevel() + }) + }) + + describe('accessoryFlags', () => { + it('should be able to construct', () => { + new Characteristic.AccessoryFlags() + }) + }) + + describe('accessoryIdentifier', () => { + it('should be able to construct', () => { + new Characteristic.AccessoryIdentifier() + }) + }) + + describe('active', () => { + it('should be able to construct', () => { + new Characteristic.Active() + }) + }) + + describe('activeIdentifier', () => { + it('should be able to construct', () => { + new Characteristic.ActiveIdentifier() + }) + }) + + describe('activityInterval', () => { + it('should be able to construct', () => { + new Characteristic.ActivityInterval() + }) + }) + + describe('administratorOnlyAccess', () => { + it('should be able to construct', () => { + new Characteristic.AdministratorOnlyAccess() + }) + }) + + describe('airParticulateDensity', () => { + it('should be able to construct', () => { + new Characteristic.AirParticulateDensity() + }) + }) + + describe('airParticulateSize', () => { + it('should be able to construct', () => { + new Characteristic.AirParticulateSize() + }) + }) + + describe('airPlayEnable', () => { + it('should be able to construct', () => { + new Characteristic.AirPlayEnable() + }) + }) + + describe('airQuality', () => { + it('should be able to construct', () => { + new Characteristic.AirQuality() + }) + }) + + describe('appMatchingIdentifier', () => { + it('should be able to construct', () => { + new Characteristic.AppMatchingIdentifier() + }) + }) + + describe('assetUpdateReadiness', () => { + it('should be able to construct', () => { + new Characteristic.AssetUpdateReadiness() + }) + }) + + describe('audioFeedback', () => { + it('should be able to construct', () => { + new Characteristic.AudioFeedback() + }) + }) + + describe('batteryLevel', () => { + it('should be able to construct', () => { + new Characteristic.BatteryLevel() + }) + }) + + describe('brightness', () => { + it('should be able to construct', () => { + new Characteristic.Brightness() + }) + }) + + describe('buttonEvent', () => { + it('should be able to construct', () => { + new Characteristic.ButtonEvent() + }) + }) + + describe('cameraOperatingModeIndicator', () => { + it('should be able to construct', () => { + new Characteristic.CameraOperatingModeIndicator() + }) + }) + + describe('carbonDioxideDetected', () => { + it('should be able to construct', () => { + new Characteristic.CarbonDioxideDetected() + }) + }) + + describe('carbonDioxideLevel', () => { + it('should be able to construct', () => { + new Characteristic.CarbonDioxideLevel() + }) + }) + + describe('carbonDioxidePeakLevel', () => { + it('should be able to construct', () => { + new Characteristic.CarbonDioxidePeakLevel() + }) + }) + + describe('carbonMonoxideDetected', () => { + it('should be able to construct', () => { + new Characteristic.CarbonMonoxideDetected() + }) + }) + + describe('carbonMonoxideLevel', () => { + it('should be able to construct', () => { + new Characteristic.CarbonMonoxideLevel() + }) + }) + + describe('carbonMonoxidePeakLevel', () => { + it('should be able to construct', () => { + new Characteristic.CarbonMonoxidePeakLevel() + }) + }) + + describe('cCAEnergyDetectThreshold', () => { + it('should be able to construct', () => { + new Characteristic.CCAEnergyDetectThreshold() + }) + }) + + describe('cCASignalDetectThreshold', () => { + it('should be able to construct', () => { + new Characteristic.CCASignalDetectThreshold() + }) + }) + + describe('characteristicValueActiveTransitionCount', () => { + it('should be able to construct', () => { + new Characteristic.CharacteristicValueActiveTransitionCount() + }) + }) + + describe('characteristicValueTransitionControl', () => { + it('should be able to construct', () => { + new Characteristic.CharacteristicValueTransitionControl() + }) + }) + + describe('chargingState', () => { + it('should be able to construct', () => { + new Characteristic.ChargingState() + }) + }) + + describe('closedCaptions', () => { + it('should be able to construct', () => { + new Characteristic.ClosedCaptions() + }) + }) + + describe('colorTemperature', () => { + it('should be able to construct', () => { + new Characteristic.ColorTemperature() + }) + }) + + describe('configurationState', () => { + it('should be able to construct', () => { + new Characteristic.ConfigurationState() + }) + }) + + describe('configuredName', () => { + it('should be able to construct', () => { + new Characteristic.ConfiguredName() + }) + }) + + describe('contactSensorState', () => { + it('should be able to construct', () => { + new Characteristic.ContactSensorState() + }) + }) + + describe('coolingThresholdTemperature', () => { + it('should be able to construct', () => { + new Characteristic.CoolingThresholdTemperature() + }) + }) + + describe('cryptoHash', () => { + it('should be able to construct', () => { + new Characteristic.CryptoHash() + }) + }) + + describe('currentAirPurifierState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentAirPurifierState() + }) + }) + + describe('currentAmbientLightLevel', () => { + it('should be able to construct', () => { + new Characteristic.CurrentAmbientLightLevel() + }) + }) + + describe('currentDoorState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentDoorState() + }) + }) + + describe('currentFanState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentFanState() + }) + }) + + describe('currentHeaterCoolerState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentHeaterCoolerState() + }) + }) + + describe('currentHeatingCoolingState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentHeatingCoolingState() + }) + }) + + describe('currentHorizontalTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.CurrentHorizontalTiltAngle() + }) + }) + + describe('currentHumidifierDehumidifierState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentHumidifierDehumidifierState() + }) + }) + + describe('currentMediaState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentMediaState() + }) + }) + + describe('currentPosition', () => { + it('should be able to construct', () => { + new Characteristic.CurrentPosition() + }) + }) + + describe('currentRelativeHumidity', () => { + it('should be able to construct', () => { + new Characteristic.CurrentRelativeHumidity() + }) + }) + + describe('currentSlatState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentSlatState() + }) + }) + + describe('currentTemperature', () => { + it('should be able to construct', () => { + new Characteristic.CurrentTemperature() + }) + }) + + describe('currentTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.CurrentTiltAngle() + }) + }) + + describe('currentTransport', () => { + it('should be able to construct', () => { + new Characteristic.CurrentTransport() + }) + }) + + describe('currentVerticalTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.CurrentVerticalTiltAngle() + }) + }) + + describe('currentVisibilityState', () => { + it('should be able to construct', () => { + new Characteristic.CurrentVisibilityState() + }) + }) + + describe('dataStreamHAPTransport', () => { + it('should be able to construct', () => { + new Characteristic.DataStreamHAPTransport() + }) + }) + + describe('dataStreamHAPTransportInterrupt', () => { + it('should be able to construct', () => { + new Characteristic.DataStreamHAPTransportInterrupt() + }) + }) + + describe('diagonalFieldOfView', () => { + it('should be able to construct', () => { + new Characteristic.DiagonalFieldOfView() + }) + }) + + describe('digitalZoom', () => { + it('should be able to construct', () => { + new Characteristic.DigitalZoom() + }) + }) + + describe('displayOrder', () => { + it('should be able to construct', () => { + new Characteristic.DisplayOrder() + }) + }) + + describe('eventRetransmissionMaximum', () => { + it('should be able to construct', () => { + new Characteristic.EventRetransmissionMaximum() + }) + }) + + describe('eventSnapshotsActive', () => { + it('should be able to construct', () => { + new Characteristic.EventSnapshotsActive() + }) + }) + + describe('eventTransmissionCounters', () => { + it('should be able to construct', () => { + new Characteristic.EventTransmissionCounters() + }) + }) + + describe('filterChangeIndication', () => { + it('should be able to construct', () => { + new Characteristic.FilterChangeIndication() + }) + }) + + describe('filterLifeLevel', () => { + it('should be able to construct', () => { + new Characteristic.FilterLifeLevel() + }) + }) + + describe('firmwareRevision', () => { + it('should be able to construct', () => { + new Characteristic.FirmwareRevision() + }) + }) + + describe('firmwareUpdateReadiness', () => { + it('should be able to construct', () => { + new Characteristic.FirmwareUpdateReadiness() + }) + }) + + describe('firmwareUpdateStatus', () => { + it('should be able to construct', () => { + new Characteristic.FirmwareUpdateStatus() + }) + }) + + describe('hardwareFinish', () => { + it('should be able to construct', () => { + new Characteristic.HardwareFinish() + }) + }) + + describe('hardwareRevision', () => { + it('should be able to construct', () => { + new Characteristic.HardwareRevision() + }) + }) + + describe('heartBeat', () => { + it('should be able to construct', () => { + new Characteristic.HeartBeat() + }) + }) + + describe('heatingThresholdTemperature', () => { + it('should be able to construct', () => { + new Characteristic.HeatingThresholdTemperature() + }) + }) + + describe('holdPosition', () => { + it('should be able to construct', () => { + new Characteristic.HoldPosition() + }) + }) + + describe('homeKitCameraActive', () => { + it('should be able to construct', () => { + new Characteristic.HomeKitCameraActive() + }) + }) + + describe('hue', () => { + it('should be able to construct', () => { + new Characteristic.Hue() + }) + }) + + describe('identifier', () => { + it('should be able to construct', () => { + new Characteristic.Identifier() + }) + }) + + describe('identify', () => { + it('should be able to construct', () => { + new Characteristic.Identify() + }) + }) + + describe('imageMirroring', () => { + it('should be able to construct', () => { + new Characteristic.ImageMirroring() + }) + }) + + describe('imageRotation', () => { + it('should be able to construct', () => { + new Characteristic.ImageRotation() + }) + }) + + describe('inputDeviceType', () => { + it('should be able to construct', () => { + new Characteristic.InputDeviceType() + }) + }) + + describe('inputSourceType', () => { + it('should be able to construct', () => { + new Characteristic.InputSourceType() + }) + }) + + describe('inUse', () => { + it('should be able to construct', () => { + new Characteristic.InUse() + }) + }) + + describe('isConfigured', () => { + it('should be able to construct', () => { + new Characteristic.IsConfigured() + }) + }) + + describe('leakDetected', () => { + it('should be able to construct', () => { + new Characteristic.LeakDetected() + }) + }) + + describe('listPairings', () => { + it('should be able to construct', () => { + new Characteristic.ListPairings() + }) + }) + + describe('lockControlPoint', () => { + it('should be able to construct', () => { + new Characteristic.LockControlPoint() + }) + }) + + describe('lockCurrentState', () => { + it('should be able to construct', () => { + new Characteristic.LockCurrentState() + }) + }) + + describe('lockLastKnownAction', () => { + it('should be able to construct', () => { + new Characteristic.LockLastKnownAction() + }) + }) + + describe('lockManagementAutoSecurityTimeout', () => { + it('should be able to construct', () => { + new Characteristic.LockManagementAutoSecurityTimeout() + }) + }) + + describe('lockPhysicalControls', () => { + it('should be able to construct', () => { + new Characteristic.LockPhysicalControls() + }) + }) + + describe('lockTargetState', () => { + it('should be able to construct', () => { + new Characteristic.LockTargetState() + }) + }) + + describe('logs', () => { + it('should be able to construct', () => { + new Characteristic.Logs() + }) + }) + + describe('mACRetransmissionMaximum', () => { + it('should be able to construct', () => { + new Characteristic.MACRetransmissionMaximum() + }) + }) + + describe('mACTransmissionCounters', () => { + it('should be able to construct', () => { + new Characteristic.MACTransmissionCounters() + }) + }) + + describe('managedNetworkEnable', () => { + it('should be able to construct', () => { + new Characteristic.ManagedNetworkEnable() + }) + }) + + describe('manuallyDisabled', () => { + it('should be able to construct', () => { + new Characteristic.ManuallyDisabled() + }) + }) + + describe('manufacturer', () => { + it('should be able to construct', () => { + new Characteristic.Manufacturer() + }) + }) + + describe('matterFirmwareRevisionNumber', () => { + it('should be able to construct', () => { + new Characteristic.MatterFirmwareRevisionNumber() + }) + }) + + describe('matterFirmwareUpdateStatus', () => { + it('should be able to construct', () => { + new Characteristic.MatterFirmwareUpdateStatus() + }) + }) + + describe('maximumTransmitPower', () => { + it('should be able to construct', () => { + new Characteristic.MaximumTransmitPower() + }) + }) + + describe('metricsBufferFullState', () => { + it('should be able to construct', () => { + new Characteristic.MetricsBufferFullState() + }) + }) + + describe('model', () => { + it('should be able to construct', () => { + new Characteristic.Model() + }) + }) + + describe('motionDetected', () => { + it('should be able to construct', () => { + new Characteristic.MotionDetected() + }) + }) + + describe('multifunctionButton', () => { + it('should be able to construct', () => { + new Characteristic.MultifunctionButton() + }) + }) + + describe('mute', () => { + it('should be able to construct', () => { + new Characteristic.Mute() + }) + }) + + describe('name', () => { + it('should be able to construct', () => { + new Characteristic.Name() + }) + }) + + describe('networkAccessViolationControl', () => { + it('should be able to construct', () => { + new Characteristic.NetworkAccessViolationControl() + }) + }) + + describe('networkClientProfileControl', () => { + it('should be able to construct', () => { + new Characteristic.NetworkClientProfileControl() + }) + }) + + describe('networkClientStatusControl', () => { + it('should be able to construct', () => { + new Characteristic.NetworkClientStatusControl() + }) + }) + + describe('nFCAccessControlPoint', () => { + it('should be able to construct', () => { + new Characteristic.NFCAccessControlPoint() + }) + }) + + describe('nFCAccessSupportedConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.NFCAccessSupportedConfiguration() + }) + }) + + describe('nightVision', () => { + it('should be able to construct', () => { + new Characteristic.NightVision() + }) + }) + + describe('nitrogenDioxideDensity', () => { + it('should be able to construct', () => { + new Characteristic.NitrogenDioxideDensity() + }) + }) + + describe('obstructionDetected', () => { + it('should be able to construct', () => { + new Characteristic.ObstructionDetected() + }) + }) + + describe('occupancyDetected', () => { + it('should be able to construct', () => { + new Characteristic.OccupancyDetected() + }) + }) + + describe('on', () => { + it('should be able to construct', () => { + new Characteristic.On() + }) + }) + + describe('operatingStateResponse', () => { + it('should be able to construct', () => { + new Characteristic.OperatingStateResponse() + }) + }) + + describe('opticalZoom', () => { + it('should be able to construct', () => { + new Characteristic.OpticalZoom() + }) + }) + + describe('outletInUse', () => { + it('should be able to construct', () => { + new Characteristic.OutletInUse() + }) + }) + + describe('ozoneDensity', () => { + it('should be able to construct', () => { + new Characteristic.OzoneDensity() + }) + }) + + describe('pairingFeatures', () => { + it('should be able to construct', () => { + new Characteristic.PairingFeatures() + }) + }) + + describe('pairSetup', () => { + it('should be able to construct', () => { + new Characteristic.PairSetup() + }) + }) + + describe('pairVerify', () => { + it('should be able to construct', () => { + new Characteristic.PairVerify() + }) + }) + + describe('passwordSetting', () => { + it('should be able to construct', () => { + new Characteristic.PasswordSetting() + }) + }) + + describe('periodicSnapshotsActive', () => { + it('should be able to construct', () => { + new Characteristic.PeriodicSnapshotsActive() + }) + }) + + describe('pictureMode', () => { + it('should be able to construct', () => { + new Characteristic.PictureMode() + }) + }) + + describe('ping', () => { + it('should be able to construct', () => { + new Characteristic.Ping() + }) + }) + + describe('pM10Density', () => { + it('should be able to construct', () => { + new Characteristic.PM10Density() + }) + }) + + describe('pM2_5Density', () => { + it('should be able to construct', () => { + new Characteristic.PM2_5Density() + }) + }) + + describe('positionState', () => { + it('should be able to construct', () => { + new Characteristic.PositionState() + }) + }) + + describe('powerModeSelection', () => { + it('should be able to construct', () => { + new Characteristic.PowerModeSelection() + }) + }) + + describe('productData', () => { + it('should be able to construct', () => { + new Characteristic.ProductData() + }) + }) + + describe('programmableSwitchEvent', () => { + it('should be able to construct', () => { + new Characteristic.ProgrammableSwitchEvent() + }) + }) + + describe('programmableSwitchOutputState', () => { + it('should be able to construct', () => { + new Characteristic.ProgrammableSwitchOutputState() + }) + }) + + describe('programMode', () => { + it('should be able to construct', () => { + new Characteristic.ProgramMode() + }) + }) + + describe('receivedSignalStrengthIndication', () => { + it('should be able to construct', () => { + new Characteristic.ReceivedSignalStrengthIndication() + }) + }) + + describe('receiverSensitivity', () => { + it('should be able to construct', () => { + new Characteristic.ReceiverSensitivity() + }) + }) + + describe('recordingAudioActive', () => { + it('should be able to construct', () => { + new Characteristic.RecordingAudioActive() + }) + }) + + describe('relativeHumidityDehumidifierThreshold', () => { + it('should be able to construct', () => { + new Characteristic.RelativeHumidityDehumidifierThreshold() + }) + }) + + describe('relativeHumidityHumidifierThreshold', () => { + it('should be able to construct', () => { + new Characteristic.RelativeHumidityHumidifierThreshold() + }) + }) + + describe('remainingDuration', () => { + it('should be able to construct', () => { + new Characteristic.RemainingDuration() + }) + }) + + describe('remoteKey', () => { + it('should be able to construct', () => { + new Characteristic.RemoteKey() + }) + }) + + describe('resetFilterIndication', () => { + it('should be able to construct', () => { + new Characteristic.ResetFilterIndication() + }) + }) + + describe('rotationDirection', () => { + it('should be able to construct', () => { + new Characteristic.RotationDirection() + }) + }) + + describe('rotationSpeed', () => { + it('should be able to construct', () => { + new Characteristic.RotationSpeed() + }) + }) + + describe('routerStatus', () => { + it('should be able to construct', () => { + new Characteristic.RouterStatus() + }) + }) + + describe('saturation', () => { + it('should be able to construct', () => { + new Characteristic.Saturation() + }) + }) + + describe('securitySystemAlarmType', () => { + it('should be able to construct', () => { + new Characteristic.SecuritySystemAlarmType() + }) + }) + + describe('securitySystemCurrentState', () => { + it('should be able to construct', () => { + new Characteristic.SecuritySystemCurrentState() + }) + }) + + describe('securitySystemTargetState', () => { + it('should be able to construct', () => { + new Characteristic.SecuritySystemTargetState() + }) + }) + + describe('selectedAudioStreamConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SelectedAudioStreamConfiguration() + }) + }) + + describe('selectedCameraRecordingConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SelectedCameraRecordingConfiguration() + }) + }) + + describe('selectedDiagnosticsModes', () => { + it('should be able to construct', () => { + new Characteristic.SelectedDiagnosticsModes() + }) + }) + + describe('selectedRTPStreamConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SelectedRTPStreamConfiguration() + }) + }) + + describe('selectedSleepConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SelectedSleepConfiguration() + }) + }) + + describe('serialNumber', () => { + it('should be able to construct', () => { + new Characteristic.SerialNumber() + }) + }) + + describe('serviceLabelIndex', () => { + it('should be able to construct', () => { + new Characteristic.ServiceLabelIndex() + }) + }) + + describe('serviceLabelNamespace', () => { + it('should be able to construct', () => { + new Characteristic.ServiceLabelNamespace() + }) + }) + + describe('setDuration', () => { + it('should be able to construct', () => { + new Characteristic.SetDuration() + }) + }) + + describe('setupDataStreamTransport', () => { + it('should be able to construct', () => { + new Characteristic.SetupDataStreamTransport() + }) + }) + + describe('setupEndpoints', () => { + it('should be able to construct', () => { + new Characteristic.SetupEndpoints() + }) + }) + + describe('setupTransferTransport', () => { + it('should be able to construct', () => { + new Characteristic.SetupTransferTransport() + }) + }) + + describe('signalToNoiseRatio', () => { + it('should be able to construct', () => { + new Characteristic.SignalToNoiseRatio() + }) + }) + + describe('siriEnable', () => { + it('should be able to construct', () => { + new Characteristic.SiriEnable() + }) + }) + + describe('siriEndpointSessionStatus', () => { + it('should be able to construct', () => { + new Characteristic.SiriEndpointSessionStatus() + }) + }) + + describe('siriEngineVersion', () => { + it('should be able to construct', () => { + new Characteristic.SiriEngineVersion() + }) + }) + + describe('siriInputType', () => { + it('should be able to construct', () => { + new Characteristic.SiriInputType() + }) + }) + + describe('siriLightOnUse', () => { + it('should be able to construct', () => { + new Characteristic.SiriLightOnUse() + }) + }) + + describe('siriListening', () => { + it('should be able to construct', () => { + new Characteristic.SiriListening() + }) + }) + + describe('siriTouchToUse', () => { + it('should be able to construct', () => { + new Characteristic.SiriTouchToUse() + }) + }) + + describe('slatType', () => { + it('should be able to construct', () => { + new Characteristic.SlatType() + }) + }) + + describe('sleepDiscoveryMode', () => { + it('should be able to construct', () => { + new Characteristic.SleepDiscoveryMode() + }) + }) + + describe('sleepInterval', () => { + it('should be able to construct', () => { + new Characteristic.SleepInterval() + }) + }) + + describe('smokeDetected', () => { + it('should be able to construct', () => { + new Characteristic.SmokeDetected() + }) + }) + + describe('softwareRevision', () => { + it('should be able to construct', () => { + new Characteristic.SoftwareRevision() + }) + }) + + describe('stagedFirmwareVersion', () => { + it('should be able to construct', () => { + new Characteristic.StagedFirmwareVersion() + }) + }) + + describe('statusActive', () => { + it('should be able to construct', () => { + new Characteristic.StatusActive() + }) + }) + + describe('statusFault', () => { + it('should be able to construct', () => { + new Characteristic.StatusFault() + }) + }) + + describe('statusJammed', () => { + it('should be able to construct', () => { + new Characteristic.StatusJammed() + }) + }) + + describe('statusLowBattery', () => { + it('should be able to construct', () => { + new Characteristic.StatusLowBattery() + }) + }) + + describe('statusTampered', () => { + it('should be able to construct', () => { + new Characteristic.StatusTampered() + }) + }) + + describe('streamingStatus', () => { + it('should be able to construct', () => { + new Characteristic.StreamingStatus() + }) + }) + + describe('sulphurDioxideDensity', () => { + it('should be able to construct', () => { + new Characteristic.SulphurDioxideDensity() + }) + }) + + describe('supportedAssetTypes', () => { + it('should be able to construct', () => { + new Characteristic.SupportedAssetTypes() + }) + }) + + describe('supportedAudioRecordingConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedAudioRecordingConfiguration() + }) + }) + + describe('supportedAudioStreamConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedAudioStreamConfiguration() + }) + }) + + describe('supportedCameraRecordingConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedCameraRecordingConfiguration() + }) + }) + + describe('supportedCharacteristicValueTransitionConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedCharacteristicValueTransitionConfiguration() + }) + }) + + describe('supportedDataStreamTransportConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedDataStreamTransportConfiguration() + }) + }) + + describe('supportedDiagnosticsModes', () => { + it('should be able to construct', () => { + new Characteristic.SupportedDiagnosticsModes() + }) + }) + + describe('supportedDiagnosticsSnapshot', () => { + it('should be able to construct', () => { + new Characteristic.SupportedDiagnosticsSnapshot() + }) + }) + + describe('supportedFirmwareUpdateConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedFirmwareUpdateConfiguration() + }) + }) + + describe('supportedMetrics', () => { + it('should be able to construct', () => { + new Characteristic.SupportedMetrics() + }) + }) + + describe('supportedRouterConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedRouterConfiguration() + }) + }) + + describe('supportedRTPConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedRTPConfiguration() + }) + }) + + describe('supportedSleepConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedSleepConfiguration() + }) + }) + + describe('supportedTransferTransportConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedTransferTransportConfiguration() + }) + }) + + describe('supportedVideoRecordingConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedVideoRecordingConfiguration() + }) + }) + + describe('supportedVideoStreamConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.SupportedVideoStreamConfiguration() + }) + }) + + describe('swingMode', () => { + it('should be able to construct', () => { + new Characteristic.SwingMode() + }) + }) + + describe('tapType', () => { + it('should be able to construct', () => { + new Characteristic.TapType() + }) + }) + + describe('targetAirPurifierState', () => { + it('should be able to construct', () => { + new Characteristic.TargetAirPurifierState() + }) + }) + + describe('targetControlList', () => { + it('should be able to construct', () => { + new Characteristic.TargetControlList() + }) + }) + + describe('targetControlSupportedConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.TargetControlSupportedConfiguration() + }) + }) + + describe('targetDoorState', () => { + it('should be able to construct', () => { + new Characteristic.TargetDoorState() + }) + }) + + describe('targetFanState', () => { + it('should be able to construct', () => { + new Characteristic.TargetFanState() + }) + }) + + describe('targetHeaterCoolerState', () => { + it('should be able to construct', () => { + new Characteristic.TargetHeaterCoolerState() + }) + }) + + describe('targetHeatingCoolingState', () => { + it('should be able to construct', () => { + new Characteristic.TargetHeatingCoolingState() + }) + }) + + describe('targetHorizontalTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.TargetHorizontalTiltAngle() + }) + }) + + describe('targetHumidifierDehumidifierState', () => { + it('should be able to construct', () => { + new Characteristic.TargetHumidifierDehumidifierState() + }) + }) + + describe('targetMediaState', () => { + it('should be able to construct', () => { + new Characteristic.TargetMediaState() + }) + }) + + describe('targetPosition', () => { + it('should be able to construct', () => { + new Characteristic.TargetPosition() + }) + }) + + describe('targetRelativeHumidity', () => { + it('should be able to construct', () => { + new Characteristic.TargetRelativeHumidity() + }) + }) + + describe('targetTemperature', () => { + it('should be able to construct', () => { + new Characteristic.TargetTemperature() + }) + }) + + describe('targetTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.TargetTiltAngle() + }) + }) + + describe('targetVerticalTiltAngle', () => { + it('should be able to construct', () => { + new Characteristic.TargetVerticalTiltAngle() + }) + }) + + describe('targetVisibilityState', () => { + it('should be able to construct', () => { + new Characteristic.TargetVisibilityState() + }) + }) + + describe('temperatureDisplayUnits', () => { + it('should be able to construct', () => { + new Characteristic.TemperatureDisplayUnits() + }) + }) + + describe('thirdPartyCameraActive', () => { + it('should be able to construct', () => { + new Characteristic.ThirdPartyCameraActive() + }) + }) + + describe('threadControlPoint', () => { + it('should be able to construct', () => { + new Characteristic.ThreadControlPoint() + }) + }) + + describe('threadNodeCapabilities', () => { + it('should be able to construct', () => { + new Characteristic.ThreadNodeCapabilities() + }) + }) + + describe('threadOpenThreadVersion', () => { + it('should be able to construct', () => { + new Characteristic.ThreadOpenThreadVersion() + }) + }) + + describe('threadStatus', () => { + it('should be able to construct', () => { + new Characteristic.ThreadStatus() + }) + }) + + describe('token', () => { + it('should be able to construct', () => { + new Characteristic.Token() + }) + }) + + describe('transmitPower', () => { + it('should be able to construct', () => { + new Characteristic.TransmitPower() + }) + }) + + describe('valveType', () => { + it('should be able to construct', () => { + new Characteristic.ValveType() + }) + }) + + describe('version', () => { + it('should be able to construct', () => { + new Characteristic.Version() + }) + }) + + describe('videoAnalysisActive', () => { + it('should be able to construct', () => { + new Characteristic.VideoAnalysisActive() + }) + }) + + describe('vOCDensity', () => { + it('should be able to construct', () => { + new Characteristic.VOCDensity() + }) + }) + + describe('volume', () => { + it('should be able to construct', () => { + new Characteristic.Volume() + }) + }) + + describe('volumeControlType', () => { + it('should be able to construct', () => { + new Characteristic.VolumeControlType() + }) + }) + + describe('volumeSelector', () => { + it('should be able to construct', () => { + new Characteristic.VolumeSelector() + }) + }) + + describe('wakeConfiguration', () => { + it('should be able to construct', () => { + new Characteristic.WakeConfiguration() + }) + }) + + describe('wANConfigurationList', () => { + it('should be able to construct', () => { + new Characteristic.WANConfigurationList() + }) + }) + + describe('wANStatusList', () => { + it('should be able to construct', () => { + new Characteristic.WANStatusList() + }) + }) + + describe('waterLevel', () => { + it('should be able to construct', () => { + new Characteristic.WaterLevel() + }) + }) + + describe('wiFiCapabilities', () => { + it('should be able to construct', () => { + new Characteristic.WiFiCapabilities() + }) + }) + + describe('wiFiConfigurationControl', () => { + it('should be able to construct', () => { + new Characteristic.WiFiConfigurationControl() + }) + }) + + describe('wiFiSatelliteStatus', () => { + it('should be able to construct', () => { + new Characteristic.WiFiSatelliteStatus() + }) + }) +}) diff --git a/src/lib/definitions/CharacteristicDefinitions.ts b/src/lib/definitions/CharacteristicDefinitions.ts index 953e8ccbf..f3d532a76 100644 --- a/src/lib/definitions/CharacteristicDefinitions.ts +++ b/src/lib/definitions/CharacteristicDefinitions.ts @@ -1,4819 +1,4579 @@ // THIS FILE IS AUTO-GENERATED - DO NOT MODIFY // V=886 -import { Access, Characteristic, Formats, Perms, Units } from "../Characteristic"; +import { Access, Characteristic, Formats, Perms, Units } from '../Characteristic.js' /** * Characteristic "Access Code Control Point" * @since iOS 15 */ export class AccessCodeControlPoint extends Characteristic { - - public static readonly UUID: string = "00000262-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000262-0000-1000-8000-0026BB765291' constructor() { - super("Access Code Control Point", AccessCodeControlPoint.UUID, { + super('Access Code Control Point', AccessCodeControlPoint.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AccessCodeControlPoint = AccessCodeControlPoint; +Characteristic.AccessCodeControlPoint = AccessCodeControlPoint /** * Characteristic "Access Code Supported Configuration" * @since iOS 15 */ export class AccessCodeSupportedConfiguration extends Characteristic { - - public static readonly UUID: string = "00000261-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000261-0000-1000-8000-0026BB765291' constructor() { - super("Access Code Supported Configuration", AccessCodeSupportedConfiguration.UUID, { + super('Access Code Supported Configuration', AccessCodeSupportedConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AccessCodeSupportedConfiguration = AccessCodeSupportedConfiguration; +Characteristic.AccessCodeSupportedConfiguration = AccessCodeSupportedConfiguration /** * Characteristic "Access Control Level" */ export class AccessControlLevel extends Characteristic { - - public static readonly UUID: string = "000000E5-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000E5-0000-1000-8000-0026BB765291' constructor() { - super("Access Control Level", AccessControlLevel.UUID, { + super('Access Control Level', AccessControlLevel.UUID, { format: Formats.UINT16, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 2, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AccessControlLevel = AccessControlLevel; +Characteristic.AccessControlLevel = AccessControlLevel /** * Characteristic "Accessory Flags" */ export class AccessoryFlags extends Characteristic { + public static readonly UUID: string = '000000A6-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000A6-0000-1000-8000-0026BB765291"; - - public static readonly REQUIRES_ADDITIONAL_SETUP_BIT_MASK = 1; + public static readonly REQUIRES_ADDITIONAL_SETUP_BIT_MASK = 1 constructor() { - super("Accessory Flags", AccessoryFlags.UUID, { + super('Accessory Flags', AccessoryFlags.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AccessoryFlags = AccessoryFlags; +Characteristic.AccessoryFlags = AccessoryFlags /** * Characteristic "Accessory Identifier" */ export class AccessoryIdentifier extends Characteristic { - - public static readonly UUID: string = "00000057-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000057-0000-1000-8000-0026BB765291' constructor() { - super("Accessory Identifier", AccessoryIdentifier.UUID, { + super('Accessory Identifier', AccessoryIdentifier.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AccessoryIdentifier = AccessoryIdentifier; +Characteristic.AccessoryIdentifier = AccessoryIdentifier /** * Characteristic "Active" */ export class Active extends Characteristic { + public static readonly UUID: string = '000000B0-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B0-0000-1000-8000-0026BB765291"; - - public static readonly INACTIVE = 0; - public static readonly ACTIVE = 1; + public static readonly INACTIVE = 0 + public static readonly ACTIVE = 1 constructor() { - super("Active", Active.UUID, { + super('Active', Active.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Active = Active; +Characteristic.Active = Active /** * Characteristic "Active Identifier" */ export class ActiveIdentifier extends Characteristic { - - public static readonly UUID: string = "000000E7-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000E7-0000-1000-8000-0026BB765291' constructor() { - super("Active Identifier", ActiveIdentifier.UUID, { + super('Active Identifier', ActiveIdentifier.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ActiveIdentifier = ActiveIdentifier; +Characteristic.ActiveIdentifier = ActiveIdentifier /** * Characteristic "Activity Interval" * @since iOS 14 */ export class ActivityInterval extends Characteristic { - - public static readonly UUID: string = "0000023B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023B-0000-1000-8000-0026BB765291' constructor() { - super("Activity Interval", ActivityInterval.UUID, { + super('Activity Interval', ActivityInterval.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ActivityInterval = ActivityInterval; +Characteristic.ActivityInterval = ActivityInterval /** * Characteristic "Administrator Only Access" */ export class AdministratorOnlyAccess extends Characteristic { - - public static readonly UUID: string = "00000001-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000001-0000-1000-8000-0026BB765291' constructor() { - super("Administrator Only Access", AdministratorOnlyAccess.UUID, { + super('Administrator Only Access', AdministratorOnlyAccess.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AdministratorOnlyAccess = AdministratorOnlyAccess; +Characteristic.AdministratorOnlyAccess = AdministratorOnlyAccess /** * Characteristic "Air Particulate Density" */ export class AirParticulateDensity extends Characteristic { - - public static readonly UUID: string = "00000064-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000064-0000-1000-8000-0026BB765291' constructor() { - super("Air Particulate Density", AirParticulateDensity.UUID, { + super('Air Particulate Density', AirParticulateDensity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AirParticulateDensity = AirParticulateDensity; +Characteristic.AirParticulateDensity = AirParticulateDensity /** * Characteristic "Air Particulate Size" */ export class AirParticulateSize extends Characteristic { + public static readonly UUID: string = '00000065-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000065-0000-1000-8000-0026BB765291"; - - public static readonly _2_5_M = 0; - public static readonly _10_M = 1; + public static readonly _2_5_M = 0 + public static readonly _10_M = 1 constructor() { - super("Air Particulate Size", AirParticulateSize.UUID, { + super('Air Particulate Size', AirParticulateSize.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AirParticulateSize = AirParticulateSize; +Characteristic.AirParticulateSize = AirParticulateSize /** * Characteristic "AirPlay Enable" */ export class AirPlayEnable extends Characteristic { - - public static readonly UUID: string = "0000025B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000025B-0000-1000-8000-0026BB765291' constructor() { - super("AirPlay Enable", AirPlayEnable.UUID, { + super('AirPlay Enable', AirPlayEnable.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AirPlayEnable = AirPlayEnable; +Characteristic.AirPlayEnable = AirPlayEnable /** * Characteristic "Air Quality" */ export class AirQuality extends Characteristic { + public static readonly UUID: string = '00000095-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000095-0000-1000-8000-0026BB765291"; - - public static readonly UNKNOWN = 0; - public static readonly EXCELLENT = 1; - public static readonly GOOD = 2; - public static readonly FAIR = 3; - public static readonly INFERIOR = 4; - public static readonly POOR = 5; + public static readonly UNKNOWN = 0 + public static readonly EXCELLENT = 1 + public static readonly GOOD = 2 + public static readonly FAIR = 3 + public static readonly INFERIOR = 4 + public static readonly POOR = 5 constructor() { - super("Air Quality", AirQuality.UUID, { + super('Air Quality', AirQuality.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 5, minStep: 1, validValues: [0, 1, 2, 3, 4, 5], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AirQuality = AirQuality; +Characteristic.AirQuality = AirQuality /** * Characteristic "App Matching Identifier" */ export class AppMatchingIdentifier extends Characteristic { - - public static readonly UUID: string = "000000A4-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000A4-0000-1000-8000-0026BB765291' constructor() { - super("App Matching Identifier", AppMatchingIdentifier.UUID, { + super('App Matching Identifier', AppMatchingIdentifier.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AppMatchingIdentifier = AppMatchingIdentifier; +Characteristic.AppMatchingIdentifier = AppMatchingIdentifier /** * Characteristic "Asset Update Readiness" */ export class AssetUpdateReadiness extends Characteristic { - - public static readonly UUID: string = "00000269-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000269-0000-1000-8000-0026BB765291' constructor() { - super("Asset Update Readiness", AssetUpdateReadiness.UUID, { + super('Asset Update Readiness', AssetUpdateReadiness.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AssetUpdateReadiness = AssetUpdateReadiness; +Characteristic.AssetUpdateReadiness = AssetUpdateReadiness /** * Characteristic "Audio Feedback" */ export class AudioFeedback extends Characteristic { - - public static readonly UUID: string = "00000005-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000005-0000-1000-8000-0026BB765291' constructor() { - super("Audio Feedback", AudioFeedback.UUID, { + super('Audio Feedback', AudioFeedback.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.AudioFeedback = AudioFeedback; +Characteristic.AudioFeedback = AudioFeedback /** * Characteristic "Battery Level" */ export class BatteryLevel extends Characteristic { - - public static readonly UUID: string = "00000068-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000068-0000-1000-8000-0026BB765291' constructor() { - super("Battery Level", BatteryLevel.UUID, { + super('Battery Level', BatteryLevel.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.BatteryLevel = BatteryLevel; +Characteristic.BatteryLevel = BatteryLevel /** * Characteristic "Brightness" */ export class Brightness extends Characteristic { - - public static readonly UUID: string = "00000008-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000008-0000-1000-8000-0026BB765291' constructor() { - super("Brightness", Brightness.UUID, { + super('Brightness', Brightness.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Brightness = Brightness; +Characteristic.Brightness = Brightness /** * Characteristic "Button Event" */ export class ButtonEvent extends Characteristic { - - public static readonly UUID: string = "00000126-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000126-0000-1000-8000-0026BB765291' constructor() { - super("Button Event", ButtonEvent.UUID, { + super('Button Event', ButtonEvent.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], adminOnlyAccess: [Access.NOTIFY], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ButtonEvent = ButtonEvent; +Characteristic.ButtonEvent = ButtonEvent /** * Characteristic "Camera Operating Mode Indicator" */ export class CameraOperatingModeIndicator extends Characteristic { - - public static readonly UUID: string = "0000021D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000021D-0000-1000-8000-0026BB765291' constructor() { - super("Camera Operating Mode Indicator", CameraOperatingModeIndicator.UUID, { + super('Camera Operating Mode Indicator', CameraOperatingModeIndicator.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CameraOperatingModeIndicator = CameraOperatingModeIndicator; +Characteristic.CameraOperatingModeIndicator = CameraOperatingModeIndicator /** * Characteristic "Carbon Dioxide Detected" */ export class CarbonDioxideDetected extends Characteristic { + public static readonly UUID: string = '00000092-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000092-0000-1000-8000-0026BB765291"; - - public static readonly CO2_LEVELS_NORMAL = 0; - public static readonly CO2_LEVELS_ABNORMAL = 1; + public static readonly CO2_LEVELS_NORMAL = 0 + public static readonly CO2_LEVELS_ABNORMAL = 1 constructor() { - super("Carbon Dioxide Detected", CarbonDioxideDetected.UUID, { + super('Carbon Dioxide Detected', CarbonDioxideDetected.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonDioxideDetected = CarbonDioxideDetected; +Characteristic.CarbonDioxideDetected = CarbonDioxideDetected /** * Characteristic "Carbon Dioxide Level" */ export class CarbonDioxideLevel extends Characteristic { - - public static readonly UUID: string = "00000093-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000093-0000-1000-8000-0026BB765291' constructor() { - super("Carbon Dioxide Level", CarbonDioxideLevel.UUID, { + super('Carbon Dioxide Level', CarbonDioxideLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 100000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonDioxideLevel = CarbonDioxideLevel; +Characteristic.CarbonDioxideLevel = CarbonDioxideLevel /** * Characteristic "Carbon Dioxide Peak Level" */ export class CarbonDioxidePeakLevel extends Characteristic { - - public static readonly UUID: string = "00000094-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000094-0000-1000-8000-0026BB765291' constructor() { - super("Carbon Dioxide Peak Level", CarbonDioxidePeakLevel.UUID, { + super('Carbon Dioxide Peak Level', CarbonDioxidePeakLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 100000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonDioxidePeakLevel = CarbonDioxidePeakLevel; +Characteristic.CarbonDioxidePeakLevel = CarbonDioxidePeakLevel /** * Characteristic "Carbon Monoxide Detected" */ export class CarbonMonoxideDetected extends Characteristic { + public static readonly UUID: string = '00000069-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000069-0000-1000-8000-0026BB765291"; - - public static readonly CO_LEVELS_NORMAL = 0; - public static readonly CO_LEVELS_ABNORMAL = 1; + public static readonly CO_LEVELS_NORMAL = 0 + public static readonly CO_LEVELS_ABNORMAL = 1 constructor() { - super("Carbon Monoxide Detected", CarbonMonoxideDetected.UUID, { + super('Carbon Monoxide Detected', CarbonMonoxideDetected.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonMonoxideDetected = CarbonMonoxideDetected; +Characteristic.CarbonMonoxideDetected = CarbonMonoxideDetected /** * Characteristic "Carbon Monoxide Level" */ export class CarbonMonoxideLevel extends Characteristic { - - public static readonly UUID: string = "00000090-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000090-0000-1000-8000-0026BB765291' constructor() { - super("Carbon Monoxide Level", CarbonMonoxideLevel.UUID, { + super('Carbon Monoxide Level', CarbonMonoxideLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonMonoxideLevel = CarbonMonoxideLevel; +Characteristic.CarbonMonoxideLevel = CarbonMonoxideLevel /** * Characteristic "Carbon Monoxide Peak Level" */ export class CarbonMonoxidePeakLevel extends Characteristic { - - public static readonly UUID: string = "00000091-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000091-0000-1000-8000-0026BB765291' constructor() { - super("Carbon Monoxide Peak Level", CarbonMonoxidePeakLevel.UUID, { + super('Carbon Monoxide Peak Level', CarbonMonoxidePeakLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CarbonMonoxidePeakLevel = CarbonMonoxidePeakLevel; +Characteristic.CarbonMonoxidePeakLevel = CarbonMonoxidePeakLevel /** * Characteristic "CCA Energy Detect Threshold" * @since iOS 14 */ export class CCAEnergyDetectThreshold extends Characteristic { - - public static readonly UUID: string = "00000246-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000246-0000-1000-8000-0026BB765291' constructor() { - super("CCA Energy Detect Threshold", CCAEnergyDetectThreshold.UUID, { + super('CCA Energy Detect Threshold', CCAEnergyDetectThreshold.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CCAEnergyDetectThreshold = CCAEnergyDetectThreshold; +Characteristic.CCAEnergyDetectThreshold = CCAEnergyDetectThreshold /** * Characteristic "CCA Signal Detect Threshold" * @since iOS 14 */ export class CCASignalDetectThreshold extends Characteristic { - - public static readonly UUID: string = "00000245-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000245-0000-1000-8000-0026BB765291' constructor() { - super("CCA Signal Detect Threshold", CCASignalDetectThreshold.UUID, { + super('CCA Signal Detect Threshold', CCASignalDetectThreshold.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CCASignalDetectThreshold = CCASignalDetectThreshold; +Characteristic.CCASignalDetectThreshold = CCASignalDetectThreshold /** * Characteristic "Characteristic Value Active Transition Count" * @since iOS 14 */ export class CharacteristicValueActiveTransitionCount extends Characteristic { - - public static readonly UUID: string = "0000024B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000024B-0000-1000-8000-0026BB765291' constructor() { - super("Characteristic Value Active Transition Count", CharacteristicValueActiveTransitionCount.UUID, { + super('Characteristic Value Active Transition Count', CharacteristicValueActiveTransitionCount.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CharacteristicValueActiveTransitionCount = CharacteristicValueActiveTransitionCount; +Characteristic.CharacteristicValueActiveTransitionCount = CharacteristicValueActiveTransitionCount /** * Characteristic "Characteristic Value Transition Control" * @since iOS 14 */ export class CharacteristicValueTransitionControl extends Characteristic { - - public static readonly UUID: string = "00000143-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000143-0000-1000-8000-0026BB765291' constructor() { - super("Characteristic Value Transition Control", CharacteristicValueTransitionControl.UUID, { + super('Characteristic Value Transition Control', CharacteristicValueTransitionControl.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CharacteristicValueTransitionControl = CharacteristicValueTransitionControl; +Characteristic.CharacteristicValueTransitionControl = CharacteristicValueTransitionControl /** * Characteristic "Charging State" */ export class ChargingState extends Characteristic { + public static readonly UUID: string = '0000008F-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000008F-0000-1000-8000-0026BB765291"; - - public static readonly NOT_CHARGING = 0; - public static readonly CHARGING = 1; - public static readonly NOT_CHARGEABLE = 2; + public static readonly NOT_CHARGING = 0 + public static readonly CHARGING = 1 + public static readonly NOT_CHARGEABLE = 2 constructor() { - super("Charging State", ChargingState.UUID, { + super('Charging State', ChargingState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ChargingState = ChargingState; +Characteristic.ChargingState = ChargingState /** * Characteristic "Closed Captions" */ export class ClosedCaptions extends Characteristic { + public static readonly UUID: string = '000000DD-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000DD-0000-1000-8000-0026BB765291"; - - public static readonly DISABLED = 0; - public static readonly ENABLED = 1; + public static readonly DISABLED = 0 + public static readonly ENABLED = 1 constructor() { - super("Closed Captions", ClosedCaptions.UUID, { + super('Closed Captions', ClosedCaptions.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ClosedCaptions = ClosedCaptions; +Characteristic.ClosedCaptions = ClosedCaptions /** * Characteristic "Color Temperature" */ export class ColorTemperature extends Characteristic { - - public static readonly UUID: string = "000000CE-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000CE-0000-1000-8000-0026BB765291' constructor() { - super("Color Temperature", ColorTemperature.UUID, { + super('Color Temperature', ColorTemperature.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 140, maxValue: 500, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ColorTemperature = ColorTemperature; +Characteristic.ColorTemperature = ColorTemperature /** * Characteristic "Configuration State" * @since iOS 15 */ export class ConfigurationState extends Characteristic { - - public static readonly UUID: string = "00000263-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000263-0000-1000-8000-0026BB765291' constructor() { - super("Configuration State", ConfigurationState.UUID, { + super('Configuration State', ConfigurationState.UUID, { format: Formats.UINT16, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ConfigurationState = ConfigurationState; +Characteristic.ConfigurationState = ConfigurationState /** * Characteristic "Configured Name" */ export class ConfiguredName extends Characteristic { - - public static readonly UUID: string = "000000E3-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000E3-0000-1000-8000-0026BB765291' constructor() { - super("Configured Name", ConfiguredName.UUID, { + super('Configured Name', ConfiguredName.UUID, { format: Formats.STRING, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ConfiguredName = ConfiguredName; +Characteristic.ConfiguredName = ConfiguredName /** * Characteristic "Contact Sensor State" */ export class ContactSensorState extends Characteristic { + public static readonly UUID: string = '0000006A-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000006A-0000-1000-8000-0026BB765291"; - - public static readonly CONTACT_DETECTED = 0; - public static readonly CONTACT_NOT_DETECTED = 1; + public static readonly CONTACT_DETECTED = 0 + public static readonly CONTACT_NOT_DETECTED = 1 constructor() { - super("Contact Sensor State", ContactSensorState.UUID, { + super('Contact Sensor State', ContactSensorState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ContactSensorState = ContactSensorState; +Characteristic.ContactSensorState = ContactSensorState /** * Characteristic "Cooling Threshold Temperature" */ export class CoolingThresholdTemperature extends Characteristic { - - public static readonly UUID: string = "0000000D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000000D-0000-1000-8000-0026BB765291' constructor() { - super("Cooling Threshold Temperature", CoolingThresholdTemperature.UUID, { + super('Cooling Threshold Temperature', CoolingThresholdTemperature.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.CELSIUS, minValue: 10, maxValue: 35, minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CoolingThresholdTemperature = CoolingThresholdTemperature; +Characteristic.CoolingThresholdTemperature = CoolingThresholdTemperature /** * Characteristic "Crypto Hash" */ export class CryptoHash extends Characteristic { - - public static readonly UUID: string = "00000250-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000250-0000-1000-8000-0026BB765291' constructor() { - super("Crypto Hash", CryptoHash.UUID, { + super('Crypto Hash', CryptoHash.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CryptoHash = CryptoHash; +Characteristic.CryptoHash = CryptoHash /** * Characteristic "Current Air Purifier State" */ export class CurrentAirPurifierState extends Characteristic { + public static readonly UUID: string = '000000A9-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000A9-0000-1000-8000-0026BB765291"; - - public static readonly INACTIVE = 0; - public static readonly IDLE = 1; - public static readonly PURIFYING_AIR = 2; + public static readonly INACTIVE = 0 + public static readonly IDLE = 1 + public static readonly PURIFYING_AIR = 2 constructor() { - super("Current Air Purifier State", CurrentAirPurifierState.UUID, { + super('Current Air Purifier State', CurrentAirPurifierState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentAirPurifierState = CurrentAirPurifierState; +Characteristic.CurrentAirPurifierState = CurrentAirPurifierState /** * Characteristic "Current Ambient Light Level" */ export class CurrentAmbientLightLevel extends Characteristic { - - public static readonly UUID: string = "0000006B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000006B-0000-1000-8000-0026BB765291' constructor() { - super("Current Ambient Light Level", CurrentAmbientLightLevel.UUID, { + super('Current Ambient Light Level', CurrentAmbientLightLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.LUX, minValue: 0.0001, maxValue: 100000, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentAmbientLightLevel = CurrentAmbientLightLevel; +Characteristic.CurrentAmbientLightLevel = CurrentAmbientLightLevel /** * Characteristic "Current Door State" */ export class CurrentDoorState extends Characteristic { + public static readonly UUID: string = '0000000E-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000000E-0000-1000-8000-0026BB765291"; - - public static readonly OPEN = 0; - public static readonly CLOSED = 1; - public static readonly OPENING = 2; - public static readonly CLOSING = 3; - public static readonly STOPPED = 4; + public static readonly OPEN = 0 + public static readonly CLOSED = 1 + public static readonly OPENING = 2 + public static readonly CLOSING = 3 + public static readonly STOPPED = 4 constructor() { - super("Current Door State", CurrentDoorState.UUID, { + super('Current Door State', CurrentDoorState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 4, minStep: 1, validValues: [0, 1, 2, 3, 4], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentDoorState = CurrentDoorState; +Characteristic.CurrentDoorState = CurrentDoorState /** * Characteristic "Current Fan State" */ export class CurrentFanState extends Characteristic { + public static readonly UUID: string = '000000AF-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000AF-0000-1000-8000-0026BB765291"; - - public static readonly INACTIVE = 0; - public static readonly IDLE = 1; - public static readonly BLOWING_AIR = 2; + public static readonly INACTIVE = 0 + public static readonly IDLE = 1 + public static readonly BLOWING_AIR = 2 constructor() { - super("Current Fan State", CurrentFanState.UUID, { + super('Current Fan State', CurrentFanState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentFanState = CurrentFanState; +Characteristic.CurrentFanState = CurrentFanState /** * Characteristic "Current Heater-Cooler State" */ export class CurrentHeaterCoolerState extends Characteristic { + public static readonly UUID: string = '000000B1-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B1-0000-1000-8000-0026BB765291"; - - public static readonly INACTIVE = 0; - public static readonly IDLE = 1; - public static readonly HEATING = 2; - public static readonly COOLING = 3; + public static readonly INACTIVE = 0 + public static readonly IDLE = 1 + public static readonly HEATING = 2 + public static readonly COOLING = 3 constructor() { - super("Current Heater-Cooler State", CurrentHeaterCoolerState.UUID, { + super('Current Heater-Cooler State', CurrentHeaterCoolerState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentHeaterCoolerState = CurrentHeaterCoolerState; +Characteristic.CurrentHeaterCoolerState = CurrentHeaterCoolerState /** * Characteristic "Current Heating Cooling State" */ export class CurrentHeatingCoolingState extends Characteristic { + public static readonly UUID: string = '0000000F-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000000F-0000-1000-8000-0026BB765291"; - - public static readonly OFF = 0; - public static readonly HEAT = 1; - public static readonly COOL = 2; + public static readonly OFF = 0 + public static readonly HEAT = 1 + public static readonly COOL = 2 constructor() { - super("Current Heating Cooling State", CurrentHeatingCoolingState.UUID, { + super('Current Heating Cooling State', CurrentHeatingCoolingState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentHeatingCoolingState = CurrentHeatingCoolingState; +Characteristic.CurrentHeatingCoolingState = CurrentHeatingCoolingState /** * Characteristic "Current Horizontal Tilt Angle" */ export class CurrentHorizontalTiltAngle extends Characteristic { - - public static readonly UUID: string = "0000006C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000006C-0000-1000-8000-0026BB765291' constructor() { - super("Current Horizontal Tilt Angle", CurrentHorizontalTiltAngle.UUID, { + super('Current Horizontal Tilt Angle', CurrentHorizontalTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentHorizontalTiltAngle = CurrentHorizontalTiltAngle; +Characteristic.CurrentHorizontalTiltAngle = CurrentHorizontalTiltAngle /** * Characteristic "Current Humidifier-Dehumidifier State" */ export class CurrentHumidifierDehumidifierState extends Characteristic { + public static readonly UUID: string = '000000B3-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B3-0000-1000-8000-0026BB765291"; - - public static readonly INACTIVE = 0; - public static readonly IDLE = 1; - public static readonly HUMIDIFYING = 2; - public static readonly DEHUMIDIFYING = 3; + public static readonly INACTIVE = 0 + public static readonly IDLE = 1 + public static readonly HUMIDIFYING = 2 + public static readonly DEHUMIDIFYING = 3 constructor() { - super("Current Humidifier-Dehumidifier State", CurrentHumidifierDehumidifierState.UUID, { + super('Current Humidifier-Dehumidifier State', CurrentHumidifierDehumidifierState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentHumidifierDehumidifierState = CurrentHumidifierDehumidifierState; +Characteristic.CurrentHumidifierDehumidifierState = CurrentHumidifierDehumidifierState /** * Characteristic "Current Media State" */ export class CurrentMediaState extends Characteristic { + public static readonly UUID: string = '000000E0-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000E0-0000-1000-8000-0026BB765291"; - - public static readonly PLAY = 0; - public static readonly PAUSE = 1; - public static readonly STOP = 2; - public static readonly LOADING = 4; - public static readonly INTERRUPTED = 5; + public static readonly PLAY = 0 + public static readonly PAUSE = 1 + public static readonly STOP = 2 + public static readonly LOADING = 4 + public static readonly INTERRUPTED = 5 constructor() { - super("Current Media State", CurrentMediaState.UUID, { + super('Current Media State', CurrentMediaState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 5, minStep: 1, validValues: [0, 1, 2, 4, 5], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentMediaState = CurrentMediaState; +Characteristic.CurrentMediaState = CurrentMediaState /** * Characteristic "Current Position" */ export class CurrentPosition extends Characteristic { - - public static readonly UUID: string = "0000006D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000006D-0000-1000-8000-0026BB765291' constructor() { - super("Current Position", CurrentPosition.UUID, { + super('Current Position', CurrentPosition.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentPosition = CurrentPosition; +Characteristic.CurrentPosition = CurrentPosition /** * Characteristic "Current Relative Humidity" */ export class CurrentRelativeHumidity extends Characteristic { - - public static readonly UUID: string = "00000010-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000010-0000-1000-8000-0026BB765291' constructor() { - super("Current Relative Humidity", CurrentRelativeHumidity.UUID, { + super('Current Relative Humidity', CurrentRelativeHumidity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentRelativeHumidity = CurrentRelativeHumidity; +Characteristic.CurrentRelativeHumidity = CurrentRelativeHumidity /** * Characteristic "Current Slat State" */ export class CurrentSlatState extends Characteristic { + public static readonly UUID: string = '000000AA-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000AA-0000-1000-8000-0026BB765291"; - - public static readonly FIXED = 0; - public static readonly JAMMED = 1; - public static readonly SWINGING = 2; + public static readonly FIXED = 0 + public static readonly JAMMED = 1 + public static readonly SWINGING = 2 constructor() { - super("Current Slat State", CurrentSlatState.UUID, { + super('Current Slat State', CurrentSlatState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentSlatState = CurrentSlatState; +Characteristic.CurrentSlatState = CurrentSlatState /** * Characteristic "Current Temperature" */ export class CurrentTemperature extends Characteristic { - - public static readonly UUID: string = "00000011-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000011-0000-1000-8000-0026BB765291' constructor() { - super("Current Temperature", CurrentTemperature.UUID, { + super('Current Temperature', CurrentTemperature.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.CELSIUS, minValue: -270, maxValue: 100, minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentTemperature = CurrentTemperature; +Characteristic.CurrentTemperature = CurrentTemperature /** * Characteristic "Current Tilt Angle" */ export class CurrentTiltAngle extends Characteristic { - - public static readonly UUID: string = "000000C1-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C1-0000-1000-8000-0026BB765291' constructor() { - super("Current Tilt Angle", CurrentTiltAngle.UUID, { + super('Current Tilt Angle', CurrentTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentTiltAngle = CurrentTiltAngle; +Characteristic.CurrentTiltAngle = CurrentTiltAngle /** * Characteristic "Current Transport" * @since iOS 14 */ export class CurrentTransport extends Characteristic { - - public static readonly UUID: string = "0000022B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022B-0000-1000-8000-0026BB765291' constructor() { - super("Current Transport", CurrentTransport.UUID, { + super('Current Transport', CurrentTransport.UUID, { format: Formats.BOOL, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentTransport = CurrentTransport; +Characteristic.CurrentTransport = CurrentTransport /** * Characteristic "Current Vertical Tilt Angle" */ export class CurrentVerticalTiltAngle extends Characteristic { - - public static readonly UUID: string = "0000006E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000006E-0000-1000-8000-0026BB765291' constructor() { - super("Current Vertical Tilt Angle", CurrentVerticalTiltAngle.UUID, { + super('Current Vertical Tilt Angle', CurrentVerticalTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentVerticalTiltAngle = CurrentVerticalTiltAngle; +Characteristic.CurrentVerticalTiltAngle = CurrentVerticalTiltAngle /** * Characteristic "Current Visibility State" */ export class CurrentVisibilityState extends Characteristic { + public static readonly UUID: string = '00000135-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000135-0000-1000-8000-0026BB765291"; - - public static readonly SHOWN = 0; - public static readonly HIDDEN = 1; + public static readonly SHOWN = 0 + public static readonly HIDDEN = 1 constructor() { - super("Current Visibility State", CurrentVisibilityState.UUID, { + super('Current Visibility State', CurrentVisibilityState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.CurrentVisibilityState = CurrentVisibilityState; +Characteristic.CurrentVisibilityState = CurrentVisibilityState /** * Characteristic "Data Stream HAP Transport" * @since iOS 14 */ export class DataStreamHAPTransport extends Characteristic { - - public static readonly UUID: string = "00000138-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000138-0000-1000-8000-0026BB765291' constructor() { - super("Data Stream HAP Transport", DataStreamHAPTransport.UUID, { + super('Data Stream HAP Transport', DataStreamHAPTransport.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.DataStreamHAPTransport = DataStreamHAPTransport; +Characteristic.DataStreamHAPTransport = DataStreamHAPTransport /** * Characteristic "Data Stream HAP Transport Interrupt" * @since iOS 14 */ export class DataStreamHAPTransportInterrupt extends Characteristic { - - public static readonly UUID: string = "00000139-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000139-0000-1000-8000-0026BB765291' constructor() { - super("Data Stream HAP Transport Interrupt", DataStreamHAPTransportInterrupt.UUID, { + super('Data Stream HAP Transport Interrupt', DataStreamHAPTransportInterrupt.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.DataStreamHAPTransportInterrupt = DataStreamHAPTransportInterrupt; +Characteristic.DataStreamHAPTransportInterrupt = DataStreamHAPTransportInterrupt /** * Characteristic "Diagonal Field Of View" * @since iOS 13.2 */ export class DiagonalFieldOfView extends Characteristic { - - public static readonly UUID: string = "00000224-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000224-0000-1000-8000-0026BB765291' constructor() { - super("Diagonal Field Of View", DiagonalFieldOfView.UUID, { + super('Diagonal Field Of View', DiagonalFieldOfView.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.ARC_DEGREE, minValue: 0, maxValue: 360, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.DiagonalFieldOfView = DiagonalFieldOfView; +Characteristic.DiagonalFieldOfView = DiagonalFieldOfView /** * Characteristic "Digital Zoom" */ export class DigitalZoom extends Characteristic { - - public static readonly UUID: string = "0000011D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011D-0000-1000-8000-0026BB765291' constructor() { - super("Digital Zoom", DigitalZoom.UUID, { + super('Digital Zoom', DigitalZoom.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.DigitalZoom = DigitalZoom; +Characteristic.DigitalZoom = DigitalZoom /** * Characteristic "Display Order" */ export class DisplayOrder extends Characteristic { - - public static readonly UUID: string = "00000136-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000136-0000-1000-8000-0026BB765291' constructor() { - super("Display Order", DisplayOrder.UUID, { + super('Display Order', DisplayOrder.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.DisplayOrder = DisplayOrder; +Characteristic.DisplayOrder = DisplayOrder /** * Characteristic "Event Retransmission Maximum" * @since iOS 14 */ export class EventRetransmissionMaximum extends Characteristic { - - public static readonly UUID: string = "0000023D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023D-0000-1000-8000-0026BB765291' constructor() { - super("Event Retransmission Maximum", EventRetransmissionMaximum.UUID, { + super('Event Retransmission Maximum', EventRetransmissionMaximum.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.EventRetransmissionMaximum = EventRetransmissionMaximum; +Characteristic.EventRetransmissionMaximum = EventRetransmissionMaximum /** * Characteristic "Event Snapshots Active" */ export class EventSnapshotsActive extends Characteristic { - - public static readonly UUID: string = "00000223-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000223-0000-1000-8000-0026BB765291' constructor() { - super("Event Snapshots Active", EventSnapshotsActive.UUID, { + super('Event Snapshots Active', EventSnapshotsActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.EventSnapshotsActive = EventSnapshotsActive; +Characteristic.EventSnapshotsActive = EventSnapshotsActive /** * Characteristic "Event Transmission Counters" * @since iOS 14 */ export class EventTransmissionCounters extends Characteristic { - - public static readonly UUID: string = "0000023E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023E-0000-1000-8000-0026BB765291' constructor() { - super("Event Transmission Counters", EventTransmissionCounters.UUID, { + super('Event Transmission Counters', EventTransmissionCounters.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.EventTransmissionCounters = EventTransmissionCounters; +Characteristic.EventTransmissionCounters = EventTransmissionCounters /** * Characteristic "Filter Change Indication" */ export class FilterChangeIndication extends Characteristic { + public static readonly UUID: string = '000000AC-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000AC-0000-1000-8000-0026BB765291"; - - public static readonly FILTER_OK = 0; - public static readonly CHANGE_FILTER = 1; + public static readonly FILTER_OK = 0 + public static readonly CHANGE_FILTER = 1 constructor() { - super("Filter Change Indication", FilterChangeIndication.UUID, { + super('Filter Change Indication', FilterChangeIndication.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.FilterChangeIndication = FilterChangeIndication; +Characteristic.FilterChangeIndication = FilterChangeIndication /** * Characteristic "Filter Life Level" */ export class FilterLifeLevel extends Characteristic { - - public static readonly UUID: string = "000000AB-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000AB-0000-1000-8000-0026BB765291' constructor() { - super("Filter Life Level", FilterLifeLevel.UUID, { + super('Filter Life Level', FilterLifeLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.FilterLifeLevel = FilterLifeLevel; +Characteristic.FilterLifeLevel = FilterLifeLevel /** * Characteristic "Firmware Revision" */ export class FirmwareRevision extends Characteristic { - - public static readonly UUID: string = "00000052-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000052-0000-1000-8000-0026BB765291' constructor() { - super("Firmware Revision", FirmwareRevision.UUID, { + super('Firmware Revision', FirmwareRevision.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.FirmwareRevision = FirmwareRevision; +Characteristic.FirmwareRevision = FirmwareRevision /** * Characteristic "Firmware Update Readiness" */ export class FirmwareUpdateReadiness extends Characteristic { - - public static readonly UUID: string = "00000234-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000234-0000-1000-8000-0026BB765291' constructor() { - super("Firmware Update Readiness", FirmwareUpdateReadiness.UUID, { + super('Firmware Update Readiness', FirmwareUpdateReadiness.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.FirmwareUpdateReadiness = FirmwareUpdateReadiness; +Characteristic.FirmwareUpdateReadiness = FirmwareUpdateReadiness /** * Characteristic "Firmware Update Status" */ export class FirmwareUpdateStatus extends Characteristic { - - public static readonly UUID: string = "00000235-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000235-0000-1000-8000-0026BB765291' constructor() { - super("Firmware Update Status", FirmwareUpdateStatus.UUID, { + super('Firmware Update Status', FirmwareUpdateStatus.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.FirmwareUpdateStatus = FirmwareUpdateStatus; +Characteristic.FirmwareUpdateStatus = FirmwareUpdateStatus /** * Characteristic "Hardware Finish" * @since iOS 15 */ export class HardwareFinish extends Characteristic { - - public static readonly UUID: string = "0000026C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000026C-0000-1000-8000-0026BB765291' constructor() { - super("Hardware Finish", HardwareFinish.UUID, { + super('Hardware Finish', HardwareFinish.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HardwareFinish = HardwareFinish; +Characteristic.HardwareFinish = HardwareFinish /** * Characteristic "Hardware Revision" */ export class HardwareRevision extends Characteristic { - - public static readonly UUID: string = "00000053-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000053-0000-1000-8000-0026BB765291' constructor() { - super("Hardware Revision", HardwareRevision.UUID, { + super('Hardware Revision', HardwareRevision.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HardwareRevision = HardwareRevision; +Characteristic.HardwareRevision = HardwareRevision /** * Characteristic "Heart Beat" * @since iOS 14 */ export class HeartBeat extends Characteristic { - - public static readonly UUID: string = "0000024A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000024A-0000-1000-8000-0026BB765291' constructor() { - super("Heart Beat", HeartBeat.UUID, { + super('Heart Beat', HeartBeat.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HeartBeat = HeartBeat; +Characteristic.HeartBeat = HeartBeat /** * Characteristic "Heating Threshold Temperature" */ export class HeatingThresholdTemperature extends Characteristic { - - public static readonly UUID: string = "00000012-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000012-0000-1000-8000-0026BB765291' constructor() { - super("Heating Threshold Temperature", HeatingThresholdTemperature.UUID, { + super('Heating Threshold Temperature', HeatingThresholdTemperature.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.CELSIUS, minValue: 0, maxValue: 25, minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HeatingThresholdTemperature = HeatingThresholdTemperature; +Characteristic.HeatingThresholdTemperature = HeatingThresholdTemperature /** * Characteristic "Hold Position" */ export class HoldPosition extends Characteristic { - - public static readonly UUID: string = "0000006F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000006F-0000-1000-8000-0026BB765291' constructor() { - super("Hold Position", HoldPosition.UUID, { + super('Hold Position', HoldPosition.UUID, { format: Formats.BOOL, perms: [Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HoldPosition = HoldPosition; +Characteristic.HoldPosition = HoldPosition /** * Characteristic "HomeKit Camera Active" */ export class HomeKitCameraActive extends Characteristic { - - public static readonly UUID: string = "0000021B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000021B-0000-1000-8000-0026BB765291' constructor() { - super("HomeKit Camera Active", HomeKitCameraActive.UUID, { + super('HomeKit Camera Active', HomeKitCameraActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.HomeKitCameraActive = HomeKitCameraActive; +Characteristic.HomeKitCameraActive = HomeKitCameraActive /** * Characteristic "Hue" */ export class Hue extends Characteristic { - - public static readonly UUID: string = "00000013-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000013-0000-1000-8000-0026BB765291' constructor() { - super("Hue", Hue.UUID, { + super('Hue', Hue.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.ARC_DEGREE, minValue: 0, maxValue: 360, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Hue = Hue; +Characteristic.Hue = Hue /** * Characteristic "Identifier" */ export class Identifier extends Characteristic { - - public static readonly UUID: string = "000000E6-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000E6-0000-1000-8000-0026BB765291' constructor() { - super("Identifier", Identifier.UUID, { + super('Identifier', Identifier.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Identifier = Identifier; +Characteristic.Identifier = Identifier /** * Characteristic "Identify" */ export class Identify extends Characteristic { - - public static readonly UUID: string = "00000014-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000014-0000-1000-8000-0026BB765291' constructor() { - super("Identify", Identify.UUID, { + super('Identify', Identify.UUID, { format: Formats.BOOL, perms: [Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Identify = Identify; +Characteristic.Identify = Identify /** * Characteristic "Image Mirroring" */ export class ImageMirroring extends Characteristic { - - public static readonly UUID: string = "0000011F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011F-0000-1000-8000-0026BB765291' constructor() { - super("Image Mirroring", ImageMirroring.UUID, { + super('Image Mirroring', ImageMirroring.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ImageMirroring = ImageMirroring; +Characteristic.ImageMirroring = ImageMirroring /** * Characteristic "Image Rotation" */ export class ImageRotation extends Characteristic { - - public static readonly UUID: string = "0000011E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011E-0000-1000-8000-0026BB765291' constructor() { - super("Image Rotation", ImageRotation.UUID, { + super('Image Rotation', ImageRotation.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.ARC_DEGREE, minValue: 0, maxValue: 360, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ImageRotation = ImageRotation; +Characteristic.ImageRotation = ImageRotation /** * Characteristic "Input Device Type" */ export class InputDeviceType extends Characteristic { + public static readonly UUID: string = '000000DC-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000DC-0000-1000-8000-0026BB765291"; - - public static readonly OTHER = 0; - public static readonly TV = 1; - public static readonly RECORDING = 2; - public static readonly TUNER = 3; - public static readonly PLAYBACK = 4; - public static readonly AUDIO_SYSTEM = 5; + public static readonly OTHER = 0 + public static readonly TV = 1 + public static readonly RECORDING = 2 + public static readonly TUNER = 3 + public static readonly PLAYBACK = 4 + public static readonly AUDIO_SYSTEM = 5 constructor() { - super("Input Device Type", InputDeviceType.UUID, { + super('Input Device Type', InputDeviceType.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 6, minStep: 1, validValues: [0, 1, 2, 3, 4, 5, 6], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.InputDeviceType = InputDeviceType; +Characteristic.InputDeviceType = InputDeviceType /** * Characteristic "Input Source Type" */ export class InputSourceType extends Characteristic { - - public static readonly UUID: string = "000000DB-0000-1000-8000-0026BB765291"; - - public static readonly OTHER = 0; - public static readonly HOME_SCREEN = 1; - public static readonly TUNER = 2; - public static readonly HDMI = 3; - public static readonly COMPOSITE_VIDEO = 4; - public static readonly S_VIDEO = 5; - public static readonly COMPONENT_VIDEO = 6; - public static readonly DVI = 7; - public static readonly AIRPLAY = 8; - public static readonly USB = 9; - public static readonly APPLICATION = 10; - - constructor() { - super("Input Source Type", InputSourceType.UUID, { + public static readonly UUID: string = '000000DB-0000-1000-8000-0026BB765291' + + public static readonly OTHER = 0 + public static readonly HOME_SCREEN = 1 + public static readonly TUNER = 2 + public static readonly HDMI = 3 + public static readonly COMPOSITE_VIDEO = 4 + public static readonly S_VIDEO = 5 + public static readonly COMPONENT_VIDEO = 6 + public static readonly DVI = 7 + public static readonly AIRPLAY = 8 + public static readonly USB = 9 + public static readonly APPLICATION = 10 + + constructor() { + super('Input Source Type', InputSourceType.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 10, minStep: 1, validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.InputSourceType = InputSourceType; +Characteristic.InputSourceType = InputSourceType /** * Characteristic "In Use" */ export class InUse extends Characteristic { + public static readonly UUID: string = '000000D2-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000D2-0000-1000-8000-0026BB765291"; - - public static readonly NOT_IN_USE = 0; - public static readonly IN_USE = 1; + public static readonly NOT_IN_USE = 0 + public static readonly IN_USE = 1 constructor() { - super("In Use", InUse.UUID, { + super('In Use', InUse.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.InUse = InUse; +Characteristic.InUse = InUse /** * Characteristic "Is Configured" */ export class IsConfigured extends Characteristic { + public static readonly UUID: string = '000000D6-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000D6-0000-1000-8000-0026BB765291"; - - public static readonly NOT_CONFIGURED = 0; - public static readonly CONFIGURED = 1; + public static readonly NOT_CONFIGURED = 0 + public static readonly CONFIGURED = 1 constructor() { - super("Is Configured", IsConfigured.UUID, { + super('Is Configured', IsConfigured.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.IsConfigured = IsConfigured; +Characteristic.IsConfigured = IsConfigured /** * Characteristic "Leak Detected" */ export class LeakDetected extends Characteristic { + public static readonly UUID: string = '00000070-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000070-0000-1000-8000-0026BB765291"; - - public static readonly LEAK_NOT_DETECTED = 0; - public static readonly LEAK_DETECTED = 1; + public static readonly LEAK_NOT_DETECTED = 0 + public static readonly LEAK_DETECTED = 1 constructor() { - super("Leak Detected", LeakDetected.UUID, { + super('Leak Detected', LeakDetected.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LeakDetected = LeakDetected; +Characteristic.LeakDetected = LeakDetected /** * Characteristic "List Pairings" */ export class ListPairings extends Characteristic { - - public static readonly UUID: string = "00000050-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000050-0000-1000-8000-0026BB765291' constructor() { - super("List Pairings", ListPairings.UUID, { + super('List Pairings', ListPairings.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ListPairings = ListPairings; +Characteristic.ListPairings = ListPairings /** * Characteristic "Lock Control Point" */ export class LockControlPoint extends Characteristic { - - public static readonly UUID: string = "00000019-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000019-0000-1000-8000-0026BB765291' constructor() { - super("Lock Control Point", LockControlPoint.UUID, { + super('Lock Control Point', LockControlPoint.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockControlPoint = LockControlPoint; +Characteristic.LockControlPoint = LockControlPoint /** * Characteristic "Lock Current State" */ export class LockCurrentState extends Characteristic { + public static readonly UUID: string = '0000001D-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000001D-0000-1000-8000-0026BB765291"; - - public static readonly UNSECURED = 0; - public static readonly SECURED = 1; - public static readonly JAMMED = 2; - public static readonly UNKNOWN = 3; + public static readonly UNSECURED = 0 + public static readonly SECURED = 1 + public static readonly JAMMED = 2 + public static readonly UNKNOWN = 3 constructor() { - super("Lock Current State", LockCurrentState.UUID, { + super('Lock Current State', LockCurrentState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockCurrentState = LockCurrentState; +Characteristic.LockCurrentState = LockCurrentState /** * Characteristic "Lock Last Known Action" */ export class LockLastKnownAction extends Characteristic { - - public static readonly UUID: string = "0000001C-0000-1000-8000-0026BB765291"; - - public static readonly SECURED_PHYSICALLY_INTERIOR = 0; - public static readonly UNSECURED_PHYSICALLY_INTERIOR = 1; - public static readonly SECURED_PHYSICALLY_EXTERIOR = 2; - public static readonly UNSECURED_PHYSICALLY_EXTERIOR = 3; - public static readonly SECURED_BY_KEYPAD = 4; - public static readonly UNSECURED_BY_KEYPAD = 5; - public static readonly SECURED_REMOTELY = 6; - public static readonly UNSECURED_REMOTELY = 7; - public static readonly SECURED_BY_AUTO_SECURE_TIMEOUT = 8; - public static readonly SECURED_PHYSICALLY = 9; - public static readonly UNSECURED_PHYSICALLY = 10; - - constructor() { - super("Lock Last Known Action", LockLastKnownAction.UUID, { + public static readonly UUID: string = '0000001C-0000-1000-8000-0026BB765291' + + public static readonly SECURED_PHYSICALLY_INTERIOR = 0 + public static readonly UNSECURED_PHYSICALLY_INTERIOR = 1 + public static readonly SECURED_PHYSICALLY_EXTERIOR = 2 + public static readonly UNSECURED_PHYSICALLY_EXTERIOR = 3 + public static readonly SECURED_BY_KEYPAD = 4 + public static readonly UNSECURED_BY_KEYPAD = 5 + public static readonly SECURED_REMOTELY = 6 + public static readonly UNSECURED_REMOTELY = 7 + public static readonly SECURED_BY_AUTO_SECURE_TIMEOUT = 8 + public static readonly SECURED_PHYSICALLY = 9 + public static readonly UNSECURED_PHYSICALLY = 10 + + constructor() { + super('Lock Last Known Action', LockLastKnownAction.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 10, minStep: 1, validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockLastKnownAction = LockLastKnownAction; +Characteristic.LockLastKnownAction = LockLastKnownAction /** * Characteristic "Lock Management Auto Security Timeout" */ export class LockManagementAutoSecurityTimeout extends Characteristic { - - public static readonly UUID: string = "0000001A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000001A-0000-1000-8000-0026BB765291' constructor() { - super("Lock Management Auto Security Timeout", LockManagementAutoSecurityTimeout.UUID, { + super('Lock Management Auto Security Timeout', LockManagementAutoSecurityTimeout.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.SECONDS, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockManagementAutoSecurityTimeout = LockManagementAutoSecurityTimeout; +Characteristic.LockManagementAutoSecurityTimeout = LockManagementAutoSecurityTimeout /** * Characteristic "Lock Physical Controls" */ export class LockPhysicalControls extends Characteristic { + public static readonly UUID: string = '000000A7-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000A7-0000-1000-8000-0026BB765291"; - - public static readonly CONTROL_LOCK_DISABLED = 0; - public static readonly CONTROL_LOCK_ENABLED = 1; + public static readonly CONTROL_LOCK_DISABLED = 0 + public static readonly CONTROL_LOCK_ENABLED = 1 constructor() { - super("Lock Physical Controls", LockPhysicalControls.UUID, { + super('Lock Physical Controls', LockPhysicalControls.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockPhysicalControls = LockPhysicalControls; +Characteristic.LockPhysicalControls = LockPhysicalControls /** * Characteristic "Lock Target State" */ export class LockTargetState extends Characteristic { + public static readonly UUID: string = '0000001E-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000001E-0000-1000-8000-0026BB765291"; - - public static readonly UNSECURED = 0; - public static readonly SECURED = 1; + public static readonly UNSECURED = 0 + public static readonly SECURED = 1 constructor() { - super("Lock Target State", LockTargetState.UUID, { + super('Lock Target State', LockTargetState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.LockTargetState = LockTargetState; +Characteristic.LockTargetState = LockTargetState /** * Characteristic "Logs" */ export class Logs extends Characteristic { - - public static readonly UUID: string = "0000001F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000001F-0000-1000-8000-0026BB765291' constructor() { - super("Logs", Logs.UUID, { + super('Logs', Logs.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Logs = Logs; +Characteristic.Logs = Logs /** * Characteristic "MAC Retransmission Maximum" * @since iOS 14 */ export class MACRetransmissionMaximum extends Characteristic { - - public static readonly UUID: string = "00000247-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000247-0000-1000-8000-0026BB765291' constructor() { - super("MAC Retransmission Maximum", MACRetransmissionMaximum.UUID, { + super('MAC Retransmission Maximum', MACRetransmissionMaximum.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MACRetransmissionMaximum = MACRetransmissionMaximum; +Characteristic.MACRetransmissionMaximum = MACRetransmissionMaximum /** * Characteristic "MAC Transmission Counters" */ export class MACTransmissionCounters extends Characteristic { - - public static readonly UUID: string = "00000248-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000248-0000-1000-8000-0026BB765291' constructor() { - super("MAC Transmission Counters", MACTransmissionCounters.UUID, { + super('MAC Transmission Counters', MACTransmissionCounters.UUID, { format: Formats.DATA, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MACTransmissionCounters = MACTransmissionCounters; +Characteristic.MACTransmissionCounters = MACTransmissionCounters /** * Characteristic "Managed Network Enable" */ export class ManagedNetworkEnable extends Characteristic { + public static readonly UUID: string = '00000215-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000215-0000-1000-8000-0026BB765291"; - - public static readonly DISABLED = 0; - public static readonly ENABLED = 1; + public static readonly DISABLED = 0 + public static readonly ENABLED = 1 constructor() { - super("Managed Network Enable", ManagedNetworkEnable.UUID, { + super('Managed Network Enable', ManagedNetworkEnable.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE], minValue: 0, maxValue: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ManagedNetworkEnable = ManagedNetworkEnable; +Characteristic.ManagedNetworkEnable = ManagedNetworkEnable /** * Characteristic "Manually Disabled" */ export class ManuallyDisabled extends Characteristic { + public static readonly UUID: string = '00000227-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000227-0000-1000-8000-0026BB765291"; - - public static readonly ENABLED = 0; - public static readonly DISABLED = 1; + public static readonly ENABLED = 0 + public static readonly DISABLED = 1 constructor() { - super("Manually Disabled", ManuallyDisabled.UUID, { + super('Manually Disabled', ManuallyDisabled.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ManuallyDisabled = ManuallyDisabled; +Characteristic.ManuallyDisabled = ManuallyDisabled /** * Characteristic "Manufacturer" */ export class Manufacturer extends Characteristic { - - public static readonly UUID: string = "00000020-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000020-0000-1000-8000-0026BB765291' constructor() { - super("Manufacturer", Manufacturer.UUID, { + super('Manufacturer', Manufacturer.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], maxLen: 64, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Manufacturer = Manufacturer; +Characteristic.Manufacturer = Manufacturer /** * Characteristic "Matter Firmware Revision Number" */ export class MatterFirmwareRevisionNumber extends Characteristic { - - public static readonly UUID: string = "0000026D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000026D-0000-1000-8000-0026BB765291' constructor() { - super("Matter Firmware Revision Number", MatterFirmwareRevisionNumber.UUID, { + super('Matter Firmware Revision Number', MatterFirmwareRevisionNumber.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MatterFirmwareRevisionNumber = MatterFirmwareRevisionNumber; +Characteristic.MatterFirmwareRevisionNumber = MatterFirmwareRevisionNumber /** * Characteristic "Matter Firmware Update Status" */ export class MatterFirmwareUpdateStatus extends Characteristic { - - public static readonly UUID: string = "0000026E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000026E-0000-1000-8000-0026BB765291' constructor() { - super("Matter Firmware Update Status", MatterFirmwareUpdateStatus.UUID, { + super('Matter Firmware Update Status', MatterFirmwareUpdateStatus.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MatterFirmwareUpdateStatus = MatterFirmwareUpdateStatus; +Characteristic.MatterFirmwareUpdateStatus = MatterFirmwareUpdateStatus /** * Characteristic "Maximum Transmit Power" * @since iOS 14 */ export class MaximumTransmitPower extends Characteristic { - - public static readonly UUID: string = "00000243-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000243-0000-1000-8000-0026BB765291' constructor() { - super("Maximum Transmit Power", MaximumTransmitPower.UUID, { + super('Maximum Transmit Power', MaximumTransmitPower.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MaximumTransmitPower = MaximumTransmitPower; +Characteristic.MaximumTransmitPower = MaximumTransmitPower /** * Characteristic "Metrics Buffer Full State" */ export class MetricsBufferFullState extends Characteristic { - - public static readonly UUID: string = "00000272-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000272-0000-1000-8000-0026BB765291' constructor() { - super("Metrics Buffer Full State", MetricsBufferFullState.UUID, { + super('Metrics Buffer Full State', MetricsBufferFullState.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MetricsBufferFullState = MetricsBufferFullState; +Characteristic.MetricsBufferFullState = MetricsBufferFullState /** * Characteristic "Model" */ export class Model extends Characteristic { - - public static readonly UUID: string = "00000021-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000021-0000-1000-8000-0026BB765291' constructor() { - super("Model", Model.UUID, { + super('Model', Model.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], maxLen: 64, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Model = Model; +Characteristic.Model = Model /** * Characteristic "Motion Detected" */ export class MotionDetected extends Characteristic { - - public static readonly UUID: string = "00000022-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000022-0000-1000-8000-0026BB765291' constructor() { - super("Motion Detected", MotionDetected.UUID, { + super('Motion Detected', MotionDetected.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MotionDetected = MotionDetected; +Characteristic.MotionDetected = MotionDetected /** * Characteristic "Multifunction Button" */ export class MultifunctionButton extends Characteristic { - - public static readonly UUID: string = "0000026B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000026B-0000-1000-8000-0026BB765291' constructor() { - super("Multifunction Button", MultifunctionButton.UUID, { + super('Multifunction Button', MultifunctionButton.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.MultifunctionButton = MultifunctionButton; +Characteristic.MultifunctionButton = MultifunctionButton /** * Characteristic "Mute" */ export class Mute extends Characteristic { - - public static readonly UUID: string = "0000011A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011A-0000-1000-8000-0026BB765291' constructor() { - super("Mute", Mute.UUID, { + super('Mute', Mute.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Mute = Mute; +Characteristic.Mute = Mute /** * Characteristic "Name" */ export class Name extends Characteristic { - - public static readonly UUID: string = "00000023-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000023-0000-1000-8000-0026BB765291' constructor() { - super("Name", Name.UUID, { + super('Name', Name.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], maxLen: 64, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Name = Name; +Characteristic.Name = Name /** * Characteristic "Network Access Violation Control" */ export class NetworkAccessViolationControl extends Characteristic { - - public static readonly UUID: string = "0000021F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000021F-0000-1000-8000-0026BB765291' constructor() { - super("Network Access Violation Control", NetworkAccessViolationControl.UUID, { + super('Network Access Violation Control', NetworkAccessViolationControl.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NetworkAccessViolationControl = NetworkAccessViolationControl; +Characteristic.NetworkAccessViolationControl = NetworkAccessViolationControl /** * Characteristic "Network Client Profile Control" */ export class NetworkClientProfileControl extends Characteristic { - - public static readonly UUID: string = "0000020C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000020C-0000-1000-8000-0026BB765291' constructor() { - super("Network Client Profile Control", NetworkClientProfileControl.UUID, { + super('Network Client Profile Control', NetworkClientProfileControl.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NetworkClientProfileControl = NetworkClientProfileControl; +Characteristic.NetworkClientProfileControl = NetworkClientProfileControl /** * Characteristic "Network Client Status Control" */ export class NetworkClientStatusControl extends Characteristic { - - public static readonly UUID: string = "0000020D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000020D-0000-1000-8000-0026BB765291' constructor() { - super("Network Client Status Control", NetworkClientStatusControl.UUID, { + super('Network Client Status Control', NetworkClientStatusControl.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NetworkClientStatusControl = NetworkClientStatusControl; +Characteristic.NetworkClientStatusControl = NetworkClientStatusControl /** * Characteristic "NFC Access Control Point" * @since iOS 15 */ export class NFCAccessControlPoint extends Characteristic { - - public static readonly UUID: string = "00000264-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000264-0000-1000-8000-0026BB765291' constructor() { - super("NFC Access Control Point", NFCAccessControlPoint.UUID, { + super('NFC Access Control Point', NFCAccessControlPoint.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NFCAccessControlPoint = NFCAccessControlPoint; +Characteristic.NFCAccessControlPoint = NFCAccessControlPoint /** * Characteristic "NFC Access Supported Configuration" * @since iOS 15 */ export class NFCAccessSupportedConfiguration extends Characteristic { - - public static readonly UUID: string = "00000265-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000265-0000-1000-8000-0026BB765291' constructor() { - super("NFC Access Supported Configuration", NFCAccessSupportedConfiguration.UUID, { + super('NFC Access Supported Configuration', NFCAccessSupportedConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NFCAccessSupportedConfiguration = NFCAccessSupportedConfiguration; +Characteristic.NFCAccessSupportedConfiguration = NFCAccessSupportedConfiguration /** * Characteristic "Night Vision" */ export class NightVision extends Characteristic { - - public static readonly UUID: string = "0000011B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011B-0000-1000-8000-0026BB765291' constructor() { - super("Night Vision", NightVision.UUID, { + super('Night Vision', NightVision.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NightVision = NightVision; +Characteristic.NightVision = NightVision /** * Characteristic "Nitrogen Dioxide Density" */ export class NitrogenDioxideDensity extends Characteristic { - - public static readonly UUID: string = "000000C4-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C4-0000-1000-8000-0026BB765291' constructor() { - super("Nitrogen Dioxide Density", NitrogenDioxideDensity.UUID, { + super('Nitrogen Dioxide Density', NitrogenDioxideDensity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.NitrogenDioxideDensity = NitrogenDioxideDensity; +Characteristic.NitrogenDioxideDensity = NitrogenDioxideDensity /** * Characteristic "Obstruction Detected" */ export class ObstructionDetected extends Characteristic { - - public static readonly UUID: string = "00000024-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000024-0000-1000-8000-0026BB765291' constructor() { - super("Obstruction Detected", ObstructionDetected.UUID, { + super('Obstruction Detected', ObstructionDetected.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ObstructionDetected = ObstructionDetected; +Characteristic.ObstructionDetected = ObstructionDetected /** * Characteristic "Occupancy Detected" */ export class OccupancyDetected extends Characteristic { + public static readonly UUID: string = '00000071-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000071-0000-1000-8000-0026BB765291"; - - public static readonly OCCUPANCY_NOT_DETECTED = 0; - public static readonly OCCUPANCY_DETECTED = 1; + public static readonly OCCUPANCY_NOT_DETECTED = 0 + public static readonly OCCUPANCY_DETECTED = 1 constructor() { - super("Occupancy Detected", OccupancyDetected.UUID, { + super('Occupancy Detected', OccupancyDetected.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.OccupancyDetected = OccupancyDetected; +Characteristic.OccupancyDetected = OccupancyDetected /** * Characteristic "On" */ export class On extends Characteristic { - - public static readonly UUID: string = "00000025-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000025-0000-1000-8000-0026BB765291' constructor() { - super("On", On.UUID, { + super('On', On.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.On = On; +Characteristic.On = On /** * Characteristic "Operating State Response" * @since iOS 14 */ export class OperatingStateResponse extends Characteristic { - - public static readonly UUID: string = "00000232-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000232-0000-1000-8000-0026BB765291' constructor() { - super("Operating State Response", OperatingStateResponse.UUID, { + super('Operating State Response', OperatingStateResponse.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.OperatingStateResponse = OperatingStateResponse; +Characteristic.OperatingStateResponse = OperatingStateResponse /** * Characteristic "Optical Zoom" */ export class OpticalZoom extends Characteristic { - - public static readonly UUID: string = "0000011C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000011C-0000-1000-8000-0026BB765291' constructor() { - super("Optical Zoom", OpticalZoom.UUID, { + super('Optical Zoom', OpticalZoom.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.OpticalZoom = OpticalZoom; +Characteristic.OpticalZoom = OpticalZoom /** * Characteristic "Outlet In Use" */ export class OutletInUse extends Characteristic { - - public static readonly UUID: string = "00000026-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000026-0000-1000-8000-0026BB765291' constructor() { - super("Outlet In Use", OutletInUse.UUID, { + super('Outlet In Use', OutletInUse.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.OutletInUse = OutletInUse; +Characteristic.OutletInUse = OutletInUse /** * Characteristic "Ozone Density" */ export class OzoneDensity extends Characteristic { - - public static readonly UUID: string = "000000C3-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C3-0000-1000-8000-0026BB765291' constructor() { - super("Ozone Density", OzoneDensity.UUID, { + super('Ozone Density', OzoneDensity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.OzoneDensity = OzoneDensity; +Characteristic.OzoneDensity = OzoneDensity /** * Characteristic "Pairing Features" */ export class PairingFeatures extends Characteristic { - - public static readonly UUID: string = "0000004F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000004F-0000-1000-8000-0026BB765291' constructor() { - super("Pairing Features", PairingFeatures.UUID, { + super('Pairing Features', PairingFeatures.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PairingFeatures = PairingFeatures; +Characteristic.PairingFeatures = PairingFeatures /** * Characteristic "Pair Setup" */ export class PairSetup extends Characteristic { - - public static readonly UUID: string = "0000004C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000004C-0000-1000-8000-0026BB765291' constructor() { - super("Pair Setup", PairSetup.UUID, { + super('Pair Setup', PairSetup.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PairSetup = PairSetup; +Characteristic.PairSetup = PairSetup /** * Characteristic "Pair Verify" */ export class PairVerify extends Characteristic { - - public static readonly UUID: string = "0000004E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000004E-0000-1000-8000-0026BB765291' constructor() { - super("Pair Verify", PairVerify.UUID, { + super('Pair Verify', PairVerify.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PairVerify = PairVerify; +Characteristic.PairVerify = PairVerify /** * Characteristic "Password Setting" */ export class PasswordSetting extends Characteristic { - - public static readonly UUID: string = "000000E4-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000E4-0000-1000-8000-0026BB765291' constructor() { - super("Password Setting", PasswordSetting.UUID, { + super('Password Setting', PasswordSetting.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PasswordSetting = PasswordSetting; +Characteristic.PasswordSetting = PasswordSetting /** * Characteristic "Periodic Snapshots Active" */ export class PeriodicSnapshotsActive extends Characteristic { - - public static readonly UUID: string = "00000225-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000225-0000-1000-8000-0026BB765291' constructor() { - super("Periodic Snapshots Active", PeriodicSnapshotsActive.UUID, { + super('Periodic Snapshots Active', PeriodicSnapshotsActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PeriodicSnapshotsActive = PeriodicSnapshotsActive; +Characteristic.PeriodicSnapshotsActive = PeriodicSnapshotsActive /** * Characteristic "Picture Mode" */ export class PictureMode extends Characteristic { + public static readonly UUID: string = '000000E2-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000E2-0000-1000-8000-0026BB765291"; - - public static readonly OTHER = 0; - public static readonly STANDARD = 1; - public static readonly CALIBRATED = 2; - public static readonly CALIBRATED_DARK = 3; - public static readonly VIVID = 4; - public static readonly GAME = 5; - public static readonly COMPUTER = 6; - public static readonly CUSTOM = 7; + public static readonly OTHER = 0 + public static readonly STANDARD = 1 + public static readonly CALIBRATED = 2 + public static readonly CALIBRATED_DARK = 3 + public static readonly VIVID = 4 + public static readonly GAME = 5 + public static readonly COMPUTER = 6 + public static readonly CUSTOM = 7 constructor() { - super("Picture Mode", PictureMode.UUID, { + super('Picture Mode', PictureMode.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 13, minStep: 1, validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PictureMode = PictureMode; +Characteristic.PictureMode = PictureMode /** * Characteristic "Ping" * @since iOS 14 */ export class Ping extends Characteristic { - - public static readonly UUID: string = "0000023C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023C-0000-1000-8000-0026BB765291' constructor() { - super("Ping", Ping.UUID, { + super('Ping', Ping.UUID, { format: Formats.DATA, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Ping = Ping; +Characteristic.Ping = Ping /** * Characteristic "PM10 Density" */ export class PM10Density extends Characteristic { - - public static readonly UUID: string = "000000C7-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C7-0000-1000-8000-0026BB765291' constructor() { - super("PM10 Density", PM10Density.UUID, { + super('PM10 Density', PM10Density.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PM10Density = PM10Density; +Characteristic.PM10Density = PM10Density /** * Characteristic "PM2.5 Density" */ export class PM2_5Density extends Characteristic { - - public static readonly UUID: string = "000000C6-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C6-0000-1000-8000-0026BB765291' constructor() { - super("PM2.5 Density", PM2_5Density.UUID, { + super('PM2.5 Density', PM2_5Density.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PM2_5Density = PM2_5Density; +Characteristic.PM2_5Density = PM2_5Density /** * Characteristic "Position State" */ export class PositionState extends Characteristic { + public static readonly UUID: string = '00000072-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000072-0000-1000-8000-0026BB765291"; - - public static readonly DECREASING = 0; - public static readonly INCREASING = 1; - public static readonly STOPPED = 2; + public static readonly DECREASING = 0 + public static readonly INCREASING = 1 + public static readonly STOPPED = 2 constructor() { - super("Position State", PositionState.UUID, { + super('Position State', PositionState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PositionState = PositionState; +Characteristic.PositionState = PositionState /** * Characteristic "Power Mode Selection" */ export class PowerModeSelection extends Characteristic { + public static readonly UUID: string = '000000DF-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000DF-0000-1000-8000-0026BB765291"; - - public static readonly SHOW = 0; - public static readonly HIDE = 1; + public static readonly SHOW = 0 + public static readonly HIDE = 1 constructor() { - super("Power Mode Selection", PowerModeSelection.UUID, { + super('Power Mode Selection', PowerModeSelection.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.PowerModeSelection = PowerModeSelection; +Characteristic.PowerModeSelection = PowerModeSelection /** * Characteristic "Product Data" */ export class ProductData extends Characteristic { - - public static readonly UUID: string = "00000220-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000220-0000-1000-8000-0026BB765291' constructor() { - super("Product Data", ProductData.UUID, { + super('Product Data', ProductData.UUID, { format: Formats.DATA, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ProductData = ProductData; +Characteristic.ProductData = ProductData /** * Characteristic "Programmable Switch Event" */ export class ProgrammableSwitchEvent extends Characteristic { + public static readonly UUID: string = '00000073-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000073-0000-1000-8000-0026BB765291"; - - public static readonly SINGLE_PRESS = 0; - public static readonly DOUBLE_PRESS = 1; - public static readonly LONG_PRESS = 2; + public static readonly SINGLE_PRESS = 0 + public static readonly DOUBLE_PRESS = 1 + public static readonly LONG_PRESS = 2 constructor() { - super("Programmable Switch Event", ProgrammableSwitchEvent.UUID, { + super('Programmable Switch Event', ProgrammableSwitchEvent.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ProgrammableSwitchEvent = ProgrammableSwitchEvent; +Characteristic.ProgrammableSwitchEvent = ProgrammableSwitchEvent /** * Characteristic "Programmable Switch Output State" */ export class ProgrammableSwitchOutputState extends Characteristic { - - public static readonly UUID: string = "00000074-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000074-0000-1000-8000-0026BB765291' constructor() { - super("Programmable Switch Output State", ProgrammableSwitchOutputState.UUID, { + super('Programmable Switch Output State', ProgrammableSwitchOutputState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ProgrammableSwitchOutputState = ProgrammableSwitchOutputState; +Characteristic.ProgrammableSwitchOutputState = ProgrammableSwitchOutputState /** * Characteristic "Program Mode" */ export class ProgramMode extends Characteristic { + public static readonly UUID: string = '000000D1-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000D1-0000-1000-8000-0026BB765291"; - - public static readonly NO_PROGRAM_SCHEDULED = 0; - public static readonly PROGRAM_SCHEDULED = 1; - public static readonly PROGRAM_SCHEDULED_MANUAL_MODE = 2; // manually edited to remove final _ + public static readonly NO_PROGRAM_SCHEDULED = 0 + public static readonly PROGRAM_SCHEDULED = 1 + public static readonly PROGRAM_SCHEDULED_MANUAL_MODE = 2 constructor() { - super("Program Mode", ProgramMode.UUID, { + super('Program Mode', ProgramMode.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ProgramMode = ProgramMode; +Characteristic.ProgramMode = ProgramMode /** * Characteristic "Received Signal Strength Indication" * @since iOS 14 */ export class ReceivedSignalStrengthIndication extends Characteristic { - - public static readonly UUID: string = "0000023F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023F-0000-1000-8000-0026BB765291' constructor() { - super("Received Signal Strength Indication", ReceivedSignalStrengthIndication.UUID, { + super('Received Signal Strength Indication', ReceivedSignalStrengthIndication.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ReceivedSignalStrengthIndication = ReceivedSignalStrengthIndication; +Characteristic.ReceivedSignalStrengthIndication = ReceivedSignalStrengthIndication /** * Characteristic "Receiver Sensitivity" * @since iOS 14 */ export class ReceiverSensitivity extends Characteristic { - - public static readonly UUID: string = "00000244-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000244-0000-1000-8000-0026BB765291' constructor() { - super("Receiver Sensitivity", ReceiverSensitivity.UUID, { + super('Receiver Sensitivity', ReceiverSensitivity.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ReceiverSensitivity = ReceiverSensitivity; +Characteristic.ReceiverSensitivity = ReceiverSensitivity /** * Characteristic "Recording Audio Active" */ export class RecordingAudioActive extends Characteristic { + public static readonly UUID: string = '00000226-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000226-0000-1000-8000-0026BB765291"; - - public static readonly DISABLE = 0; - public static readonly ENABLE = 1; + public static readonly DISABLE = 0 + public static readonly ENABLE = 1 constructor() { - super("Recording Audio Active", RecordingAudioActive.UUID, { + super('Recording Audio Active', RecordingAudioActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE], validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RecordingAudioActive = RecordingAudioActive; +Characteristic.RecordingAudioActive = RecordingAudioActive /** * Characteristic "Relative Humidity Dehumidifier Threshold" */ export class RelativeHumidityDehumidifierThreshold extends Characteristic { - - public static readonly UUID: string = "000000C9-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C9-0000-1000-8000-0026BB765291' constructor() { - super("Relative Humidity Dehumidifier Threshold", RelativeHumidityDehumidifierThreshold.UUID, { + super('Relative Humidity Dehumidifier Threshold', RelativeHumidityDehumidifierThreshold.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RelativeHumidityDehumidifierThreshold = RelativeHumidityDehumidifierThreshold; +Characteristic.RelativeHumidityDehumidifierThreshold = RelativeHumidityDehumidifierThreshold /** * Characteristic "Relative Humidity Humidifier Threshold" */ export class RelativeHumidityHumidifierThreshold extends Characteristic { - - public static readonly UUID: string = "000000CA-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000CA-0000-1000-8000-0026BB765291' constructor() { - super("Relative Humidity Humidifier Threshold", RelativeHumidityHumidifierThreshold.UUID, { + super('Relative Humidity Humidifier Threshold', RelativeHumidityHumidifierThreshold.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RelativeHumidityHumidifierThreshold = RelativeHumidityHumidifierThreshold; +Characteristic.RelativeHumidityHumidifierThreshold = RelativeHumidityHumidifierThreshold /** * Characteristic "Remaining Duration" */ export class RemainingDuration extends Characteristic { - - public static readonly UUID: string = "000000D4-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D4-0000-1000-8000-0026BB765291' constructor() { - super("Remaining Duration", RemainingDuration.UUID, { + super('Remaining Duration', RemainingDuration.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.SECONDS, minValue: 0, maxValue: 3600, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RemainingDuration = RemainingDuration; +Characteristic.RemainingDuration = RemainingDuration /** * Characteristic "Remote Key" */ export class RemoteKey extends Characteristic { - - public static readonly UUID: string = "000000E1-0000-1000-8000-0026BB765291"; - - public static readonly REWIND = 0; - public static readonly FAST_FORWARD = 1; - public static readonly NEXT_TRACK = 2; - public static readonly PREVIOUS_TRACK = 3; - public static readonly ARROW_UP = 4; - public static readonly ARROW_DOWN = 5; - public static readonly ARROW_LEFT = 6; - public static readonly ARROW_RIGHT = 7; - public static readonly SELECT = 8; - public static readonly BACK = 9; - public static readonly EXIT = 10; - public static readonly PLAY_PAUSE = 11; - public static readonly INFORMATION = 15; - - constructor() { - super("Remote Key", RemoteKey.UUID, { + public static readonly UUID: string = '000000E1-0000-1000-8000-0026BB765291' + + public static readonly REWIND = 0 + public static readonly FAST_FORWARD = 1 + public static readonly NEXT_TRACK = 2 + public static readonly PREVIOUS_TRACK = 3 + public static readonly ARROW_UP = 4 + public static readonly ARROW_DOWN = 5 + public static readonly ARROW_LEFT = 6 + public static readonly ARROW_RIGHT = 7 + public static readonly SELECT = 8 + public static readonly BACK = 9 + public static readonly EXIT = 10 + public static readonly PLAY_PAUSE = 11 + public static readonly INFORMATION = 15 + + constructor() { + super('Remote Key', RemoteKey.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_WRITE], minValue: 0, maxValue: 16, minStep: 1, validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RemoteKey = RemoteKey; +Characteristic.RemoteKey = RemoteKey /** * Characteristic "Reset Filter Indication" */ export class ResetFilterIndication extends Characteristic { - - public static readonly UUID: string = "000000AD-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000AD-0000-1000-8000-0026BB765291' constructor() { - super("Reset Filter Indication", ResetFilterIndication.UUID, { + super('Reset Filter Indication', ResetFilterIndication.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_WRITE], minValue: 1, maxValue: 1, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ResetFilterIndication = ResetFilterIndication; +Characteristic.ResetFilterIndication = ResetFilterIndication /** * Characteristic "Rotation Direction" */ export class RotationDirection extends Characteristic { + public static readonly UUID: string = '00000028-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000028-0000-1000-8000-0026BB765291"; - - public static readonly CLOCKWISE = 0; - public static readonly COUNTER_CLOCKWISE = 1; + public static readonly CLOCKWISE = 0 + public static readonly COUNTER_CLOCKWISE = 1 constructor() { - super("Rotation Direction", RotationDirection.UUID, { + super('Rotation Direction', RotationDirection.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RotationDirection = RotationDirection; +Characteristic.RotationDirection = RotationDirection /** * Characteristic "Rotation Speed" */ export class RotationSpeed extends Characteristic { - - public static readonly UUID: string = "00000029-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000029-0000-1000-8000-0026BB765291' constructor() { - super("Rotation Speed", RotationSpeed.UUID, { + super('Rotation Speed', RotationSpeed.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RotationSpeed = RotationSpeed; +Characteristic.RotationSpeed = RotationSpeed /** * Characteristic "Router Status" */ export class RouterStatus extends Characteristic { + public static readonly UUID: string = '0000020E-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000020E-0000-1000-8000-0026BB765291"; - - public static readonly READY = 0; - public static readonly NOT_READY = 1; + public static readonly READY = 0 + public static readonly NOT_READY = 1 constructor() { - super("Router Status", RouterStatus.UUID, { + super('Router Status', RouterStatus.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.RouterStatus = RouterStatus; +Characteristic.RouterStatus = RouterStatus /** * Characteristic "Saturation" */ export class Saturation extends Characteristic { - - public static readonly UUID: string = "0000002F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000002F-0000-1000-8000-0026BB765291' constructor() { - super("Saturation", Saturation.UUID, { + super('Saturation', Saturation.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Saturation = Saturation; +Characteristic.Saturation = Saturation /** * Characteristic "Security System Alarm Type" */ export class SecuritySystemAlarmType extends Characteristic { - - public static readonly UUID: string = "0000008E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000008E-0000-1000-8000-0026BB765291' constructor() { - super("Security System Alarm Type", SecuritySystemAlarmType.UUID, { + super('Security System Alarm Type', SecuritySystemAlarmType.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SecuritySystemAlarmType = SecuritySystemAlarmType; +Characteristic.SecuritySystemAlarmType = SecuritySystemAlarmType /** * Characteristic "Security System Current State" */ export class SecuritySystemCurrentState extends Characteristic { + public static readonly UUID: string = '00000066-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000066-0000-1000-8000-0026BB765291"; - - public static readonly STAY_ARM = 0; - public static readonly AWAY_ARM = 1; - public static readonly NIGHT_ARM = 2; - public static readonly DISARMED = 3; - public static readonly ALARM_TRIGGERED = 4; + public static readonly STAY_ARM = 0 + public static readonly AWAY_ARM = 1 + public static readonly NIGHT_ARM = 2 + public static readonly DISARMED = 3 + public static readonly ALARM_TRIGGERED = 4 constructor() { - super("Security System Current State", SecuritySystemCurrentState.UUID, { + super('Security System Current State', SecuritySystemCurrentState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 4, minStep: 1, validValues: [0, 1, 2, 3, 4], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SecuritySystemCurrentState = SecuritySystemCurrentState; +Characteristic.SecuritySystemCurrentState = SecuritySystemCurrentState /** * Characteristic "Security System Target State" */ export class SecuritySystemTargetState extends Characteristic { + public static readonly UUID: string = '00000067-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000067-0000-1000-8000-0026BB765291"; - - public static readonly STAY_ARM = 0; - public static readonly AWAY_ARM = 1; - public static readonly NIGHT_ARM = 2; - public static readonly DISARM = 3; + public static readonly STAY_ARM = 0 + public static readonly AWAY_ARM = 1 + public static readonly NIGHT_ARM = 2 + public static readonly DISARM = 3 constructor() { - super("Security System Target State", SecuritySystemTargetState.UUID, { + super('Security System Target State', SecuritySystemTargetState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SecuritySystemTargetState = SecuritySystemTargetState; +Characteristic.SecuritySystemTargetState = SecuritySystemTargetState /** * Characteristic "Selected Audio Stream Configuration" */ export class SelectedAudioStreamConfiguration extends Characteristic { - - public static readonly UUID: string = "00000128-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000128-0000-1000-8000-0026BB765291' constructor() { - super("Selected Audio Stream Configuration", SelectedAudioStreamConfiguration.UUID, { + super('Selected Audio Stream Configuration', SelectedAudioStreamConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SelectedAudioStreamConfiguration = SelectedAudioStreamConfiguration; +Characteristic.SelectedAudioStreamConfiguration = SelectedAudioStreamConfiguration /** * Characteristic "Selected Camera Recording Configuration" */ export class SelectedCameraRecordingConfiguration extends Characteristic { - - public static readonly UUID: string = "00000209-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000209-0000-1000-8000-0026BB765291' constructor() { - super("Selected Camera Recording Configuration", SelectedCameraRecordingConfiguration.UUID, { + super('Selected Camera Recording Configuration', SelectedCameraRecordingConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SelectedCameraRecordingConfiguration = SelectedCameraRecordingConfiguration; +Characteristic.SelectedCameraRecordingConfiguration = SelectedCameraRecordingConfiguration /** * Characteristic "Selected Diagnostics Modes" */ export class SelectedDiagnosticsModes extends Characteristic { - - public static readonly UUID: string = "0000024D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000024D-0000-1000-8000-0026BB765291' constructor() { - super("Selected Diagnostics Modes", SelectedDiagnosticsModes.UUID, { + super('Selected Diagnostics Modes', SelectedDiagnosticsModes.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SelectedDiagnosticsModes = SelectedDiagnosticsModes; +Characteristic.SelectedDiagnosticsModes = SelectedDiagnosticsModes /** * Characteristic "Selected RTP Stream Configuration" */ export class SelectedRTPStreamConfiguration extends Characteristic { - - public static readonly UUID: string = "00000117-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000117-0000-1000-8000-0026BB765291' constructor() { - super("Selected RTP Stream Configuration", SelectedRTPStreamConfiguration.UUID, { + super('Selected RTP Stream Configuration', SelectedRTPStreamConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SelectedRTPStreamConfiguration = SelectedRTPStreamConfiguration; +Characteristic.SelectedRTPStreamConfiguration = SelectedRTPStreamConfiguration /** * Characteristic "Selected Sleep Configuration" */ export class SelectedSleepConfiguration extends Characteristic { - - public static readonly UUID: string = "00000252-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000252-0000-1000-8000-0026BB765291' constructor() { - super("Selected Sleep Configuration", SelectedSleepConfiguration.UUID, { + super('Selected Sleep Configuration', SelectedSleepConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SelectedSleepConfiguration = SelectedSleepConfiguration; +Characteristic.SelectedSleepConfiguration = SelectedSleepConfiguration /** * Characteristic "Serial Number" */ export class SerialNumber extends Characteristic { - - public static readonly UUID: string = "00000030-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000030-0000-1000-8000-0026BB765291' constructor() { - super("Serial Number", SerialNumber.UUID, { + super('Serial Number', SerialNumber.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], maxLen: 64, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SerialNumber = SerialNumber; +Characteristic.SerialNumber = SerialNumber /** * Characteristic "Service Label Index" */ export class ServiceLabelIndex extends Characteristic { - - public static readonly UUID: string = "000000CB-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000CB-0000-1000-8000-0026BB765291' constructor() { - super("Service Label Index", ServiceLabelIndex.UUID, { + super('Service Label Index', ServiceLabelIndex.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], minValue: 1, maxValue: 255, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ServiceLabelIndex = ServiceLabelIndex; +Characteristic.ServiceLabelIndex = ServiceLabelIndex /** * Characteristic "Service Label Namespace" */ export class ServiceLabelNamespace extends Characteristic { + public static readonly UUID: string = '000000CD-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000CD-0000-1000-8000-0026BB765291"; - - public static readonly DOTS = 0; - public static readonly ARABIC_NUMERALS = 1; + public static readonly DOTS = 0 + public static readonly ARABIC_NUMERALS = 1 constructor() { - super("Service Label Namespace", ServiceLabelNamespace.UUID, { + super('Service Label Namespace', ServiceLabelNamespace.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ServiceLabelNamespace = ServiceLabelNamespace; +Characteristic.ServiceLabelNamespace = ServiceLabelNamespace /** * Characteristic "Set Duration" */ export class SetDuration extends Characteristic { - - public static readonly UUID: string = "000000D3-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D3-0000-1000-8000-0026BB765291' constructor() { - super("Set Duration", SetDuration.UUID, { + super('Set Duration', SetDuration.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.SECONDS, minValue: 0, maxValue: 3600, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SetDuration = SetDuration; +Characteristic.SetDuration = SetDuration /** * Characteristic "Setup Data Stream Transport" */ export class SetupDataStreamTransport extends Characteristic { - - public static readonly UUID: string = "00000131-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000131-0000-1000-8000-0026BB765291' constructor() { - super("Setup Data Stream Transport", SetupDataStreamTransport.UUID, { + super('Setup Data Stream Transport', SetupDataStreamTransport.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SetupDataStreamTransport = SetupDataStreamTransport; +Characteristic.SetupDataStreamTransport = SetupDataStreamTransport /** * Characteristic "Setup Endpoints" */ export class SetupEndpoints extends Characteristic { - - public static readonly UUID: string = "00000118-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000118-0000-1000-8000-0026BB765291' constructor() { - super("Setup Endpoints", SetupEndpoints.UUID, { + super('Setup Endpoints', SetupEndpoints.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SetupEndpoints = SetupEndpoints; +Characteristic.SetupEndpoints = SetupEndpoints /** * Characteristic "Setup Transfer Transport" * @since iOS 13.4 */ export class SetupTransferTransport extends Characteristic { - - public static readonly UUID: string = "00000201-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000201-0000-1000-8000-0026BB765291' constructor() { - super("Setup Transfer Transport", SetupTransferTransport.UUID, { + super('Setup Transfer Transport', SetupTransferTransport.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SetupTransferTransport = SetupTransferTransport; +Characteristic.SetupTransferTransport = SetupTransferTransport /** * Characteristic "Signal To Noise Ratio" * @since iOS 14 */ export class SignalToNoiseRatio extends Characteristic { - - public static readonly UUID: string = "00000241-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000241-0000-1000-8000-0026BB765291' constructor() { - super("Signal To Noise Ratio", SignalToNoiseRatio.UUID, { + super('Signal To Noise Ratio', SignalToNoiseRatio.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SignalToNoiseRatio = SignalToNoiseRatio; +Characteristic.SignalToNoiseRatio = SignalToNoiseRatio /** * Characteristic "Siri Enable" */ export class SiriEnable extends Characteristic { - - public static readonly UUID: string = "00000255-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000255-0000-1000-8000-0026BB765291' constructor() { - super("Siri Enable", SiriEnable.UUID, { + super('Siri Enable', SiriEnable.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriEnable = SiriEnable; +Characteristic.SiriEnable = SiriEnable /** * Characteristic "Siri Endpoint Session Status" */ export class SiriEndpointSessionStatus extends Characteristic { - - public static readonly UUID: string = "00000254-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000254-0000-1000-8000-0026BB765291' constructor() { - super("Siri Endpoint Session Status", SiriEndpointSessionStatus.UUID, { + super('Siri Endpoint Session Status', SiriEndpointSessionStatus.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriEndpointSessionStatus = SiriEndpointSessionStatus; +Characteristic.SiriEndpointSessionStatus = SiriEndpointSessionStatus /** * Characteristic "Siri Engine Version" */ export class SiriEngineVersion extends Characteristic { - - public static readonly UUID: string = "0000025A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000025A-0000-1000-8000-0026BB765291' constructor() { - super("Siri Engine Version", SiriEngineVersion.UUID, { + super('Siri Engine Version', SiriEngineVersion.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriEngineVersion = SiriEngineVersion; +Characteristic.SiriEngineVersion = SiriEngineVersion /** * Characteristic "Siri Input Type" */ export class SiriInputType extends Characteristic { + public static readonly UUID: string = '00000132-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000132-0000-1000-8000-0026BB765291"; - - public static readonly PUSH_BUTTON_TRIGGERED_APPLE_TV = 0; + public static readonly PUSH_BUTTON_TRIGGERED_APPLE_TV = 0 constructor() { - super("Siri Input Type", SiriInputType.UUID, { + super('Siri Input Type', SiriInputType.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], minValue: 0, maxValue: 0, validValues: [0], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriInputType = SiriInputType; +Characteristic.SiriInputType = SiriInputType /** * Characteristic "Siri Light On Use" */ export class SiriLightOnUse extends Characteristic { - - public static readonly UUID: string = "00000258-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000258-0000-1000-8000-0026BB765291' constructor() { - super("Siri Light On Use", SiriLightOnUse.UUID, { + super('Siri Light On Use', SiriLightOnUse.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriLightOnUse = SiriLightOnUse; +Characteristic.SiriLightOnUse = SiriLightOnUse /** * Characteristic "Siri Listening" */ export class SiriListening extends Characteristic { - - public static readonly UUID: string = "00000256-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000256-0000-1000-8000-0026BB765291' constructor() { - super("Siri Listening", SiriListening.UUID, { + super('Siri Listening', SiriListening.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriListening = SiriListening; +Characteristic.SiriListening = SiriListening /** * Characteristic "Siri Touch To Use" */ export class SiriTouchToUse extends Characteristic { - - public static readonly UUID: string = "00000257-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000257-0000-1000-8000-0026BB765291' constructor() { - super("Siri Touch To Use", SiriTouchToUse.UUID, { + super('Siri Touch To Use', SiriTouchToUse.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SiriTouchToUse = SiriTouchToUse; +Characteristic.SiriTouchToUse = SiriTouchToUse /** * Characteristic "Slat Type" */ export class SlatType extends Characteristic { + public static readonly UUID: string = '000000C0-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000C0-0000-1000-8000-0026BB765291"; - - public static readonly HORIZONTAL = 0; - public static readonly VERTICAL = 1; + public static readonly HORIZONTAL = 0 + public static readonly VERTICAL = 1 constructor() { - super("Slat Type", SlatType.UUID, { + super('Slat Type', SlatType.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SlatType = SlatType; +Characteristic.SlatType = SlatType /** * Characteristic "Sleep Discovery Mode" */ export class SleepDiscoveryMode extends Characteristic { + public static readonly UUID: string = '000000E8-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000E8-0000-1000-8000-0026BB765291"; - - public static readonly NOT_DISCOVERABLE = 0; - public static readonly ALWAYS_DISCOVERABLE = 1; + public static readonly NOT_DISCOVERABLE = 0 + public static readonly ALWAYS_DISCOVERABLE = 1 constructor() { - super("Sleep Discovery Mode", SleepDiscoveryMode.UUID, { + super('Sleep Discovery Mode', SleepDiscoveryMode.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SleepDiscoveryMode = SleepDiscoveryMode; +Characteristic.SleepDiscoveryMode = SleepDiscoveryMode /** * Characteristic "Sleep Interval" * @since iOS 14 */ export class SleepInterval extends Characteristic { - - public static readonly UUID: string = "0000023A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000023A-0000-1000-8000-0026BB765291' constructor() { - super("Sleep Interval", SleepInterval.UUID, { + super('Sleep Interval', SleepInterval.UUID, { format: Formats.UINT32, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SleepInterval = SleepInterval; +Characteristic.SleepInterval = SleepInterval /** * Characteristic "Smoke Detected" */ export class SmokeDetected extends Characteristic { + public static readonly UUID: string = '00000076-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000076-0000-1000-8000-0026BB765291"; - - public static readonly SMOKE_NOT_DETECTED = 0; - public static readonly SMOKE_DETECTED = 1; + public static readonly SMOKE_NOT_DETECTED = 0 + public static readonly SMOKE_DETECTED = 1 constructor() { - super("Smoke Detected", SmokeDetected.UUID, { + super('Smoke Detected', SmokeDetected.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SmokeDetected = SmokeDetected; +Characteristic.SmokeDetected = SmokeDetected /** * Characteristic "Software Revision" */ export class SoftwareRevision extends Characteristic { - - public static readonly UUID: string = "00000054-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000054-0000-1000-8000-0026BB765291' constructor() { - super("Software Revision", SoftwareRevision.UUID, { + super('Software Revision', SoftwareRevision.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SoftwareRevision = SoftwareRevision; +Characteristic.SoftwareRevision = SoftwareRevision /** * Characteristic "Staged Firmware Version" */ export class StagedFirmwareVersion extends Characteristic { - - public static readonly UUID: string = "00000249-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000249-0000-1000-8000-0026BB765291' constructor() { - super("Staged Firmware Version", StagedFirmwareVersion.UUID, { + super('Staged Firmware Version', StagedFirmwareVersion.UUID, { format: Formats.STRING, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StagedFirmwareVersion = StagedFirmwareVersion; +Characteristic.StagedFirmwareVersion = StagedFirmwareVersion /** * Characteristic "Status Active" */ export class StatusActive extends Characteristic { - - public static readonly UUID: string = "00000075-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000075-0000-1000-8000-0026BB765291' constructor() { - super("Status Active", StatusActive.UUID, { + super('Status Active', StatusActive.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StatusActive = StatusActive; +Characteristic.StatusActive = StatusActive /** * Characteristic "Status Fault" */ export class StatusFault extends Characteristic { + public static readonly UUID: string = '00000077-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000077-0000-1000-8000-0026BB765291"; - - public static readonly NO_FAULT = 0; - public static readonly GENERAL_FAULT = 1; + public static readonly NO_FAULT = 0 + public static readonly GENERAL_FAULT = 1 constructor() { - super("Status Fault", StatusFault.UUID, { + super('Status Fault', StatusFault.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StatusFault = StatusFault; +Characteristic.StatusFault = StatusFault /** * Characteristic "Status Jammed" */ export class StatusJammed extends Characteristic { + public static readonly UUID: string = '00000078-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000078-0000-1000-8000-0026BB765291"; - - public static readonly NOT_JAMMED = 0; - public static readonly JAMMED = 1; + public static readonly NOT_JAMMED = 0 + public static readonly JAMMED = 1 constructor() { - super("Status Jammed", StatusJammed.UUID, { + super('Status Jammed', StatusJammed.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StatusJammed = StatusJammed; +Characteristic.StatusJammed = StatusJammed /** * Characteristic "Status Low Battery" */ export class StatusLowBattery extends Characteristic { + public static readonly UUID: string = '00000079-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000079-0000-1000-8000-0026BB765291"; - - public static readonly BATTERY_LEVEL_NORMAL = 0; - public static readonly BATTERY_LEVEL_LOW = 1; + public static readonly BATTERY_LEVEL_NORMAL = 0 + public static readonly BATTERY_LEVEL_LOW = 1 constructor() { - super("Status Low Battery", StatusLowBattery.UUID, { + super('Status Low Battery', StatusLowBattery.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StatusLowBattery = StatusLowBattery; +Characteristic.StatusLowBattery = StatusLowBattery /** * Characteristic "Status Tampered" */ export class StatusTampered extends Characteristic { + public static readonly UUID: string = '0000007A-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000007A-0000-1000-8000-0026BB765291"; - - public static readonly NOT_TAMPERED = 0; - public static readonly TAMPERED = 1; + public static readonly NOT_TAMPERED = 0 + public static readonly TAMPERED = 1 constructor() { - super("Status Tampered", StatusTampered.UUID, { + super('Status Tampered', StatusTampered.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StatusTampered = StatusTampered; +Characteristic.StatusTampered = StatusTampered /** * Characteristic "Streaming Status" */ export class StreamingStatus extends Characteristic { - - public static readonly UUID: string = "00000120-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000120-0000-1000-8000-0026BB765291' constructor() { - super("Streaming Status", StreamingStatus.UUID, { + super('Streaming Status', StreamingStatus.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.StreamingStatus = StreamingStatus; +Characteristic.StreamingStatus = StreamingStatus /** * Characteristic "Sulphur Dioxide Density" */ export class SulphurDioxideDensity extends Characteristic { - - public static readonly UUID: string = "000000C5-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C5-0000-1000-8000-0026BB765291' constructor() { - super("Sulphur Dioxide Density", SulphurDioxideDensity.UUID, { + super('Sulphur Dioxide Density', SulphurDioxideDensity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SulphurDioxideDensity = SulphurDioxideDensity; +Characteristic.SulphurDioxideDensity = SulphurDioxideDensity /** * Characteristic "Supported Asset Types" */ export class SupportedAssetTypes extends Characteristic { - - public static readonly UUID: string = "00000268-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000268-0000-1000-8000-0026BB765291' constructor() { - super("Supported Asset Types", SupportedAssetTypes.UUID, { + super('Supported Asset Types', SupportedAssetTypes.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedAssetTypes = SupportedAssetTypes; +Characteristic.SupportedAssetTypes = SupportedAssetTypes /** * Characteristic "Supported Audio Recording Configuration" */ export class SupportedAudioRecordingConfiguration extends Characteristic { - - public static readonly UUID: string = "00000207-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000207-0000-1000-8000-0026BB765291' constructor() { - super("Supported Audio Recording Configuration", SupportedAudioRecordingConfiguration.UUID, { + super('Supported Audio Recording Configuration', SupportedAudioRecordingConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedAudioRecordingConfiguration = SupportedAudioRecordingConfiguration; +Characteristic.SupportedAudioRecordingConfiguration = SupportedAudioRecordingConfiguration /** * Characteristic "Supported Audio Stream Configuration" */ export class SupportedAudioStreamConfiguration extends Characteristic { - - public static readonly UUID: string = "00000115-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000115-0000-1000-8000-0026BB765291' constructor() { - super("Supported Audio Stream Configuration", SupportedAudioStreamConfiguration.UUID, { + super('Supported Audio Stream Configuration', SupportedAudioStreamConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedAudioStreamConfiguration = SupportedAudioStreamConfiguration; +Characteristic.SupportedAudioStreamConfiguration = SupportedAudioStreamConfiguration /** * Characteristic "Supported Camera Recording Configuration" */ export class SupportedCameraRecordingConfiguration extends Characteristic { - - public static readonly UUID: string = "00000205-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000205-0000-1000-8000-0026BB765291' constructor() { - super("Supported Camera Recording Configuration", SupportedCameraRecordingConfiguration.UUID, { + super('Supported Camera Recording Configuration', SupportedCameraRecordingConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedCameraRecordingConfiguration = SupportedCameraRecordingConfiguration; +Characteristic.SupportedCameraRecordingConfiguration = SupportedCameraRecordingConfiguration /** * Characteristic "Supported Characteristic Value Transition Configuration" * @since iOS 14 */ export class SupportedCharacteristicValueTransitionConfiguration extends Characteristic { - - public static readonly UUID: string = "00000144-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000144-0000-1000-8000-0026BB765291' constructor() { - super("Supported Characteristic Value Transition Configuration", SupportedCharacteristicValueTransitionConfiguration.UUID, { + super('Supported Characteristic Value Transition Configuration', SupportedCharacteristicValueTransitionConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedCharacteristicValueTransitionConfiguration = SupportedCharacteristicValueTransitionConfiguration; +Characteristic.SupportedCharacteristicValueTransitionConfiguration = SupportedCharacteristicValueTransitionConfiguration /** * Characteristic "Supported Data Stream Transport Configuration" */ export class SupportedDataStreamTransportConfiguration extends Characteristic { - - public static readonly UUID: string = "00000130-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000130-0000-1000-8000-0026BB765291' constructor() { - super("Supported Data Stream Transport Configuration", SupportedDataStreamTransportConfiguration.UUID, { + super('Supported Data Stream Transport Configuration', SupportedDataStreamTransportConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedDataStreamTransportConfiguration = SupportedDataStreamTransportConfiguration; +Characteristic.SupportedDataStreamTransportConfiguration = SupportedDataStreamTransportConfiguration /** * Characteristic "Supported Diagnostics Modes" */ export class SupportedDiagnosticsModes extends Characteristic { - - public static readonly UUID: string = "0000024C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000024C-0000-1000-8000-0026BB765291' constructor() { - super("Supported Diagnostics Modes", SupportedDiagnosticsModes.UUID, { + super('Supported Diagnostics Modes', SupportedDiagnosticsModes.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedDiagnosticsModes = SupportedDiagnosticsModes; +Characteristic.SupportedDiagnosticsModes = SupportedDiagnosticsModes /** * Characteristic "Supported Diagnostics Snapshot" * @since iOS 14 */ export class SupportedDiagnosticsSnapshot extends Characteristic { - - public static readonly UUID: string = "00000238-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000238-0000-1000-8000-0026BB765291' constructor() { - super("Supported Diagnostics Snapshot", SupportedDiagnosticsSnapshot.UUID, { + super('Supported Diagnostics Snapshot', SupportedDiagnosticsSnapshot.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedDiagnosticsSnapshot = SupportedDiagnosticsSnapshot; +Characteristic.SupportedDiagnosticsSnapshot = SupportedDiagnosticsSnapshot /** * Characteristic "Supported Firmware Update Configuration" */ export class SupportedFirmwareUpdateConfiguration extends Characteristic { - - public static readonly UUID: string = "00000233-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000233-0000-1000-8000-0026BB765291' constructor() { - super("Supported Firmware Update Configuration", SupportedFirmwareUpdateConfiguration.UUID, { + super('Supported Firmware Update Configuration', SupportedFirmwareUpdateConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedFirmwareUpdateConfiguration = SupportedFirmwareUpdateConfiguration; +Characteristic.SupportedFirmwareUpdateConfiguration = SupportedFirmwareUpdateConfiguration /** * Characteristic "Supported Metrics" */ export class SupportedMetrics extends Characteristic { - - public static readonly UUID: string = "00000271-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000271-0000-1000-8000-0026BB765291' constructor() { - super("Supported Metrics", SupportedMetrics.UUID, { + super('Supported Metrics', SupportedMetrics.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedMetrics = SupportedMetrics; +Characteristic.SupportedMetrics = SupportedMetrics /** * Characteristic "Supported Router Configuration" */ export class SupportedRouterConfiguration extends Characteristic { - - public static readonly UUID: string = "00000210-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000210-0000-1000-8000-0026BB765291' constructor() { - super("Supported Router Configuration", SupportedRouterConfiguration.UUID, { + super('Supported Router Configuration', SupportedRouterConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedRouterConfiguration = SupportedRouterConfiguration; +Characteristic.SupportedRouterConfiguration = SupportedRouterConfiguration /** * Characteristic "Supported RTP Configuration" */ export class SupportedRTPConfiguration extends Characteristic { - - public static readonly UUID: string = "00000116-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000116-0000-1000-8000-0026BB765291' constructor() { - super("Supported RTP Configuration", SupportedRTPConfiguration.UUID, { + super('Supported RTP Configuration', SupportedRTPConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedRTPConfiguration = SupportedRTPConfiguration; +Characteristic.SupportedRTPConfiguration = SupportedRTPConfiguration /** * Characteristic "Supported Sleep Configuration" */ export class SupportedSleepConfiguration extends Characteristic { - - public static readonly UUID: string = "00000251-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000251-0000-1000-8000-0026BB765291' constructor() { - super("Supported Sleep Configuration", SupportedSleepConfiguration.UUID, { + super('Supported Sleep Configuration', SupportedSleepConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedSleepConfiguration = SupportedSleepConfiguration; +Characteristic.SupportedSleepConfiguration = SupportedSleepConfiguration /** * Characteristic "Supported Transfer Transport Configuration" * @since iOS 13.4 */ export class SupportedTransferTransportConfiguration extends Characteristic { - - public static readonly UUID: string = "00000202-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000202-0000-1000-8000-0026BB765291' constructor() { - super("Supported Transfer Transport Configuration", SupportedTransferTransportConfiguration.UUID, { + super('Supported Transfer Transport Configuration', SupportedTransferTransportConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedTransferTransportConfiguration = SupportedTransferTransportConfiguration; +Characteristic.SupportedTransferTransportConfiguration = SupportedTransferTransportConfiguration /** * Characteristic "Supported Video Recording Configuration" */ export class SupportedVideoRecordingConfiguration extends Characteristic { - - public static readonly UUID: string = "00000206-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000206-0000-1000-8000-0026BB765291' constructor() { - super("Supported Video Recording Configuration", SupportedVideoRecordingConfiguration.UUID, { + super('Supported Video Recording Configuration', SupportedVideoRecordingConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedVideoRecordingConfiguration = SupportedVideoRecordingConfiguration; +Characteristic.SupportedVideoRecordingConfiguration = SupportedVideoRecordingConfiguration /** * Characteristic "Supported Video Stream Configuration" */ export class SupportedVideoStreamConfiguration extends Characteristic { - - public static readonly UUID: string = "00000114-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000114-0000-1000-8000-0026BB765291' constructor() { - super("Supported Video Stream Configuration", SupportedVideoStreamConfiguration.UUID, { + super('Supported Video Stream Configuration', SupportedVideoStreamConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SupportedVideoStreamConfiguration = SupportedVideoStreamConfiguration; +Characteristic.SupportedVideoStreamConfiguration = SupportedVideoStreamConfiguration /** * Characteristic "Swing Mode" */ export class SwingMode extends Characteristic { + public static readonly UUID: string = '000000B6-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B6-0000-1000-8000-0026BB765291"; - - public static readonly SWING_DISABLED = 0; - public static readonly SWING_ENABLED = 1; + public static readonly SWING_DISABLED = 0 + public static readonly SWING_ENABLED = 1 constructor() { - super("Swing Mode", SwingMode.UUID, { + super('Swing Mode', SwingMode.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.SwingMode = SwingMode; +Characteristic.SwingMode = SwingMode /** * Characteristic "Tap Type" */ export class TapType extends Characteristic { - - public static readonly UUID: string = "0000022F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022F-0000-1000-8000-0026BB765291' constructor() { - super("Tap Type", TapType.UUID, { + super('Tap Type', TapType.UUID, { format: Formats.UINT16, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TapType = TapType; +Characteristic.TapType = TapType /** * Characteristic "Target Air Purifier State" */ export class TargetAirPurifierState extends Characteristic { + public static readonly UUID: string = '000000A8-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000A8-0000-1000-8000-0026BB765291"; - - public static readonly MANUAL = 0; - public static readonly AUTO = 1; + public static readonly MANUAL = 0 + public static readonly AUTO = 1 constructor() { - super("Target Air Purifier State", TargetAirPurifierState.UUID, { + super('Target Air Purifier State', TargetAirPurifierState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetAirPurifierState = TargetAirPurifierState; +Characteristic.TargetAirPurifierState = TargetAirPurifierState /** * Characteristic "Target Control List" */ export class TargetControlList extends Characteristic { - - public static readonly UUID: string = "00000124-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000124-0000-1000-8000-0026BB765291' constructor() { - super("Target Control List", TargetControlList.UUID, { + super('Target Control List', TargetControlList.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.WRITE_RESPONSE], adminOnlyAccess: [Access.READ, Access.WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetControlList = TargetControlList; +Characteristic.TargetControlList = TargetControlList /** * Characteristic "Target Control Supported Configuration" */ export class TargetControlSupportedConfiguration extends Characteristic { - - public static readonly UUID: string = "00000123-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000123-0000-1000-8000-0026BB765291' constructor() { - super("Target Control Supported Configuration", TargetControlSupportedConfiguration.UUID, { + super('Target Control Supported Configuration', TargetControlSupportedConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetControlSupportedConfiguration = TargetControlSupportedConfiguration; +Characteristic.TargetControlSupportedConfiguration = TargetControlSupportedConfiguration /** * Characteristic "Target Door State" */ export class TargetDoorState extends Characteristic { + public static readonly UUID: string = '00000032-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000032-0000-1000-8000-0026BB765291"; - - public static readonly OPEN = 0; - public static readonly CLOSED = 1; + public static readonly OPEN = 0 + public static readonly CLOSED = 1 constructor() { - super("Target Door State", TargetDoorState.UUID, { + super('Target Door State', TargetDoorState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetDoorState = TargetDoorState; +Characteristic.TargetDoorState = TargetDoorState /** * Characteristic "Target Fan State" */ export class TargetFanState extends Characteristic { + public static readonly UUID: string = '000000BF-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000BF-0000-1000-8000-0026BB765291"; - - public static readonly MANUAL = 0; - public static readonly AUTO = 1; + public static readonly MANUAL = 0 + public static readonly AUTO = 1 constructor() { - super("Target Fan State", TargetFanState.UUID, { + super('Target Fan State', TargetFanState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetFanState = TargetFanState; +Characteristic.TargetFanState = TargetFanState /** * Characteristic "Target Heater-Cooler State" */ export class TargetHeaterCoolerState extends Characteristic { + public static readonly UUID: string = '000000B2-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B2-0000-1000-8000-0026BB765291"; - - public static readonly AUTO = 0; - public static readonly HEAT = 1; - public static readonly COOL = 2; + public static readonly AUTO = 0 + public static readonly HEAT = 1 + public static readonly COOL = 2 constructor() { - super("Target Heater-Cooler State", TargetHeaterCoolerState.UUID, { + super('Target Heater-Cooler State', TargetHeaterCoolerState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetHeaterCoolerState = TargetHeaterCoolerState; +Characteristic.TargetHeaterCoolerState = TargetHeaterCoolerState /** * Characteristic "Target Heating Cooling State" */ export class TargetHeatingCoolingState extends Characteristic { + public static readonly UUID: string = '00000033-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000033-0000-1000-8000-0026BB765291"; - - public static readonly OFF = 0; - public static readonly HEAT = 1; - public static readonly COOL = 2; - public static readonly AUTO = 3; + public static readonly OFF = 0 + public static readonly HEAT = 1 + public static readonly COOL = 2 + public static readonly AUTO = 3 constructor() { - super("Target Heating Cooling State", TargetHeatingCoolingState.UUID, { + super('Target Heating Cooling State', TargetHeatingCoolingState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetHeatingCoolingState = TargetHeatingCoolingState; +Characteristic.TargetHeatingCoolingState = TargetHeatingCoolingState /** * Characteristic "Target Horizontal Tilt Angle" */ export class TargetHorizontalTiltAngle extends Characteristic { - - public static readonly UUID: string = "0000007B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000007B-0000-1000-8000-0026BB765291' constructor() { - super("Target Horizontal Tilt Angle", TargetHorizontalTiltAngle.UUID, { + super('Target Horizontal Tilt Angle', TargetHorizontalTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetHorizontalTiltAngle = TargetHorizontalTiltAngle; +Characteristic.TargetHorizontalTiltAngle = TargetHorizontalTiltAngle /** * Characteristic "Target Humidifier-Dehumidifier State" */ export class TargetHumidifierDehumidifierState extends Characteristic { + public static readonly UUID: string = '000000B4-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000B4-0000-1000-8000-0026BB765291"; - - public static readonly HUMIDIFIER_OR_DEHUMIDIFIER = 0; - public static readonly HUMIDIFIER = 1; - public static readonly DEHUMIDIFIER = 2; + public static readonly HUMIDIFIER_OR_DEHUMIDIFIER = 0 + public static readonly HUMIDIFIER = 1 + public static readonly DEHUMIDIFIER = 2 constructor() { - super("Target Humidifier-Dehumidifier State", TargetHumidifierDehumidifierState.UUID, { + super('Target Humidifier-Dehumidifier State', TargetHumidifierDehumidifierState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetHumidifierDehumidifierState = TargetHumidifierDehumidifierState; +Characteristic.TargetHumidifierDehumidifierState = TargetHumidifierDehumidifierState /** * Characteristic "Target Media State" */ export class TargetMediaState extends Characteristic { + public static readonly UUID: string = '00000137-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000137-0000-1000-8000-0026BB765291"; - - public static readonly PLAY = 0; - public static readonly PAUSE = 1; - public static readonly STOP = 2; + public static readonly PLAY = 0 + public static readonly PAUSE = 1 + public static readonly STOP = 2 constructor() { - super("Target Media State", TargetMediaState.UUID, { + super('Target Media State', TargetMediaState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 2, minStep: 1, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetMediaState = TargetMediaState; +Characteristic.TargetMediaState = TargetMediaState /** * Characteristic "Target Position" */ export class TargetPosition extends Characteristic { - - public static readonly UUID: string = "0000007C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000007C-0000-1000-8000-0026BB765291' constructor() { - super("Target Position", TargetPosition.UUID, { + super('Target Position', TargetPosition.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetPosition = TargetPosition; +Characteristic.TargetPosition = TargetPosition /** * Characteristic "Target Relative Humidity" */ export class TargetRelativeHumidity extends Characteristic { - - public static readonly UUID: string = "00000034-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000034-0000-1000-8000-0026BB765291' constructor() { - super("Target Relative Humidity", TargetRelativeHumidity.UUID, { + super('Target Relative Humidity', TargetRelativeHumidity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetRelativeHumidity = TargetRelativeHumidity; +Characteristic.TargetRelativeHumidity = TargetRelativeHumidity /** * Characteristic "Target Temperature" */ export class TargetTemperature extends Characteristic { - - public static readonly UUID: string = "00000035-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000035-0000-1000-8000-0026BB765291' constructor() { - super("Target Temperature", TargetTemperature.UUID, { + super('Target Temperature', TargetTemperature.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.CELSIUS, minValue: 10, maxValue: 38, minStep: 0.1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetTemperature = TargetTemperature; +Characteristic.TargetTemperature = TargetTemperature /** * Characteristic "Target Tilt Angle" */ export class TargetTiltAngle extends Characteristic { - - public static readonly UUID: string = "000000C2-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C2-0000-1000-8000-0026BB765291' constructor() { - super("Target Tilt Angle", TargetTiltAngle.UUID, { + super('Target Tilt Angle', TargetTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetTiltAngle = TargetTiltAngle; +Characteristic.TargetTiltAngle = TargetTiltAngle /** * Characteristic "Target Vertical Tilt Angle" */ export class TargetVerticalTiltAngle extends Characteristic { - - public static readonly UUID: string = "0000007D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000007D-0000-1000-8000-0026BB765291' constructor() { - super("Target Vertical Tilt Angle", TargetVerticalTiltAngle.UUID, { + super('Target Vertical Tilt Angle', TargetVerticalTiltAngle.UUID, { format: Formats.INT, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.ARC_DEGREE, minValue: -90, maxValue: 90, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetVerticalTiltAngle = TargetVerticalTiltAngle; +Characteristic.TargetVerticalTiltAngle = TargetVerticalTiltAngle /** * Characteristic "Target Visibility State" */ export class TargetVisibilityState extends Characteristic { + public static readonly UUID: string = '00000134-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000134-0000-1000-8000-0026BB765291"; - - public static readonly SHOWN = 0; - public static readonly HIDDEN = 1; + public static readonly SHOWN = 0 + public static readonly HIDDEN = 1 constructor() { - super("Target Visibility State", TargetVisibilityState.UUID, { + super('Target Visibility State', TargetVisibilityState.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TargetVisibilityState = TargetVisibilityState; +Characteristic.TargetVisibilityState = TargetVisibilityState /** * Characteristic "Temperature Display Units" */ export class TemperatureDisplayUnits extends Characteristic { + public static readonly UUID: string = '00000036-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "00000036-0000-1000-8000-0026BB765291"; - - public static readonly CELSIUS = 0; - public static readonly FAHRENHEIT = 1; + public static readonly CELSIUS = 0 + public static readonly FAHRENHEIT = 1 constructor() { - super("Temperature Display Units", TemperatureDisplayUnits.UUID, { + super('Temperature Display Units', TemperatureDisplayUnits.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TemperatureDisplayUnits = TemperatureDisplayUnits; +Characteristic.TemperatureDisplayUnits = TemperatureDisplayUnits /** * Characteristic "Third Party Camera Active" */ export class ThirdPartyCameraActive extends Characteristic { - - public static readonly UUID: string = "0000021C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000021C-0000-1000-8000-0026BB765291' constructor() { - super("Third Party Camera Active", ThirdPartyCameraActive.UUID, { + super('Third Party Camera Active', ThirdPartyCameraActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ThirdPartyCameraActive = ThirdPartyCameraActive; +Characteristic.ThirdPartyCameraActive = ThirdPartyCameraActive /** * Characteristic "Thread Control Point" */ export class ThreadControlPoint extends Characteristic { - - public static readonly UUID: string = "00000704-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000704-0000-1000-8000-0026BB765291' constructor() { - super("Thread Control Point", ThreadControlPoint.UUID, { + super('Thread Control Point', ThreadControlPoint.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ThreadControlPoint = ThreadControlPoint; +Characteristic.ThreadControlPoint = ThreadControlPoint /** * Characteristic "Thread Node Capabilities" */ export class ThreadNodeCapabilities extends Characteristic { - - public static readonly UUID: string = "00000702-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000702-0000-1000-8000-0026BB765291' constructor() { - super("Thread Node Capabilities", ThreadNodeCapabilities.UUID, { + super('Thread Node Capabilities', ThreadNodeCapabilities.UUID, { format: Formats.UINT16, perms: [Perms.PAIRED_READ], minValue: 0, maxValue: 31, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ThreadNodeCapabilities = ThreadNodeCapabilities; +Characteristic.ThreadNodeCapabilities = ThreadNodeCapabilities /** * Characteristic "Thread OpenThread Version" */ export class ThreadOpenThreadVersion extends Characteristic { - - public static readonly UUID: string = "00000706-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000706-0000-1000-8000-0026BB765291' constructor() { - super("Thread OpenThread Version", ThreadOpenThreadVersion.UUID, { + super('Thread OpenThread Version', ThreadOpenThreadVersion.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ThreadOpenThreadVersion = ThreadOpenThreadVersion; +Characteristic.ThreadOpenThreadVersion = ThreadOpenThreadVersion /** * Characteristic "Thread Status" */ export class ThreadStatus extends Characteristic { - - public static readonly UUID: string = "00000703-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000703-0000-1000-8000-0026BB765291' constructor() { - super("Thread Status", ThreadStatus.UUID, { + super('Thread Status', ThreadStatus.UUID, { format: Formats.UINT16, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 6, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ThreadStatus = ThreadStatus; +Characteristic.ThreadStatus = ThreadStatus /** * Characteristic "Token" */ export class Token extends Characteristic { - - public static readonly UUID: string = "00000231-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000231-0000-1000-8000-0026BB765291' constructor() { - super("Token", Token.UUID, { + super('Token', Token.UUID, { format: Formats.DATA, perms: [Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Token = Token; +Characteristic.Token = Token /** * Characteristic "Transmit Power" * @since iOS 14 */ export class TransmitPower extends Characteristic { - - public static readonly UUID: string = "00000242-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000242-0000-1000-8000-0026BB765291' constructor() { - super("Transmit Power", TransmitPower.UUID, { + super('Transmit Power', TransmitPower.UUID, { format: Formats.INT, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.TransmitPower = TransmitPower; +Characteristic.TransmitPower = TransmitPower /** * Characteristic "Valve Type" */ export class ValveType extends Characteristic { + public static readonly UUID: string = '000000D5-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000D5-0000-1000-8000-0026BB765291"; - - public static readonly GENERIC_VALVE = 0; - public static readonly IRRIGATION = 1; - public static readonly SHOWER_HEAD = 2; - public static readonly WATER_FAUCET = 3; + public static readonly GENERIC_VALVE = 0 + public static readonly IRRIGATION = 1 + public static readonly SHOWER_HEAD = 2 + public static readonly WATER_FAUCET = 3 constructor() { - super("Valve Type", ValveType.UUID, { + super('Valve Type', ValveType.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.ValveType = ValveType; +Characteristic.ValveType = ValveType /** * Characteristic "Version" */ export class Version extends Characteristic { - - public static readonly UUID: string = "00000037-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000037-0000-1000-8000-0026BB765291' constructor() { - super("Version", Version.UUID, { + super('Version', Version.UUID, { format: Formats.STRING, perms: [Perms.PAIRED_READ], maxLen: 64, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Version = Version; +Characteristic.Version = Version /** * Characteristic "Video Analysis Active" * @since iOS 14 */ export class VideoAnalysisActive extends Characteristic { - - public static readonly UUID: string = "00000229-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000229-0000-1000-8000-0026BB765291' constructor() { - super("Video Analysis Active", VideoAnalysisActive.UUID, { + super('Video Analysis Active', VideoAnalysisActive.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.VideoAnalysisActive = VideoAnalysisActive; +Characteristic.VideoAnalysisActive = VideoAnalysisActive /** * Characteristic "VOC Density" */ export class VOCDensity extends Characteristic { - - public static readonly UUID: string = "000000C8-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000C8-0000-1000-8000-0026BB765291' constructor() { - super("VOC Density", VOCDensity.UUID, { + super('VOC Density', VOCDensity.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 1000, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.VOCDensity = VOCDensity; +Characteristic.VOCDensity = VOCDensity /** * Characteristic "Volume" */ export class Volume extends Characteristic { - - public static readonly UUID: string = "00000119-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000119-0000-1000-8000-0026BB765291' constructor() { - super("Volume", Volume.UUID, { + super('Volume', Volume.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.Volume = Volume; +Characteristic.Volume = Volume /** * Characteristic "Volume Control Type" */ export class VolumeControlType extends Characteristic { + public static readonly UUID: string = '000000E9-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000E9-0000-1000-8000-0026BB765291"; - - public static readonly NONE = 0; - public static readonly RELATIVE = 1; - public static readonly RELATIVE_WITH_CURRENT = 2; - public static readonly ABSOLUTE = 3; + public static readonly NONE = 0 + public static readonly RELATIVE = 1 + public static readonly RELATIVE_WITH_CURRENT = 2 + public static readonly ABSOLUTE = 3 constructor() { - super("Volume Control Type", VolumeControlType.UUID, { + super('Volume Control Type', VolumeControlType.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 3, minStep: 1, validValues: [0, 1, 2, 3], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.VolumeControlType = VolumeControlType; +Characteristic.VolumeControlType = VolumeControlType /** * Characteristic "Volume Selector" */ export class VolumeSelector extends Characteristic { + public static readonly UUID: string = '000000EA-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "000000EA-0000-1000-8000-0026BB765291"; - - public static readonly INCREMENT = 0; - public static readonly DECREMENT = 1; + public static readonly INCREMENT = 0 + public static readonly DECREMENT = 1 constructor() { - super("Volume Selector", VolumeSelector.UUID, { + super('Volume Selector', VolumeSelector.UUID, { format: Formats.UINT8, perms: [Perms.PAIRED_WRITE], minValue: 0, maxValue: 1, minStep: 1, validValues: [0, 1], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.VolumeSelector = VolumeSelector; +Characteristic.VolumeSelector = VolumeSelector /** * Characteristic "Wake Configuration" * @since iOS 13.4 */ export class WakeConfiguration extends Characteristic { - - public static readonly UUID: string = "00000222-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000222-0000-1000-8000-0026BB765291' constructor() { - super("Wake Configuration", WakeConfiguration.UUID, { + super('Wake Configuration', WakeConfiguration.UUID, { format: Formats.TLV8, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WakeConfiguration = WakeConfiguration; +Characteristic.WakeConfiguration = WakeConfiguration /** * Characteristic "WAN Configuration List" */ export class WANConfigurationList extends Characteristic { - - public static readonly UUID: string = "00000211-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000211-0000-1000-8000-0026BB765291' constructor() { - super("WAN Configuration List", WANConfigurationList.UUID, { + super('WAN Configuration List', WANConfigurationList.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WANConfigurationList = WANConfigurationList; +Characteristic.WANConfigurationList = WANConfigurationList /** * Characteristic "WAN Status List" */ export class WANStatusList extends Characteristic { - - public static readonly UUID: string = "00000212-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000212-0000-1000-8000-0026BB765291' constructor() { - super("WAN Status List", WANStatusList.UUID, { + super('WAN Status List', WANStatusList.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WANStatusList = WANStatusList; +Characteristic.WANStatusList = WANStatusList /** * Characteristic "Water Level" */ export class WaterLevel extends Characteristic { - - public static readonly UUID: string = "000000B5-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000B5-0000-1000-8000-0026BB765291' constructor() { - super("Water Level", WaterLevel.UUID, { + super('Water Level', WaterLevel.UUID, { format: Formats.FLOAT, perms: [Perms.NOTIFY, Perms.PAIRED_READ], unit: Units.PERCENTAGE, minValue: 0, maxValue: 100, minStep: 1, - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WaterLevel = WaterLevel; +Characteristic.WaterLevel = WaterLevel /** * Characteristic "Wi-Fi Capabilities" * @since iOS 14 */ export class WiFiCapabilities extends Characteristic { - - public static readonly UUID: string = "0000022C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022C-0000-1000-8000-0026BB765291' constructor() { - super("Wi-Fi Capabilities", WiFiCapabilities.UUID, { + super('Wi-Fi Capabilities', WiFiCapabilities.UUID, { format: Formats.UINT32, perms: [Perms.PAIRED_READ], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WiFiCapabilities = WiFiCapabilities; +Characteristic.WiFiCapabilities = WiFiCapabilities /** * Characteristic "Wi-Fi Configuration Control" * @since iOS 14 */ export class WiFiConfigurationControl extends Characteristic { - - public static readonly UUID: string = "0000022D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022D-0000-1000-8000-0026BB765291' constructor() { - super("Wi-Fi Configuration Control", WiFiConfigurationControl.UUID, { + super('Wi-Fi Configuration Control', WiFiConfigurationControl.UUID, { format: Formats.TLV8, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE, Perms.TIMED_WRITE, Perms.WRITE_RESPONSE], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WiFiConfigurationControl = WiFiConfigurationControl; +Characteristic.WiFiConfigurationControl = WiFiConfigurationControl /** * Characteristic "Wi-Fi Satellite Status" */ export class WiFiSatelliteStatus extends Characteristic { + public static readonly UUID: string = '0000021E-0000-1000-8000-0026BB765291' - public static readonly UUID: string = "0000021E-0000-1000-8000-0026BB765291"; - - public static readonly UNKNOWN = 0; - public static readonly CONNECTED = 1; - public static readonly NOT_CONNECTED = 2; + public static readonly UNKNOWN = 0 + public static readonly CONNECTED = 1 + public static readonly NOT_CONNECTED = 2 constructor() { - super("Wi-Fi Satellite Status", WiFiSatelliteStatus.UUID, { + super('Wi-Fi Satellite Status', WiFiSatelliteStatus.UUID, { format: Formats.UINT8, perms: [Perms.NOTIFY, Perms.PAIRED_READ], minValue: 0, maxValue: 2, validValues: [0, 1, 2], - }); - this.value = this.getDefaultValue(); + }) + this.value = this.getDefaultValue() } } -Characteristic.WiFiSatelliteStatus = WiFiSatelliteStatus; - +Characteristic.WiFiSatelliteStatus = WiFiSatelliteStatus diff --git a/src/lib/definitions/ServiceDefinitions.spec.ts b/src/lib/definitions/ServiceDefinitions.spec.ts index 2d534f3b5..9397debe3 100644 --- a/src/lib/definitions/ServiceDefinitions.spec.ts +++ b/src/lib/definitions/ServiceDefinitions.spec.ts @@ -1,1569 +1,1570 @@ // THIS FILE IS AUTO-GENERATED - DO NOT MODIFY -import "./"; - -import { Characteristic } from "../Characteristic"; -import { Service } from "../Service"; - -describe("ServiceDefinitions", () => { - describe("AccessCode", () => { - it("should be able to construct", () => { - const service0 = new Service.AccessCode(); - const service1 = new Service.AccessCode("test name"); - const service2 = new Service.AccessCode("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AccessControl", () => { - it("should be able to construct", () => { - const service0 = new Service.AccessControl(); - const service1 = new Service.AccessControl("test name"); - const service2 = new Service.AccessControl("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AccessoryInformation", () => { - it("should be able to construct", () => { - const service0 = new Service.AccessoryInformation(); - const service1 = new Service.AccessoryInformation("test name"); - const service2 = new Service.AccessoryInformation("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AccessoryMetrics", () => { - it("should be able to construct", () => { - const service0 = new Service.AccessoryMetrics(); - const service1 = new Service.AccessoryMetrics("test name"); - const service2 = new Service.AccessoryMetrics("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AccessoryRuntimeInformation", () => { - it("should be able to construct", () => { - const service0 = new Service.AccessoryRuntimeInformation(); - const service1 = new Service.AccessoryRuntimeInformation("test name"); - const service2 = new Service.AccessoryRuntimeInformation("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AirPurifier", () => { - it("should be able to construct", () => { - const service0 = new Service.AirPurifier(); - const service1 = new Service.AirPurifier("test name"); - const service2 = new Service.AirPurifier("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AirQualitySensor", () => { - it("should be able to construct", () => { - const service0 = new Service.AirQualitySensor(); - const service1 = new Service.AirQualitySensor("test name"); - const service2 = new Service.AirQualitySensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AssetUpdate", () => { - it("should be able to construct", () => { - const service0 = new Service.AssetUpdate(); - const service1 = new Service.AssetUpdate("test name"); - const service2 = new Service.AssetUpdate("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Assistant", () => { - it("should be able to construct", () => { - const service0 = new Service.Assistant(); - const service1 = new Service.Assistant("test name"); - const service2 = new Service.Assistant("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("AudioStreamManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.AudioStreamManagement(); - const service1 = new Service.AudioStreamManagement("test name"); - const service2 = new Service.AudioStreamManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Battery", () => { - it("should be able to construct", () => { - const service0 = new Service.Battery(); - const service1 = new Service.Battery("test name"); - const service2 = new Service.Battery("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("CameraOperatingMode", () => { - it("should be able to construct", () => { - const service0 = new Service.CameraOperatingMode(); - const service1 = new Service.CameraOperatingMode("test name"); - const service2 = new Service.CameraOperatingMode("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("CameraRecordingManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.CameraRecordingManagement(); - const service1 = new Service.CameraRecordingManagement("test name"); - const service2 = new Service.CameraRecordingManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("CameraRTPStreamManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.CameraRTPStreamManagement(); - const service1 = new Service.CameraRTPStreamManagement("test name"); - const service2 = new Service.CameraRTPStreamManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("CarbonDioxideSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.CarbonDioxideSensor(); - const service1 = new Service.CarbonDioxideSensor("test name"); - const service2 = new Service.CarbonDioxideSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("CarbonMonoxideSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.CarbonMonoxideSensor(); - const service1 = new Service.CarbonMonoxideSensor("test name"); - const service2 = new Service.CarbonMonoxideSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("ContactSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.ContactSensor(); - const service1 = new Service.ContactSensor("test name"); - const service2 = new Service.ContactSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("DataStreamTransportManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.DataStreamTransportManagement(); - const service1 = new Service.DataStreamTransportManagement("test name"); - const service2 = new Service.DataStreamTransportManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Diagnostics", () => { - it("should be able to construct", () => { - const service0 = new Service.Diagnostics(); - const service1 = new Service.Diagnostics("test name"); - const service2 = new Service.Diagnostics("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Door", () => { - it("should be able to construct", () => { - const service0 = new Service.Door(); - const service1 = new Service.Door("test name"); - const service2 = new Service.Door("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Doorbell", () => { - it("should be able to construct", () => { - const service0 = new Service.Doorbell(); - const service1 = new Service.Doorbell("test name"); - const service2 = new Service.Doorbell("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Fan", () => { - it("should be able to construct", () => { - const service0 = new Service.Fan(); - const service1 = new Service.Fan("test name"); - const service2 = new Service.Fan("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Fanv2", () => { - it("should be able to construct", () => { - const service0 = new Service.Fanv2(); - const service1 = new Service.Fanv2("test name"); - const service2 = new Service.Fanv2("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Faucet", () => { - it("should be able to construct", () => { - const service0 = new Service.Faucet(); - const service1 = new Service.Faucet("test name"); - const service2 = new Service.Faucet("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("FilterMaintenance", () => { - it("should be able to construct", () => { - const service0 = new Service.FilterMaintenance(); - const service1 = new Service.FilterMaintenance("test name"); - const service2 = new Service.FilterMaintenance("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("FirmwareUpdate", () => { - it("should be able to construct", () => { - const service0 = new Service.FirmwareUpdate(); - const service1 = new Service.FirmwareUpdate("test name"); - const service2 = new Service.FirmwareUpdate("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("GarageDoorOpener", () => { - it("should be able to construct", () => { - const service0 = new Service.GarageDoorOpener(); - const service1 = new Service.GarageDoorOpener("test name"); - const service2 = new Service.GarageDoorOpener("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("HeaterCooler", () => { - it("should be able to construct", () => { - const service0 = new Service.HeaterCooler(); - const service1 = new Service.HeaterCooler("test name"); - const service2 = new Service.HeaterCooler("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("HumidifierDehumidifier", () => { - it("should be able to construct", () => { - const service0 = new Service.HumidifierDehumidifier(); - const service1 = new Service.HumidifierDehumidifier("test name"); - const service2 = new Service.HumidifierDehumidifier("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("HumiditySensor", () => { - it("should be able to construct", () => { - const service0 = new Service.HumiditySensor(); - const service1 = new Service.HumiditySensor("test name"); - const service2 = new Service.HumiditySensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("InputSource", () => { - it("should be able to construct", () => { - const service0 = new Service.InputSource(); - const service1 = new Service.InputSource("test name"); - const service2 = new Service.InputSource("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("IrrigationSystem", () => { - it("should be able to construct", () => { - const service0 = new Service.IrrigationSystem(); - const service1 = new Service.IrrigationSystem("test name"); - const service2 = new Service.IrrigationSystem("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("LeakSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.LeakSensor(); - const service1 = new Service.LeakSensor("test name"); - const service2 = new Service.LeakSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Lightbulb", () => { - it("should be able to construct", () => { - const service0 = new Service.Lightbulb(); - const service1 = new Service.Lightbulb("test name"); - const service2 = new Service.Lightbulb("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("LightSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.LightSensor(); - const service1 = new Service.LightSensor("test name"); - const service2 = new Service.LightSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("LockManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.LockManagement(); - const service1 = new Service.LockManagement("test name"); - const service2 = new Service.LockManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("LockMechanism", () => { - it("should be able to construct", () => { - const service0 = new Service.LockMechanism(); - const service1 = new Service.LockMechanism("test name"); - const service2 = new Service.LockMechanism("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Microphone", () => { - it("should be able to construct", () => { - const service0 = new Service.Microphone(); - const service1 = new Service.Microphone("test name"); - const service2 = new Service.Microphone("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("MotionSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.MotionSensor(); - const service1 = new Service.MotionSensor("test name"); - const service2 = new Service.MotionSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("NFCAccess", () => { - it("should be able to construct", () => { - const service0 = new Service.NFCAccess(); - const service1 = new Service.NFCAccess("test name"); - const service2 = new Service.NFCAccess("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("OccupancySensor", () => { - it("should be able to construct", () => { - const service0 = new Service.OccupancySensor(); - const service1 = new Service.OccupancySensor("test name"); - const service2 = new Service.OccupancySensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Outlet", () => { - it("should be able to construct", () => { - const service0 = new Service.Outlet(); - const service1 = new Service.Outlet("test name"); - const service2 = new Service.Outlet("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Pairing", () => { - it("should be able to construct", () => { - const service0 = new Service.Pairing(); - const service1 = new Service.Pairing("test name"); - const service2 = new Service.Pairing("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("PowerManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.PowerManagement(); - const service1 = new Service.PowerManagement("test name"); - const service2 = new Service.PowerManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("ProtocolInformation", () => { - it("should be able to construct", () => { - const service0 = new Service.ProtocolInformation(); - const service1 = new Service.ProtocolInformation("test name"); - const service2 = new Service.ProtocolInformation("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("SecuritySystem", () => { - it("should be able to construct", () => { - const service0 = new Service.SecuritySystem(); - const service1 = new Service.SecuritySystem("test name"); - const service2 = new Service.SecuritySystem("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("ServiceLabel", () => { - it("should be able to construct", () => { - const service0 = new Service.ServiceLabel(); - const service1 = new Service.ServiceLabel("test name"); - const service2 = new Service.ServiceLabel("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Siri", () => { - it("should be able to construct", () => { - const service0 = new Service.Siri(); - const service1 = new Service.Siri("test name"); - const service2 = new Service.Siri("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("SiriEndpoint", () => { - it("should be able to construct", () => { - const service0 = new Service.SiriEndpoint(); - const service1 = new Service.SiriEndpoint("test name"); - const service2 = new Service.SiriEndpoint("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Slats", () => { - it("should be able to construct", () => { - const service0 = new Service.Slats(); - const service1 = new Service.Slats("test name"); - const service2 = new Service.Slats("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("SmartSpeaker", () => { - it("should be able to construct", () => { - const service0 = new Service.SmartSpeaker(); - const service1 = new Service.SmartSpeaker("test name"); - const service2 = new Service.SmartSpeaker("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("SmokeSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.SmokeSensor(); - const service1 = new Service.SmokeSensor("test name"); - const service2 = new Service.SmokeSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Speaker", () => { - it("should be able to construct", () => { - const service0 = new Service.Speaker(); - const service1 = new Service.Speaker("test name"); - const service2 = new Service.Speaker("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("StatefulProgrammableSwitch", () => { - it("should be able to construct", () => { - const service0 = new Service.StatefulProgrammableSwitch(); - const service1 = new Service.StatefulProgrammableSwitch("test name"); - const service2 = new Service.StatefulProgrammableSwitch("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("StatelessProgrammableSwitch", () => { - it("should be able to construct", () => { - const service0 = new Service.StatelessProgrammableSwitch(); - const service1 = new Service.StatelessProgrammableSwitch("test name"); - const service2 = new Service.StatelessProgrammableSwitch("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Switch", () => { - it("should be able to construct", () => { - const service0 = new Service.Switch(); - const service1 = new Service.Switch("test name"); - const service2 = new Service.Switch("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TapManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.TapManagement(); - const service1 = new Service.TapManagement("test name"); - const service2 = new Service.TapManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TargetControl", () => { - it("should be able to construct", () => { - const service0 = new Service.TargetControl(); - const service1 = new Service.TargetControl("test name"); - const service2 = new Service.TargetControl("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TargetControlManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.TargetControlManagement(); - const service1 = new Service.TargetControlManagement("test name"); - const service2 = new Service.TargetControlManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Television", () => { - it("should be able to construct", () => { - const service0 = new Service.Television(); - const service1 = new Service.Television("test name"); - const service2 = new Service.Television("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TelevisionSpeaker", () => { - it("should be able to construct", () => { - const service0 = new Service.TelevisionSpeaker(); - const service1 = new Service.TelevisionSpeaker("test name"); - const service2 = new Service.TelevisionSpeaker("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TemperatureSensor", () => { - it("should be able to construct", () => { - const service0 = new Service.TemperatureSensor(); - const service1 = new Service.TemperatureSensor("test name"); - const service2 = new Service.TemperatureSensor("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Thermostat", () => { - it("should be able to construct", () => { - const service0 = new Service.Thermostat(); - const service1 = new Service.Thermostat("test name"); - const service2 = new Service.Thermostat("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("ThreadTransport", () => { - it("should be able to construct", () => { - const service0 = new Service.ThreadTransport(); - const service1 = new Service.ThreadTransport("test name"); - const service2 = new Service.ThreadTransport("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("TransferTransportManagement", () => { - it("should be able to construct", () => { - const service0 = new Service.TransferTransportManagement(); - const service1 = new Service.TransferTransportManagement("test name"); - const service2 = new Service.TransferTransportManagement("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Valve", () => { - it("should be able to construct", () => { - const service0 = new Service.Valve(); - const service1 = new Service.Valve("test name"); - const service2 = new Service.Valve("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("WiFiRouter", () => { - it("should be able to construct", () => { - const service0 = new Service.WiFiRouter(); - const service1 = new Service.WiFiRouter("test name"); - const service2 = new Service.WiFiRouter("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("WiFiSatellite", () => { - it("should be able to construct", () => { - const service0 = new Service.WiFiSatellite(); - const service1 = new Service.WiFiSatellite("test name"); - const service2 = new Service.WiFiSatellite("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("WiFiTransport", () => { - it("should be able to construct", () => { - const service0 = new Service.WiFiTransport(); - const service1 = new Service.WiFiTransport("test name"); - const service2 = new Service.WiFiTransport("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("Window", () => { - it("should be able to construct", () => { - const service0 = new Service.Window(); - const service1 = new Service.Window("test name"); - const service2 = new Service.Window("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); - - describe("WindowCovering", () => { - it("should be able to construct", () => { - const service0 = new Service.WindowCovering(); - const service1 = new Service.WindowCovering("test name"); - const service2 = new Service.WindowCovering("test name", "test sub type"); - - expect(service0.displayName).toBe(""); - expect(service0.testCharacteristic(Characteristic.Name)).toBe(false); - expect(service0.subtype).toBeUndefined(); - - expect(service1.displayName).toBe("test name"); - expect(service1.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service1.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service1.subtype).toBeUndefined(); - - expect(service2.displayName).toBe("test name"); - expect(service2.testCharacteristic(Characteristic.Name)).toBe(true); - expect(service2.getCharacteristic(Characteristic.Name).value).toBe("test name"); - expect(service2.subtype).toBe("test sub type"); - }); - }); -}); +import { describe, expect, it } from 'vitest' + +import { Characteristic } from '../Characteristic.js' +import { Service } from '../Service.js' +import './index.js' + +describe('serviceDefinitions', () => { + describe('accessCode', () => { + it('should be able to construct', () => { + const service0 = new Service.AccessCode() + const service1 = new Service.AccessCode('test name') + const service2 = new Service.AccessCode('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('accessControl', () => { + it('should be able to construct', () => { + const service0 = new Service.AccessControl() + const service1 = new Service.AccessControl('test name') + const service2 = new Service.AccessControl('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('accessoryInformation', () => { + it('should be able to construct', () => { + const service0 = new Service.AccessoryInformation() + const service1 = new Service.AccessoryInformation('test name') + const service2 = new Service.AccessoryInformation('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('accessoryMetrics', () => { + it('should be able to construct', () => { + const service0 = new Service.AccessoryMetrics() + const service1 = new Service.AccessoryMetrics('test name') + const service2 = new Service.AccessoryMetrics('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('accessoryRuntimeInformation', () => { + it('should be able to construct', () => { + const service0 = new Service.AccessoryRuntimeInformation() + const service1 = new Service.AccessoryRuntimeInformation('test name') + const service2 = new Service.AccessoryRuntimeInformation('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('airPurifier', () => { + it('should be able to construct', () => { + const service0 = new Service.AirPurifier() + const service1 = new Service.AirPurifier('test name') + const service2 = new Service.AirPurifier('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('airQualitySensor', () => { + it('should be able to construct', () => { + const service0 = new Service.AirQualitySensor() + const service1 = new Service.AirQualitySensor('test name') + const service2 = new Service.AirQualitySensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('assetUpdate', () => { + it('should be able to construct', () => { + const service0 = new Service.AssetUpdate() + const service1 = new Service.AssetUpdate('test name') + const service2 = new Service.AssetUpdate('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('assistant', () => { + it('should be able to construct', () => { + const service0 = new Service.Assistant() + const service1 = new Service.Assistant('test name') + const service2 = new Service.Assistant('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('audioStreamManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.AudioStreamManagement() + const service1 = new Service.AudioStreamManagement('test name') + const service2 = new Service.AudioStreamManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('battery', () => { + it('should be able to construct', () => { + const service0 = new Service.Battery() + const service1 = new Service.Battery('test name') + const service2 = new Service.Battery('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('cameraOperatingMode', () => { + it('should be able to construct', () => { + const service0 = new Service.CameraOperatingMode() + const service1 = new Service.CameraOperatingMode('test name') + const service2 = new Service.CameraOperatingMode('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('cameraRecordingManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.CameraRecordingManagement() + const service1 = new Service.CameraRecordingManagement('test name') + const service2 = new Service.CameraRecordingManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('cameraRTPStreamManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.CameraRTPStreamManagement() + const service1 = new Service.CameraRTPStreamManagement('test name') + const service2 = new Service.CameraRTPStreamManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('carbonDioxideSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.CarbonDioxideSensor() + const service1 = new Service.CarbonDioxideSensor('test name') + const service2 = new Service.CarbonDioxideSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('carbonMonoxideSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.CarbonMonoxideSensor() + const service1 = new Service.CarbonMonoxideSensor('test name') + const service2 = new Service.CarbonMonoxideSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('contactSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.ContactSensor() + const service1 = new Service.ContactSensor('test name') + const service2 = new Service.ContactSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('dataStreamTransportManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.DataStreamTransportManagement() + const service1 = new Service.DataStreamTransportManagement('test name') + const service2 = new Service.DataStreamTransportManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('diagnostics', () => { + it('should be able to construct', () => { + const service0 = new Service.Diagnostics() + const service1 = new Service.Diagnostics('test name') + const service2 = new Service.Diagnostics('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('door', () => { + it('should be able to construct', () => { + const service0 = new Service.Door() + const service1 = new Service.Door('test name') + const service2 = new Service.Door('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('doorbell', () => { + it('should be able to construct', () => { + const service0 = new Service.Doorbell() + const service1 = new Service.Doorbell('test name') + const service2 = new Service.Doorbell('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('fan', () => { + it('should be able to construct', () => { + const service0 = new Service.Fan() + const service1 = new Service.Fan('test name') + const service2 = new Service.Fan('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('fanv2', () => { + it('should be able to construct', () => { + const service0 = new Service.Fanv2() + const service1 = new Service.Fanv2('test name') + const service2 = new Service.Fanv2('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('faucet', () => { + it('should be able to construct', () => { + const service0 = new Service.Faucet() + const service1 = new Service.Faucet('test name') + const service2 = new Service.Faucet('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('filterMaintenance', () => { + it('should be able to construct', () => { + const service0 = new Service.FilterMaintenance() + const service1 = new Service.FilterMaintenance('test name') + const service2 = new Service.FilterMaintenance('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('firmwareUpdate', () => { + it('should be able to construct', () => { + const service0 = new Service.FirmwareUpdate() + const service1 = new Service.FirmwareUpdate('test name') + const service2 = new Service.FirmwareUpdate('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('garageDoorOpener', () => { + it('should be able to construct', () => { + const service0 = new Service.GarageDoorOpener() + const service1 = new Service.GarageDoorOpener('test name') + const service2 = new Service.GarageDoorOpener('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('heaterCooler', () => { + it('should be able to construct', () => { + const service0 = new Service.HeaterCooler() + const service1 = new Service.HeaterCooler('test name') + const service2 = new Service.HeaterCooler('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('humidifierDehumidifier', () => { + it('should be able to construct', () => { + const service0 = new Service.HumidifierDehumidifier() + const service1 = new Service.HumidifierDehumidifier('test name') + const service2 = new Service.HumidifierDehumidifier('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('humiditySensor', () => { + it('should be able to construct', () => { + const service0 = new Service.HumiditySensor() + const service1 = new Service.HumiditySensor('test name') + const service2 = new Service.HumiditySensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('inputSource', () => { + it('should be able to construct', () => { + const service0 = new Service.InputSource() + const service1 = new Service.InputSource('test name') + const service2 = new Service.InputSource('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('irrigationSystem', () => { + it('should be able to construct', () => { + const service0 = new Service.IrrigationSystem() + const service1 = new Service.IrrigationSystem('test name') + const service2 = new Service.IrrigationSystem('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('leakSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.LeakSensor() + const service1 = new Service.LeakSensor('test name') + const service2 = new Service.LeakSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('lightbulb', () => { + it('should be able to construct', () => { + const service0 = new Service.Lightbulb() + const service1 = new Service.Lightbulb('test name') + const service2 = new Service.Lightbulb('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('lightSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.LightSensor() + const service1 = new Service.LightSensor('test name') + const service2 = new Service.LightSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('lockManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.LockManagement() + const service1 = new Service.LockManagement('test name') + const service2 = new Service.LockManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('lockMechanism', () => { + it('should be able to construct', () => { + const service0 = new Service.LockMechanism() + const service1 = new Service.LockMechanism('test name') + const service2 = new Service.LockMechanism('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('microphone', () => { + it('should be able to construct', () => { + const service0 = new Service.Microphone() + const service1 = new Service.Microphone('test name') + const service2 = new Service.Microphone('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('motionSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.MotionSensor() + const service1 = new Service.MotionSensor('test name') + const service2 = new Service.MotionSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('nFCAccess', () => { + it('should be able to construct', () => { + const service0 = new Service.NFCAccess() + const service1 = new Service.NFCAccess('test name') + const service2 = new Service.NFCAccess('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('occupancySensor', () => { + it('should be able to construct', () => { + const service0 = new Service.OccupancySensor() + const service1 = new Service.OccupancySensor('test name') + const service2 = new Service.OccupancySensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('outlet', () => { + it('should be able to construct', () => { + const service0 = new Service.Outlet() + const service1 = new Service.Outlet('test name') + const service2 = new Service.Outlet('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('pairing', () => { + it('should be able to construct', () => { + const service0 = new Service.Pairing() + const service1 = new Service.Pairing('test name') + const service2 = new Service.Pairing('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('powerManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.PowerManagement() + const service1 = new Service.PowerManagement('test name') + const service2 = new Service.PowerManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('protocolInformation', () => { + it('should be able to construct', () => { + const service0 = new Service.ProtocolInformation() + const service1 = new Service.ProtocolInformation('test name') + const service2 = new Service.ProtocolInformation('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('securitySystem', () => { + it('should be able to construct', () => { + const service0 = new Service.SecuritySystem() + const service1 = new Service.SecuritySystem('test name') + const service2 = new Service.SecuritySystem('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('serviceLabel', () => { + it('should be able to construct', () => { + const service0 = new Service.ServiceLabel() + const service1 = new Service.ServiceLabel('test name') + const service2 = new Service.ServiceLabel('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('siri', () => { + it('should be able to construct', () => { + const service0 = new Service.Siri() + const service1 = new Service.Siri('test name') + const service2 = new Service.Siri('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('siriEndpoint', () => { + it('should be able to construct', () => { + const service0 = new Service.SiriEndpoint() + const service1 = new Service.SiriEndpoint('test name') + const service2 = new Service.SiriEndpoint('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('slats', () => { + it('should be able to construct', () => { + const service0 = new Service.Slats() + const service1 = new Service.Slats('test name') + const service2 = new Service.Slats('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('smartSpeaker', () => { + it('should be able to construct', () => { + const service0 = new Service.SmartSpeaker() + const service1 = new Service.SmartSpeaker('test name') + const service2 = new Service.SmartSpeaker('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('smokeSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.SmokeSensor() + const service1 = new Service.SmokeSensor('test name') + const service2 = new Service.SmokeSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('speaker', () => { + it('should be able to construct', () => { + const service0 = new Service.Speaker() + const service1 = new Service.Speaker('test name') + const service2 = new Service.Speaker('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('statefulProgrammableSwitch', () => { + it('should be able to construct', () => { + const service0 = new Service.StatefulProgrammableSwitch() + const service1 = new Service.StatefulProgrammableSwitch('test name') + const service2 = new Service.StatefulProgrammableSwitch('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('statelessProgrammableSwitch', () => { + it('should be able to construct', () => { + const service0 = new Service.StatelessProgrammableSwitch() + const service1 = new Service.StatelessProgrammableSwitch('test name') + const service2 = new Service.StatelessProgrammableSwitch('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('switch', () => { + it('should be able to construct', () => { + const service0 = new Service.Switch() + const service1 = new Service.Switch('test name') + const service2 = new Service.Switch('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('tapManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.TapManagement() + const service1 = new Service.TapManagement('test name') + const service2 = new Service.TapManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('targetControl', () => { + it('should be able to construct', () => { + const service0 = new Service.TargetControl() + const service1 = new Service.TargetControl('test name') + const service2 = new Service.TargetControl('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('targetControlManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.TargetControlManagement() + const service1 = new Service.TargetControlManagement('test name') + const service2 = new Service.TargetControlManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('television', () => { + it('should be able to construct', () => { + const service0 = new Service.Television() + const service1 = new Service.Television('test name') + const service2 = new Service.Television('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('televisionSpeaker', () => { + it('should be able to construct', () => { + const service0 = new Service.TelevisionSpeaker() + const service1 = new Service.TelevisionSpeaker('test name') + const service2 = new Service.TelevisionSpeaker('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('temperatureSensor', () => { + it('should be able to construct', () => { + const service0 = new Service.TemperatureSensor() + const service1 = new Service.TemperatureSensor('test name') + const service2 = new Service.TemperatureSensor('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('thermostat', () => { + it('should be able to construct', () => { + const service0 = new Service.Thermostat() + const service1 = new Service.Thermostat('test name') + const service2 = new Service.Thermostat('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('threadTransport', () => { + it('should be able to construct', () => { + const service0 = new Service.ThreadTransport() + const service1 = new Service.ThreadTransport('test name') + const service2 = new Service.ThreadTransport('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('transferTransportManagement', () => { + it('should be able to construct', () => { + const service0 = new Service.TransferTransportManagement() + const service1 = new Service.TransferTransportManagement('test name') + const service2 = new Service.TransferTransportManagement('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('valve', () => { + it('should be able to construct', () => { + const service0 = new Service.Valve() + const service1 = new Service.Valve('test name') + const service2 = new Service.Valve('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('wiFiRouter', () => { + it('should be able to construct', () => { + const service0 = new Service.WiFiRouter() + const service1 = new Service.WiFiRouter('test name') + const service2 = new Service.WiFiRouter('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('wiFiSatellite', () => { + it('should be able to construct', () => { + const service0 = new Service.WiFiSatellite() + const service1 = new Service.WiFiSatellite('test name') + const service2 = new Service.WiFiSatellite('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('wiFiTransport', () => { + it('should be able to construct', () => { + const service0 = new Service.WiFiTransport() + const service1 = new Service.WiFiTransport('test name') + const service2 = new Service.WiFiTransport('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('window', () => { + it('should be able to construct', () => { + const service0 = new Service.Window() + const service1 = new Service.Window('test name') + const service2 = new Service.Window('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) + + describe('windowCovering', () => { + it('should be able to construct', () => { + const service0 = new Service.WindowCovering() + const service1 = new Service.WindowCovering('test name') + const service2 = new Service.WindowCovering('test name', 'test sub type') + + expect(service0.displayName).toBe('') + expect(service0.testCharacteristic(Characteristic.Name)).toBe(false) + expect(service0.subtype).toBeUndefined() + + expect(service1.displayName).toBe('test name') + expect(service1.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service1.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service1.subtype).toBeUndefined() + + expect(service2.displayName).toBe('test name') + expect(service2.testCharacteristic(Characteristic.Name)).toBe(true) + expect(service2.getCharacteristic(Characteristic.Name).value).toBe('test name') + expect(service2.subtype).toBe('test sub type') + }) + }) +}) diff --git a/src/lib/definitions/ServiceDefinitions.ts b/src/lib/definitions/ServiceDefinitions.ts index 5209c2352..fd9831ddd 100644 --- a/src/lib/definitions/ServiceDefinitions.ts +++ b/src/lib/definitions/ServiceDefinitions.ts @@ -1,1591 +1,1519 @@ // THIS FILE IS AUTO-GENERATED - DO NOT MODIFY // V=886 -import { Characteristic } from "../Characteristic"; -import { Service } from "../Service"; +import { Characteristic } from '../Characteristic.js' +import { Service } from '../Service.js' /** * Service "Access Code" * @since iOS 15 */ export class AccessCode extends Service { - - public static readonly UUID: string = "00000260-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000260-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AccessCode.UUID, subtype); + super(displayName, AccessCode.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.AccessCodeControlPoint); - this.addCharacteristic(Characteristic.AccessCodeSupportedConfiguration); - this.addCharacteristic(Characteristic.ConfigurationState); + this.addCharacteristic(Characteristic.AccessCodeControlPoint) + this.addCharacteristic(Characteristic.AccessCodeSupportedConfiguration) + this.addCharacteristic(Characteristic.ConfigurationState) } } -Service.AccessCode = AccessCode; +Service.AccessCode = AccessCode /** * Service "Access Control" */ export class AccessControl extends Service { - - public static readonly UUID: string = "000000DA-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000DA-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AccessControl.UUID, subtype); + super(displayName, AccessControl.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.AccessControlLevel); + this.addCharacteristic(Characteristic.AccessControlLevel) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.PasswordSetting); + this.addOptionalCharacteristic(Characteristic.PasswordSetting) } } -Service.AccessControl = AccessControl; +Service.AccessControl = AccessControl /** * Service "Accessory Information" */ export class AccessoryInformation extends Service { - - public static readonly UUID: string = "0000003E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000003E-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AccessoryInformation.UUID, subtype); + super(displayName, AccessoryInformation.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Identify); - this.addCharacteristic(Characteristic.Manufacturer); - this.addCharacteristic(Characteristic.Model); + this.addCharacteristic(Characteristic.Identify) + this.addCharacteristic(Characteristic.Manufacturer) + this.addCharacteristic(Characteristic.Model) if (!this.testCharacteristic(Characteristic.Name)) { // workaround for Name characteristic collision in constructor - this.addCharacteristic(Characteristic.Name).updateValue("Unnamed Service"); + this.addCharacteristic(Characteristic.Name).updateValue('Unnamed Service') } - this.addCharacteristic(Characteristic.SerialNumber); - this.addCharacteristic(Characteristic.FirmwareRevision); + this.addCharacteristic(Characteristic.SerialNumber) + this.addCharacteristic(Characteristic.FirmwareRevision) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.AccessoryFlags); - this.addOptionalCharacteristic(Characteristic.AppMatchingIdentifier); - this.addOptionalCharacteristic(Characteristic.ConfiguredName); - this.addOptionalCharacteristic(Characteristic.MatterFirmwareRevisionNumber); - this.addOptionalCharacteristic(Characteristic.HardwareFinish); - this.addOptionalCharacteristic(Characteristic.HardwareRevision); - this.addOptionalCharacteristic(Characteristic.ProductData); - this.addOptionalCharacteristic(Characteristic.SoftwareRevision); + this.addOptionalCharacteristic(Characteristic.AccessoryFlags) + this.addOptionalCharacteristic(Characteristic.AppMatchingIdentifier) + this.addOptionalCharacteristic(Characteristic.ConfiguredName) + this.addOptionalCharacteristic(Characteristic.MatterFirmwareRevisionNumber) + this.addOptionalCharacteristic(Characteristic.HardwareFinish) + this.addOptionalCharacteristic(Characteristic.HardwareRevision) + this.addOptionalCharacteristic(Characteristic.ProductData) + this.addOptionalCharacteristic(Characteristic.SoftwareRevision) } } -Service.AccessoryInformation = AccessoryInformation; +Service.AccessoryInformation = AccessoryInformation /** * Service "Accessory Metrics" */ export class AccessoryMetrics extends Service { - - public static readonly UUID: string = "00000270-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000270-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AccessoryMetrics.UUID, subtype); + super(displayName, AccessoryMetrics.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.MetricsBufferFullState); - this.addCharacteristic(Characteristic.SupportedMetrics); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.MetricsBufferFullState) + this.addCharacteristic(Characteristic.SupportedMetrics) } } -Service.AccessoryMetrics = AccessoryMetrics; +Service.AccessoryMetrics = AccessoryMetrics /** * Service "Accessory Runtime Information" */ export class AccessoryRuntimeInformation extends Service { - - public static readonly UUID: string = "00000239-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000239-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AccessoryRuntimeInformation.UUID, subtype); + super(displayName, AccessoryRuntimeInformation.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Ping); + this.addCharacteristic(Characteristic.Ping) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.ActivityInterval); - this.addOptionalCharacteristic(Characteristic.HeartBeat); - this.addOptionalCharacteristic(Characteristic.SleepInterval); + this.addOptionalCharacteristic(Characteristic.ActivityInterval) + this.addOptionalCharacteristic(Characteristic.HeartBeat) + this.addOptionalCharacteristic(Characteristic.SleepInterval) } } -Service.AccessoryRuntimeInformation = AccessoryRuntimeInformation; +Service.AccessoryRuntimeInformation = AccessoryRuntimeInformation /** * Service "Air Purifier" */ export class AirPurifier extends Service { - - public static readonly UUID: string = "000000BB-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000BB-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AirPurifier.UUID, subtype); + super(displayName, AirPurifier.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.CurrentAirPurifierState); - this.addCharacteristic(Characteristic.TargetAirPurifierState); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.CurrentAirPurifierState) + this.addCharacteristic(Characteristic.TargetAirPurifierState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.LockPhysicalControls); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RotationSpeed); - this.addOptionalCharacteristic(Characteristic.SwingMode); + this.addOptionalCharacteristic(Characteristic.LockPhysicalControls) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RotationSpeed) + this.addOptionalCharacteristic(Characteristic.SwingMode) } } -Service.AirPurifier = AirPurifier; +Service.AirPurifier = AirPurifier /** * Service "Air Quality Sensor" */ export class AirQualitySensor extends Service { - - public static readonly UUID: string = "0000008D-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000008D-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AirQualitySensor.UUID, subtype); + super(displayName, AirQualitySensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.AirQuality); + this.addCharacteristic(Characteristic.AirQuality) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.NitrogenDioxideDensity); - this.addOptionalCharacteristic(Characteristic.OzoneDensity); - this.addOptionalCharacteristic(Characteristic.PM10Density); - this.addOptionalCharacteristic(Characteristic.PM2_5Density); - this.addOptionalCharacteristic(Characteristic.SulphurDioxideDensity); - this.addOptionalCharacteristic(Characteristic.VOCDensity); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.NitrogenDioxideDensity) + this.addOptionalCharacteristic(Characteristic.OzoneDensity) + this.addOptionalCharacteristic(Characteristic.PM10Density) + this.addOptionalCharacteristic(Characteristic.PM2_5Density) + this.addOptionalCharacteristic(Characteristic.SulphurDioxideDensity) + this.addOptionalCharacteristic(Characteristic.VOCDensity) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.AirQualitySensor = AirQualitySensor; +Service.AirQualitySensor = AirQualitySensor /** * Service "Asset Update" */ export class AssetUpdate extends Service { - - public static readonly UUID: string = "00000267-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000267-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AssetUpdate.UUID, subtype); + super(displayName, AssetUpdate.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.AssetUpdateReadiness); - this.addCharacteristic(Characteristic.SupportedAssetTypes); + this.addCharacteristic(Characteristic.AssetUpdateReadiness) + this.addCharacteristic(Characteristic.SupportedAssetTypes) } } -Service.AssetUpdate = AssetUpdate; +Service.AssetUpdate = AssetUpdate /** * Service "Assistant" */ export class Assistant extends Service { - - public static readonly UUID: string = "0000026A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000026A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Assistant.UUID, subtype); + super(displayName, Assistant.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.Identifier); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.Identifier) if (!this.testCharacteristic(Characteristic.Name)) { // workaround for Name characteristic collision in constructor - this.addCharacteristic(Characteristic.Name).updateValue("Unnamed Service"); + this.addCharacteristic(Characteristic.Name).updateValue('Unnamed Service') } } } -Service.Assistant = Assistant; +Service.Assistant = Assistant /** * Service "Audio Stream Management" */ export class AudioStreamManagement extends Service { - - public static readonly UUID: string = "00000127-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000127-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, AudioStreamManagement.UUID, subtype); + super(displayName, AudioStreamManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SupportedAudioStreamConfiguration); - this.addCharacteristic(Characteristic.SelectedAudioStreamConfiguration); + this.addCharacteristic(Characteristic.SupportedAudioStreamConfiguration) + this.addCharacteristic(Characteristic.SelectedAudioStreamConfiguration) } } -Service.AudioStreamManagement = AudioStreamManagement; +Service.AudioStreamManagement = AudioStreamManagement /** * Service "Battery" */ export class Battery extends Service { - - public static readonly UUID: string = "00000096-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000096-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Battery.UUID, subtype); + super(displayName, Battery.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.StatusLowBattery); + this.addCharacteristic(Characteristic.StatusLowBattery) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.BatteryLevel); - this.addOptionalCharacteristic(Characteristic.ChargingState); - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.BatteryLevel) + this.addOptionalCharacteristic(Characteristic.ChargingState) + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.Battery = Battery; +Service.Battery = Battery /** * Service "Camera Operating Mode" */ export class CameraOperatingMode extends Service { - - public static readonly UUID: string = "0000021A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000021A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, CameraOperatingMode.UUID, subtype); + super(displayName, CameraOperatingMode.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.EventSnapshotsActive); - this.addCharacteristic(Characteristic.HomeKitCameraActive); + this.addCharacteristic(Characteristic.EventSnapshotsActive) + this.addCharacteristic(Characteristic.HomeKitCameraActive) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CameraOperatingModeIndicator); - this.addOptionalCharacteristic(Characteristic.ManuallyDisabled); - this.addOptionalCharacteristic(Characteristic.NightVision); - this.addOptionalCharacteristic(Characteristic.PeriodicSnapshotsActive); - this.addOptionalCharacteristic(Characteristic.ThirdPartyCameraActive); - this.addOptionalCharacteristic(Characteristic.DiagonalFieldOfView); + this.addOptionalCharacteristic(Characteristic.CameraOperatingModeIndicator) + this.addOptionalCharacteristic(Characteristic.ManuallyDisabled) + this.addOptionalCharacteristic(Characteristic.NightVision) + this.addOptionalCharacteristic(Characteristic.PeriodicSnapshotsActive) + this.addOptionalCharacteristic(Characteristic.ThirdPartyCameraActive) + this.addOptionalCharacteristic(Characteristic.DiagonalFieldOfView) } } -Service.CameraOperatingMode = CameraOperatingMode; +Service.CameraOperatingMode = CameraOperatingMode /** * Service "Camera Recording Management" */ export class CameraRecordingManagement extends Service { - - public static readonly UUID: string = "00000204-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000204-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, CameraRecordingManagement.UUID, subtype); + super(displayName, CameraRecordingManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.SelectedCameraRecordingConfiguration); - this.addCharacteristic(Characteristic.SupportedAudioRecordingConfiguration); - this.addCharacteristic(Characteristic.SupportedCameraRecordingConfiguration); - this.addCharacteristic(Characteristic.SupportedVideoRecordingConfiguration); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.SelectedCameraRecordingConfiguration) + this.addCharacteristic(Characteristic.SupportedAudioRecordingConfiguration) + this.addCharacteristic(Characteristic.SupportedCameraRecordingConfiguration) + this.addCharacteristic(Characteristic.SupportedVideoRecordingConfiguration) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.RecordingAudioActive); + this.addOptionalCharacteristic(Characteristic.RecordingAudioActive) } } -Service.CameraRecordingManagement = CameraRecordingManagement; +Service.CameraRecordingManagement = CameraRecordingManagement /** * Service "Camera RTP Stream Management" */ export class CameraRTPStreamManagement extends Service { - - public static readonly UUID: string = "00000110-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000110-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, CameraRTPStreamManagement.UUID, subtype); + super(displayName, CameraRTPStreamManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SelectedRTPStreamConfiguration); - this.addCharacteristic(Characteristic.SetupEndpoints); - this.addCharacteristic(Characteristic.StreamingStatus); - this.addCharacteristic(Characteristic.SupportedAudioStreamConfiguration); - this.addCharacteristic(Characteristic.SupportedRTPConfiguration); - this.addCharacteristic(Characteristic.SupportedVideoStreamConfiguration); + this.addCharacteristic(Characteristic.SelectedRTPStreamConfiguration) + this.addCharacteristic(Characteristic.SetupEndpoints) + this.addCharacteristic(Characteristic.StreamingStatus) + this.addCharacteristic(Characteristic.SupportedAudioStreamConfiguration) + this.addCharacteristic(Characteristic.SupportedRTPConfiguration) + this.addCharacteristic(Characteristic.SupportedVideoStreamConfiguration) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Active); + this.addOptionalCharacteristic(Characteristic.Active) } } -Service.CameraRTPStreamManagement = CameraRTPStreamManagement; +Service.CameraRTPStreamManagement = CameraRTPStreamManagement /** * Service "Carbon Dioxide Sensor" */ export class CarbonDioxideSensor extends Service { - - public static readonly UUID: string = "00000097-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000097-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, CarbonDioxideSensor.UUID, subtype); + super(displayName, CarbonDioxideSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CarbonDioxideDetected); + this.addCharacteristic(Characteristic.CarbonDioxideDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CarbonDioxideLevel); - this.addOptionalCharacteristic(Characteristic.CarbonDioxidePeakLevel); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.CarbonDioxideLevel) + this.addOptionalCharacteristic(Characteristic.CarbonDioxidePeakLevel) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.CarbonDioxideSensor = CarbonDioxideSensor; +Service.CarbonDioxideSensor = CarbonDioxideSensor /** * Service "Carbon Monoxide Sensor" */ export class CarbonMonoxideSensor extends Service { - - public static readonly UUID: string = "0000007F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000007F-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, CarbonMonoxideSensor.UUID, subtype); + super(displayName, CarbonMonoxideSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CarbonMonoxideDetected); + this.addCharacteristic(Characteristic.CarbonMonoxideDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CarbonMonoxideLevel); - this.addOptionalCharacteristic(Characteristic.CarbonMonoxidePeakLevel); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.CarbonMonoxideLevel) + this.addOptionalCharacteristic(Characteristic.CarbonMonoxidePeakLevel) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.CarbonMonoxideSensor = CarbonMonoxideSensor; +Service.CarbonMonoxideSensor = CarbonMonoxideSensor /** * Service "Contact Sensor" */ export class ContactSensor extends Service { - - public static readonly UUID: string = "00000080-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000080-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, ContactSensor.UUID, subtype); + super(displayName, ContactSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ContactSensorState); + this.addCharacteristic(Characteristic.ContactSensorState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.ContactSensor = ContactSensor; +Service.ContactSensor = ContactSensor /** * Service "Data Stream Transport Management" */ export class DataStreamTransportManagement extends Service { - - public static readonly UUID: string = "00000129-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000129-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, DataStreamTransportManagement.UUID, subtype); + super(displayName, DataStreamTransportManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SetupDataStreamTransport); - this.addCharacteristic(Characteristic.SupportedDataStreamTransportConfiguration); - this.addCharacteristic(Characteristic.Version); + this.addCharacteristic(Characteristic.SetupDataStreamTransport) + this.addCharacteristic(Characteristic.SupportedDataStreamTransportConfiguration) + this.addCharacteristic(Characteristic.Version) } } -Service.DataStreamTransportManagement = DataStreamTransportManagement; +Service.DataStreamTransportManagement = DataStreamTransportManagement /** * Service "Diagnostics" */ export class Diagnostics extends Service { - - public static readonly UUID: string = "00000237-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000237-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Diagnostics.UUID, subtype); + super(displayName, Diagnostics.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SupportedDiagnosticsSnapshot); + this.addCharacteristic(Characteristic.SupportedDiagnosticsSnapshot) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.SelectedDiagnosticsModes); - this.addOptionalCharacteristic(Characteristic.SupportedDiagnosticsModes); + this.addOptionalCharacteristic(Characteristic.SelectedDiagnosticsModes) + this.addOptionalCharacteristic(Characteristic.SupportedDiagnosticsModes) } } -Service.Diagnostics = Diagnostics; +Service.Diagnostics = Diagnostics /** * Service "Door" */ export class Door extends Service { - - public static readonly UUID: string = "00000081-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000081-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Door.UUID, subtype); + super(displayName, Door.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentPosition); - this.addCharacteristic(Characteristic.PositionState); - this.addCharacteristic(Characteristic.TargetPosition); + this.addCharacteristic(Characteristic.CurrentPosition) + this.addCharacteristic(Characteristic.PositionState) + this.addCharacteristic(Characteristic.TargetPosition) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.ObstructionDetected); - this.addOptionalCharacteristic(Characteristic.HoldPosition); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.ObstructionDetected) + this.addOptionalCharacteristic(Characteristic.HoldPosition) } } -Service.Door = Door; +Service.Door = Door /** * Service "Doorbell" */ export class Doorbell extends Service { - - public static readonly UUID: string = "00000121-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000121-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Doorbell.UUID, subtype); + super(displayName, Doorbell.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ProgrammableSwitchEvent); + this.addCharacteristic(Characteristic.ProgrammableSwitchEvent) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Brightness); - this.addOptionalCharacteristic(Characteristic.Mute); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.OperatingStateResponse); - this.addOptionalCharacteristic(Characteristic.Volume); + this.addOptionalCharacteristic(Characteristic.Brightness) + this.addOptionalCharacteristic(Characteristic.Mute) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.OperatingStateResponse) + this.addOptionalCharacteristic(Characteristic.Volume) } } -Service.Doorbell = Doorbell; +Service.Doorbell = Doorbell /** * Service "Fan" */ export class Fan extends Service { - - public static readonly UUID: string = "00000040-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000040-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Fan.UUID, subtype); + super(displayName, Fan.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.On); + this.addCharacteristic(Characteristic.On) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RotationDirection); - this.addOptionalCharacteristic(Characteristic.RotationSpeed); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RotationDirection) + this.addOptionalCharacteristic(Characteristic.RotationSpeed) } } -Service.Fan = Fan; +Service.Fan = Fan /** * Service "Fanv2" */ export class Fanv2 extends Service { - - public static readonly UUID: string = "000000B7-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000B7-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Fanv2.UUID, subtype); + super(displayName, Fanv2.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); + this.addCharacteristic(Characteristic.Active) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CurrentFanState); - this.addOptionalCharacteristic(Characteristic.TargetFanState); - this.addOptionalCharacteristic(Characteristic.LockPhysicalControls); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RotationDirection); - this.addOptionalCharacteristic(Characteristic.RotationSpeed); - this.addOptionalCharacteristic(Characteristic.SwingMode); + this.addOptionalCharacteristic(Characteristic.CurrentFanState) + this.addOptionalCharacteristic(Characteristic.TargetFanState) + this.addOptionalCharacteristic(Characteristic.LockPhysicalControls) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RotationDirection) + this.addOptionalCharacteristic(Characteristic.RotationSpeed) + this.addOptionalCharacteristic(Characteristic.SwingMode) } } -Service.Fanv2 = Fanv2; +Service.Fanv2 = Fanv2 /** * Service "Faucet" */ export class Faucet extends Service { - - public static readonly UUID: string = "000000D7-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D7-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Faucet.UUID, subtype); + super(displayName, Faucet.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); + this.addCharacteristic(Characteristic.Active) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusFault); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusFault) } } -Service.Faucet = Faucet; +Service.Faucet = Faucet /** * Service "Filter Maintenance" */ export class FilterMaintenance extends Service { - - public static readonly UUID: string = "000000BA-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000BA-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, FilterMaintenance.UUID, subtype); + super(displayName, FilterMaintenance.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.FilterChangeIndication); + this.addCharacteristic(Characteristic.FilterChangeIndication) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.FilterLifeLevel); - this.addOptionalCharacteristic(Characteristic.ResetFilterIndication); - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.FilterLifeLevel) + this.addOptionalCharacteristic(Characteristic.ResetFilterIndication) + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.FilterMaintenance = FilterMaintenance; +Service.FilterMaintenance = FilterMaintenance /** * Service "Firmware Update" */ export class FirmwareUpdate extends Service { - - public static readonly UUID: string = "00000236-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000236-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, FirmwareUpdate.UUID, subtype); + super(displayName, FirmwareUpdate.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.FirmwareUpdateReadiness); - this.addCharacteristic(Characteristic.FirmwareUpdateStatus); + this.addCharacteristic(Characteristic.FirmwareUpdateReadiness) + this.addCharacteristic(Characteristic.FirmwareUpdateStatus) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.MatterFirmwareUpdateStatus); - this.addOptionalCharacteristic(Characteristic.StagedFirmwareVersion); - this.addOptionalCharacteristic(Characteristic.SupportedFirmwareUpdateConfiguration); + this.addOptionalCharacteristic(Characteristic.MatterFirmwareUpdateStatus) + this.addOptionalCharacteristic(Characteristic.StagedFirmwareVersion) + this.addOptionalCharacteristic(Characteristic.SupportedFirmwareUpdateConfiguration) } } -Service.FirmwareUpdate = FirmwareUpdate; +Service.FirmwareUpdate = FirmwareUpdate /** * Service "Garage Door Opener" */ export class GarageDoorOpener extends Service { - - public static readonly UUID: string = "00000041-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000041-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, GarageDoorOpener.UUID, subtype); + super(displayName, GarageDoorOpener.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentDoorState); - this.addCharacteristic(Characteristic.TargetDoorState); - this.addCharacteristic(Characteristic.ObstructionDetected); + this.addCharacteristic(Characteristic.CurrentDoorState) + this.addCharacteristic(Characteristic.TargetDoorState) + this.addCharacteristic(Characteristic.ObstructionDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.LockCurrentState); - this.addOptionalCharacteristic(Characteristic.LockTargetState); - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.LockCurrentState) + this.addOptionalCharacteristic(Characteristic.LockTargetState) + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.GarageDoorOpener = GarageDoorOpener; +Service.GarageDoorOpener = GarageDoorOpener /** * Service "Heater-Cooler" */ export class HeaterCooler extends Service { - - public static readonly UUID: string = "000000BC-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000BC-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, HeaterCooler.UUID, subtype); + super(displayName, HeaterCooler.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.CurrentHeaterCoolerState); - this.addCharacteristic(Characteristic.TargetHeaterCoolerState); - this.addCharacteristic(Characteristic.CurrentTemperature); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.CurrentHeaterCoolerState) + this.addCharacteristic(Characteristic.TargetHeaterCoolerState) + this.addCharacteristic(Characteristic.CurrentTemperature) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.LockPhysicalControls); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RotationSpeed); - this.addOptionalCharacteristic(Characteristic.SwingMode); - this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature); - this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); - this.addOptionalCharacteristic(Characteristic.TemperatureDisplayUnits); + this.addOptionalCharacteristic(Characteristic.LockPhysicalControls) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RotationSpeed) + this.addOptionalCharacteristic(Characteristic.SwingMode) + this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature) + this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature) + this.addOptionalCharacteristic(Characteristic.TemperatureDisplayUnits) } } -Service.HeaterCooler = HeaterCooler; +Service.HeaterCooler = HeaterCooler /** * Service "Humidifier-Dehumidifier" */ export class HumidifierDehumidifier extends Service { - - public static readonly UUID: string = "000000BD-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000BD-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, HumidifierDehumidifier.UUID, subtype); + super(displayName, HumidifierDehumidifier.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.CurrentHumidifierDehumidifierState); - this.addCharacteristic(Characteristic.TargetHumidifierDehumidifierState); - this.addCharacteristic(Characteristic.CurrentRelativeHumidity); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) + this.addCharacteristic(Characteristic.TargetHumidifierDehumidifierState) + this.addCharacteristic(Characteristic.CurrentRelativeHumidity) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.LockPhysicalControls); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold); - this.addOptionalCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold); - this.addOptionalCharacteristic(Characteristic.RotationSpeed); - this.addOptionalCharacteristic(Characteristic.SwingMode); - this.addOptionalCharacteristic(Characteristic.WaterLevel); + this.addOptionalCharacteristic(Characteristic.LockPhysicalControls) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold) + this.addOptionalCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold) + this.addOptionalCharacteristic(Characteristic.RotationSpeed) + this.addOptionalCharacteristic(Characteristic.SwingMode) + this.addOptionalCharacteristic(Characteristic.WaterLevel) } } -Service.HumidifierDehumidifier = HumidifierDehumidifier; +Service.HumidifierDehumidifier = HumidifierDehumidifier /** * Service "Humidity Sensor" */ export class HumiditySensor extends Service { - - public static readonly UUID: string = "00000082-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000082-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, HumiditySensor.UUID, subtype); + super(displayName, HumiditySensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentRelativeHumidity); + this.addCharacteristic(Characteristic.CurrentRelativeHumidity) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.HumiditySensor = HumiditySensor; +Service.HumiditySensor = HumiditySensor /** * Service "Input Source" */ export class InputSource extends Service { - - public static readonly UUID: string = "000000D9-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D9-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, InputSource.UUID, subtype); + super(displayName, InputSource.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ConfiguredName); - this.addCharacteristic(Characteristic.InputSourceType); - this.addCharacteristic(Characteristic.IsConfigured); + this.addCharacteristic(Characteristic.ConfiguredName) + this.addCharacteristic(Characteristic.InputSourceType) + this.addCharacteristic(Characteristic.IsConfigured) if (!this.testCharacteristic(Characteristic.Name)) { // workaround for Name characteristic collision in constructor - this.addCharacteristic(Characteristic.Name).updateValue("Unnamed Service"); + this.addCharacteristic(Characteristic.Name).updateValue('Unnamed Service') } - this.addCharacteristic(Characteristic.CurrentVisibilityState); + this.addCharacteristic(Characteristic.CurrentVisibilityState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Identifier); - this.addOptionalCharacteristic(Characteristic.InputDeviceType); - this.addOptionalCharacteristic(Characteristic.TargetVisibilityState); + this.addOptionalCharacteristic(Characteristic.Identifier) + this.addOptionalCharacteristic(Characteristic.InputDeviceType) + this.addOptionalCharacteristic(Characteristic.TargetVisibilityState) } } -Service.InputSource = InputSource; +Service.InputSource = InputSource /** * Service "Irrigation-System" */ export class IrrigationSystem extends Service { - - public static readonly UUID: string = "000000CF-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000CF-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, IrrigationSystem.UUID, subtype); + super(displayName, IrrigationSystem.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.ProgramMode); - this.addCharacteristic(Characteristic.InUse); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.ProgramMode) + this.addCharacteristic(Characteristic.InUse) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.RemainingDuration); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusFault); + this.addOptionalCharacteristic(Characteristic.RemainingDuration) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusFault) } } -Service.IrrigationSystem = IrrigationSystem; +Service.IrrigationSystem = IrrigationSystem /** * Service "Leak Sensor" */ export class LeakSensor extends Service { - - public static readonly UUID: string = "00000083-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000083-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, LeakSensor.UUID, subtype); + super(displayName, LeakSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.LeakDetected); + this.addCharacteristic(Characteristic.LeakDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.LeakSensor = LeakSensor; +Service.LeakSensor = LeakSensor /** * Service "Lightbulb" */ export class Lightbulb extends Service { - - public static readonly UUID: string = "00000043-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000043-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Lightbulb.UUID, subtype); + super(displayName, Lightbulb.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.On); + this.addCharacteristic(Characteristic.On) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Brightness); - this.addOptionalCharacteristic(Characteristic.CharacteristicValueActiveTransitionCount); - this.addOptionalCharacteristic(Characteristic.CharacteristicValueTransitionControl); - this.addOptionalCharacteristic(Characteristic.ColorTemperature); - this.addOptionalCharacteristic(Characteristic.Hue); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.Saturation); - this.addOptionalCharacteristic(Characteristic.SupportedCharacteristicValueTransitionConfiguration); + this.addOptionalCharacteristic(Characteristic.Brightness) + this.addOptionalCharacteristic(Characteristic.CharacteristicValueActiveTransitionCount) + this.addOptionalCharacteristic(Characteristic.CharacteristicValueTransitionControl) + this.addOptionalCharacteristic(Characteristic.ColorTemperature) + this.addOptionalCharacteristic(Characteristic.Hue) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.Saturation) + this.addOptionalCharacteristic(Characteristic.SupportedCharacteristicValueTransitionConfiguration) } } -Service.Lightbulb = Lightbulb; +Service.Lightbulb = Lightbulb /** * Service "Light Sensor" */ export class LightSensor extends Service { - - public static readonly UUID: string = "00000084-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000084-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, LightSensor.UUID, subtype); + super(displayName, LightSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentAmbientLightLevel); + this.addCharacteristic(Characteristic.CurrentAmbientLightLevel) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.LightSensor = LightSensor; +Service.LightSensor = LightSensor /** * Service "Lock Management" */ export class LockManagement extends Service { - - public static readonly UUID: string = "00000044-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000044-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, LockManagement.UUID, subtype); + super(displayName, LockManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.LockControlPoint); - this.addCharacteristic(Characteristic.Version); + this.addCharacteristic(Characteristic.LockControlPoint) + this.addCharacteristic(Characteristic.Version) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.AdministratorOnlyAccess); - this.addOptionalCharacteristic(Characteristic.AudioFeedback); - this.addOptionalCharacteristic(Characteristic.CurrentDoorState); - this.addOptionalCharacteristic(Characteristic.LockManagementAutoSecurityTimeout); - this.addOptionalCharacteristic(Characteristic.LockLastKnownAction); - this.addOptionalCharacteristic(Characteristic.Logs); - this.addOptionalCharacteristic(Characteristic.MotionDetected); + this.addOptionalCharacteristic(Characteristic.AdministratorOnlyAccess) + this.addOptionalCharacteristic(Characteristic.AudioFeedback) + this.addOptionalCharacteristic(Characteristic.CurrentDoorState) + this.addOptionalCharacteristic(Characteristic.LockManagementAutoSecurityTimeout) + this.addOptionalCharacteristic(Characteristic.LockLastKnownAction) + this.addOptionalCharacteristic(Characteristic.Logs) + this.addOptionalCharacteristic(Characteristic.MotionDetected) } } -Service.LockManagement = LockManagement; +Service.LockManagement = LockManagement /** * Service "Lock Mechanism" */ export class LockMechanism extends Service { - - public static readonly UUID: string = "00000045-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000045-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, LockMechanism.UUID, subtype); + super(displayName, LockMechanism.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.LockCurrentState); - this.addCharacteristic(Characteristic.LockTargetState); + this.addCharacteristic(Characteristic.LockCurrentState) + this.addCharacteristic(Characteristic.LockTargetState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.LockMechanism = LockMechanism; +Service.LockMechanism = LockMechanism /** * Service "Microphone" */ export class Microphone extends Service { - - public static readonly UUID: string = "00000112-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000112-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Microphone.UUID, subtype); + super(displayName, Microphone.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Mute); + this.addCharacteristic(Characteristic.Mute) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Volume); + this.addOptionalCharacteristic(Characteristic.Volume) } } -Service.Microphone = Microphone; +Service.Microphone = Microphone /** * Service "Motion Sensor" */ export class MotionSensor extends Service { - - public static readonly UUID: string = "00000085-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000085-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, MotionSensor.UUID, subtype); + super(displayName, MotionSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.MotionDetected); + this.addCharacteristic(Characteristic.MotionDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.MotionSensor = MotionSensor; +Service.MotionSensor = MotionSensor /** * Service "NFC Access" * @since iOS 15 */ export class NFCAccess extends Service { - - public static readonly UUID: string = "00000266-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000266-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, NFCAccess.UUID, subtype); + super(displayName, NFCAccess.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ConfigurationState); - this.addCharacteristic(Characteristic.NFCAccessControlPoint); - this.addCharacteristic(Characteristic.NFCAccessSupportedConfiguration); + this.addCharacteristic(Characteristic.ConfigurationState) + this.addCharacteristic(Characteristic.NFCAccessControlPoint) + this.addCharacteristic(Characteristic.NFCAccessSupportedConfiguration) } } -Service.NFCAccess = NFCAccess; +Service.NFCAccess = NFCAccess /** * Service "Occupancy Sensor" */ export class OccupancySensor extends Service { - - public static readonly UUID: string = "00000086-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000086-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, OccupancySensor.UUID, subtype); + super(displayName, OccupancySensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.OccupancyDetected); + this.addCharacteristic(Characteristic.OccupancyDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.OccupancySensor = OccupancySensor; +Service.OccupancySensor = OccupancySensor /** * Service "Outlet" * @since iOS 13 */ export class Outlet extends Service { - - public static readonly UUID: string = "00000047-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000047-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Outlet.UUID, subtype); + super(displayName, Outlet.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.On); + this.addCharacteristic(Characteristic.On) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.OutletInUse); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.OutletInUse) } } -Service.Outlet = Outlet; +Service.Outlet = Outlet /** * Service "Pairing" */ export class Pairing extends Service { - - public static readonly UUID: string = "00000055-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000055-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Pairing.UUID, subtype); + super(displayName, Pairing.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ListPairings); - this.addCharacteristic(Characteristic.PairSetup); - this.addCharacteristic(Characteristic.PairVerify); - this.addCharacteristic(Characteristic.PairingFeatures); + this.addCharacteristic(Characteristic.ListPairings) + this.addCharacteristic(Characteristic.PairSetup) + this.addCharacteristic(Characteristic.PairVerify) + this.addCharacteristic(Characteristic.PairingFeatures) } } -Service.Pairing = Pairing; +Service.Pairing = Pairing /** * Service "Power Management" */ export class PowerManagement extends Service { - - public static readonly UUID: string = "00000221-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000221-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, PowerManagement.UUID, subtype); + super(displayName, PowerManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.WakeConfiguration); + this.addCharacteristic(Characteristic.WakeConfiguration) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.SelectedSleepConfiguration); - this.addOptionalCharacteristic(Characteristic.SupportedSleepConfiguration); + this.addOptionalCharacteristic(Characteristic.SelectedSleepConfiguration) + this.addOptionalCharacteristic(Characteristic.SupportedSleepConfiguration) } } -Service.PowerManagement = PowerManagement; +Service.PowerManagement = PowerManagement /** * Service "Protocol Information" */ export class ProtocolInformation extends Service { - - public static readonly UUID: string = "000000A2-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000A2-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, ProtocolInformation.UUID, subtype); + super(displayName, ProtocolInformation.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Version); + this.addCharacteristic(Characteristic.Version) } } -Service.ProtocolInformation = ProtocolInformation; +Service.ProtocolInformation = ProtocolInformation /** * Service "Security System" */ export class SecuritySystem extends Service { - - public static readonly UUID: string = "0000007E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000007E-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, SecuritySystem.UUID, subtype); + super(displayName, SecuritySystem.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SecuritySystemCurrentState); - this.addCharacteristic(Characteristic.SecuritySystemTargetState); + this.addCharacteristic(Characteristic.SecuritySystemCurrentState) + this.addCharacteristic(Characteristic.SecuritySystemTargetState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.SecuritySystemAlarmType); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.SecuritySystemAlarmType) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.SecuritySystem = SecuritySystem; +Service.SecuritySystem = SecuritySystem /** * Service "Service Label" */ export class ServiceLabel extends Service { - - public static readonly UUID: string = "000000CC-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000CC-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, ServiceLabel.UUID, subtype); + super(displayName, ServiceLabel.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ServiceLabelNamespace); + this.addCharacteristic(Characteristic.ServiceLabelNamespace) } } -Service.ServiceLabel = ServiceLabel; +Service.ServiceLabel = ServiceLabel /** * Service "Siri" */ export class Siri extends Service { - - public static readonly UUID: string = "00000133-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000133-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Siri.UUID, subtype); + super(displayName, Siri.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SiriInputType); + this.addCharacteristic(Characteristic.SiriInputType) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.MultifunctionButton); - this.addOptionalCharacteristic(Characteristic.SiriEnable); - this.addOptionalCharacteristic(Characteristic.SiriEngineVersion); - this.addOptionalCharacteristic(Characteristic.SiriLightOnUse); - this.addOptionalCharacteristic(Characteristic.SiriListening); - this.addOptionalCharacteristic(Characteristic.SiriTouchToUse); + this.addOptionalCharacteristic(Characteristic.MultifunctionButton) + this.addOptionalCharacteristic(Characteristic.SiriEnable) + this.addOptionalCharacteristic(Characteristic.SiriEngineVersion) + this.addOptionalCharacteristic(Characteristic.SiriLightOnUse) + this.addOptionalCharacteristic(Characteristic.SiriListening) + this.addOptionalCharacteristic(Characteristic.SiriTouchToUse) } } -Service.Siri = Siri; +Service.Siri = Siri /** * Service "Siri Endpoint" */ export class SiriEndpoint extends Service { - - public static readonly UUID: string = "00000253-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000253-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, SiriEndpoint.UUID, subtype); + super(displayName, SiriEndpoint.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SiriEndpointSessionStatus); - this.addCharacteristic(Characteristic.Version); + this.addCharacteristic(Characteristic.SiriEndpointSessionStatus) + this.addCharacteristic(Characteristic.Version) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.ActiveIdentifier); - this.addOptionalCharacteristic(Characteristic.ManuallyDisabled); + this.addOptionalCharacteristic(Characteristic.ActiveIdentifier) + this.addOptionalCharacteristic(Characteristic.ManuallyDisabled) } } -Service.SiriEndpoint = SiriEndpoint; +Service.SiriEndpoint = SiriEndpoint /** * Service "Slats" */ export class Slats extends Service { - - public static readonly UUID: string = "000000B9-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000B9-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Slats.UUID, subtype); + super(displayName, Slats.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentSlatState); - this.addCharacteristic(Characteristic.SlatType); + this.addCharacteristic(Characteristic.CurrentSlatState) + this.addCharacteristic(Characteristic.SlatType) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.SwingMode); - this.addOptionalCharacteristic(Characteristic.CurrentTiltAngle); - this.addOptionalCharacteristic(Characteristic.TargetTiltAngle); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.SwingMode) + this.addOptionalCharacteristic(Characteristic.CurrentTiltAngle) + this.addOptionalCharacteristic(Characteristic.TargetTiltAngle) } } -Service.Slats = Slats; +Service.Slats = Slats /** * Service "Smart Speaker" */ export class SmartSpeaker extends Service { - - public static readonly UUID: string = "00000228-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000228-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, SmartSpeaker.UUID, subtype); + super(displayName, SmartSpeaker.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentMediaState); - this.addCharacteristic(Characteristic.TargetMediaState); + this.addCharacteristic(Characteristic.CurrentMediaState) + this.addCharacteristic(Characteristic.TargetMediaState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.AirPlayEnable); - this.addOptionalCharacteristic(Characteristic.ConfiguredName); - this.addOptionalCharacteristic(Characteristic.Mute); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.Volume); + this.addOptionalCharacteristic(Characteristic.AirPlayEnable) + this.addOptionalCharacteristic(Characteristic.ConfiguredName) + this.addOptionalCharacteristic(Characteristic.Mute) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.Volume) } } -Service.SmartSpeaker = SmartSpeaker; +Service.SmartSpeaker = SmartSpeaker /** * Service "Smoke Sensor" */ export class SmokeSensor extends Service { - - public static readonly UUID: string = "00000087-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000087-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, SmokeSensor.UUID, subtype); + super(displayName, SmokeSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SmokeDetected); + this.addCharacteristic(Characteristic.SmokeDetected) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.SmokeSensor = SmokeSensor; +Service.SmokeSensor = SmokeSensor /** * Service "Speaker" * @since iOS 10 */ export class Speaker extends Service { - - public static readonly UUID: string = "00000113-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000113-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Speaker.UUID, subtype); + super(displayName, Speaker.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Mute); + this.addCharacteristic(Characteristic.Mute) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Active); - this.addOptionalCharacteristic(Characteristic.Volume); + this.addOptionalCharacteristic(Characteristic.Active) + this.addOptionalCharacteristic(Characteristic.Volume) } } -Service.Speaker = Speaker; +Service.Speaker = Speaker /** * Service "Stateful Programmable Switch" */ export class StatefulProgrammableSwitch extends Service { - - public static readonly UUID: string = "00000088-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000088-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, StatefulProgrammableSwitch.UUID, subtype); + super(displayName, StatefulProgrammableSwitch.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ProgrammableSwitchEvent); - this.addCharacteristic(Characteristic.ProgrammableSwitchOutputState); + this.addCharacteristic(Characteristic.ProgrammableSwitchEvent) + this.addCharacteristic(Characteristic.ProgrammableSwitchOutputState) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.StatefulProgrammableSwitch = StatefulProgrammableSwitch; +Service.StatefulProgrammableSwitch = StatefulProgrammableSwitch /** * Service "Stateless Programmable Switch" */ export class StatelessProgrammableSwitch extends Service { - - public static readonly UUID: string = "00000089-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000089-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, StatelessProgrammableSwitch.UUID, subtype); + super(displayName, StatelessProgrammableSwitch.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ProgrammableSwitchEvent); + this.addCharacteristic(Characteristic.ProgrammableSwitchEvent) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.ServiceLabelIndex); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.ServiceLabelIndex) } } -Service.StatelessProgrammableSwitch = StatelessProgrammableSwitch; +Service.StatelessProgrammableSwitch = StatelessProgrammableSwitch /** * Service "Switch" */ export class Switch extends Service { - - public static readonly UUID: string = "00000049-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000049-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Switch.UUID, subtype); + super(displayName, Switch.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.On); + this.addCharacteristic(Characteristic.On) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.Switch = Switch; +Service.Switch = Switch /** * Service "Tap Management" */ export class TapManagement extends Service { - - public static readonly UUID: string = "0000022E-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022E-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TapManagement.UUID, subtype); + super(displayName, TapManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.CryptoHash); - this.addCharacteristic(Characteristic.TapType); - this.addCharacteristic(Characteristic.Token); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.CryptoHash) + this.addCharacteristic(Characteristic.TapType) + this.addCharacteristic(Characteristic.Token) } } -Service.TapManagement = TapManagement; +Service.TapManagement = TapManagement /** * Service "Target Control" */ export class TargetControl extends Service { - - public static readonly UUID: string = "00000125-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000125-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TargetControl.UUID, subtype); + super(displayName, TargetControl.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.ActiveIdentifier); - this.addCharacteristic(Characteristic.ButtonEvent); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.ActiveIdentifier) + this.addCharacteristic(Characteristic.ButtonEvent) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); + this.addOptionalCharacteristic(Characteristic.Name) } } -Service.TargetControl = TargetControl; +Service.TargetControl = TargetControl /** * Service "Target Control Management" */ export class TargetControlManagement extends Service { - - public static readonly UUID: string = "00000122-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000122-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TargetControlManagement.UUID, subtype); + super(displayName, TargetControlManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.TargetControlSupportedConfiguration); - this.addCharacteristic(Characteristic.TargetControlList); + this.addCharacteristic(Characteristic.TargetControlSupportedConfiguration) + this.addCharacteristic(Characteristic.TargetControlList) } } -Service.TargetControlManagement = TargetControlManagement; +Service.TargetControlManagement = TargetControlManagement /** * Service "Television" */ export class Television extends Service { - - public static readonly UUID: string = "000000D8-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D8-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Television.UUID, subtype); + super(displayName, Television.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.ActiveIdentifier); - this.addCharacteristic(Characteristic.ConfiguredName); - this.addCharacteristic(Characteristic.RemoteKey); - this.addCharacteristic(Characteristic.SleepDiscoveryMode); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.ActiveIdentifier) + this.addCharacteristic(Characteristic.ConfiguredName) + this.addCharacteristic(Characteristic.RemoteKey) + this.addCharacteristic(Characteristic.SleepDiscoveryMode) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Brightness); - this.addOptionalCharacteristic(Characteristic.ClosedCaptions); - this.addOptionalCharacteristic(Characteristic.DisplayOrder); - this.addOptionalCharacteristic(Characteristic.CurrentMediaState); - this.addOptionalCharacteristic(Characteristic.TargetMediaState); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.PictureMode); - this.addOptionalCharacteristic(Characteristic.PowerModeSelection); + this.addOptionalCharacteristic(Characteristic.Brightness) + this.addOptionalCharacteristic(Characteristic.ClosedCaptions) + this.addOptionalCharacteristic(Characteristic.DisplayOrder) + this.addOptionalCharacteristic(Characteristic.CurrentMediaState) + this.addOptionalCharacteristic(Characteristic.TargetMediaState) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.PictureMode) + this.addOptionalCharacteristic(Characteristic.PowerModeSelection) } } -Service.Television = Television; +Service.Television = Television /** * Service "Television Speaker" */ export class TelevisionSpeaker extends Service { - - public static readonly UUID: string = "00000113-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000113-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TelevisionSpeaker.UUID, subtype); + super(displayName, TelevisionSpeaker.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Mute); + this.addCharacteristic(Characteristic.Mute) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Active); - this.addOptionalCharacteristic(Characteristic.Volume); - this.addOptionalCharacteristic(Characteristic.VolumeControlType); - this.addOptionalCharacteristic(Characteristic.VolumeSelector); + this.addOptionalCharacteristic(Characteristic.Active) + this.addOptionalCharacteristic(Characteristic.Volume) + this.addOptionalCharacteristic(Characteristic.VolumeControlType) + this.addOptionalCharacteristic(Characteristic.VolumeSelector) } } -Service.TelevisionSpeaker = TelevisionSpeaker; +Service.TelevisionSpeaker = TelevisionSpeaker /** * Service "Temperature Sensor" */ export class TemperatureSensor extends Service { - - public static readonly UUID: string = "0000008A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000008A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TemperatureSensor.UUID, subtype); + super(displayName, TemperatureSensor.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentTemperature); + this.addCharacteristic(Characteristic.CurrentTemperature) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.StatusActive); - this.addOptionalCharacteristic(Characteristic.StatusFault); - this.addOptionalCharacteristic(Characteristic.StatusLowBattery); - this.addOptionalCharacteristic(Characteristic.StatusTampered); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.StatusActive) + this.addOptionalCharacteristic(Characteristic.StatusFault) + this.addOptionalCharacteristic(Characteristic.StatusLowBattery) + this.addOptionalCharacteristic(Characteristic.StatusTampered) } } -Service.TemperatureSensor = TemperatureSensor; +Service.TemperatureSensor = TemperatureSensor /** * Service "Thermostat" */ export class Thermostat extends Service { - - public static readonly UUID: string = "0000004A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000004A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Thermostat.UUID, subtype); + super(displayName, Thermostat.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentHeatingCoolingState); - this.addCharacteristic(Characteristic.TargetHeatingCoolingState); - this.addCharacteristic(Characteristic.CurrentTemperature); - this.addCharacteristic(Characteristic.TargetTemperature); - this.addCharacteristic(Characteristic.TemperatureDisplayUnits); + this.addCharacteristic(Characteristic.CurrentHeatingCoolingState) + this.addCharacteristic(Characteristic.TargetHeatingCoolingState) + this.addCharacteristic(Characteristic.CurrentTemperature) + this.addCharacteristic(Characteristic.TargetTemperature) + this.addCharacteristic(Characteristic.TemperatureDisplayUnits) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity); - this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity); - this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature); - this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity) + this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity) + this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature) + this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature) } } -Service.Thermostat = Thermostat; +Service.Thermostat = Thermostat /** * Service "Thread Transport" */ export class ThreadTransport extends Service { - - public static readonly UUID: string = "00000701-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000701-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, ThreadTransport.UUID, subtype); + super(displayName, ThreadTransport.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentTransport); - this.addCharacteristic(Characteristic.ThreadControlPoint); - this.addCharacteristic(Characteristic.ThreadNodeCapabilities); - this.addCharacteristic(Characteristic.ThreadStatus); + this.addCharacteristic(Characteristic.CurrentTransport) + this.addCharacteristic(Characteristic.ThreadControlPoint) + this.addCharacteristic(Characteristic.ThreadNodeCapabilities) + this.addCharacteristic(Characteristic.ThreadStatus) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CCAEnergyDetectThreshold); - this.addOptionalCharacteristic(Characteristic.CCASignalDetectThreshold); - this.addOptionalCharacteristic(Characteristic.EventRetransmissionMaximum); - this.addOptionalCharacteristic(Characteristic.EventTransmissionCounters); - this.addOptionalCharacteristic(Characteristic.MACRetransmissionMaximum); - this.addOptionalCharacteristic(Characteristic.MACTransmissionCounters); - this.addOptionalCharacteristic(Characteristic.ReceiverSensitivity); - this.addOptionalCharacteristic(Characteristic.ReceivedSignalStrengthIndication); - this.addOptionalCharacteristic(Characteristic.SignalToNoiseRatio); - this.addOptionalCharacteristic(Characteristic.ThreadOpenThreadVersion); - this.addOptionalCharacteristic(Characteristic.TransmitPower); - this.addOptionalCharacteristic(Characteristic.MaximumTransmitPower); + this.addOptionalCharacteristic(Characteristic.CCAEnergyDetectThreshold) + this.addOptionalCharacteristic(Characteristic.CCASignalDetectThreshold) + this.addOptionalCharacteristic(Characteristic.EventRetransmissionMaximum) + this.addOptionalCharacteristic(Characteristic.EventTransmissionCounters) + this.addOptionalCharacteristic(Characteristic.MACRetransmissionMaximum) + this.addOptionalCharacteristic(Characteristic.MACTransmissionCounters) + this.addOptionalCharacteristic(Characteristic.ReceiverSensitivity) + this.addOptionalCharacteristic(Characteristic.ReceivedSignalStrengthIndication) + this.addOptionalCharacteristic(Characteristic.SignalToNoiseRatio) + this.addOptionalCharacteristic(Characteristic.ThreadOpenThreadVersion) + this.addOptionalCharacteristic(Characteristic.TransmitPower) + this.addOptionalCharacteristic(Characteristic.MaximumTransmitPower) } } -Service.ThreadTransport = ThreadTransport; +Service.ThreadTransport = ThreadTransport /** * Service "Transfer Transport Management" */ export class TransferTransportManagement extends Service { - - public static readonly UUID: string = "00000203-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '00000203-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, TransferTransportManagement.UUID, subtype); + super(displayName, TransferTransportManagement.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.SupportedTransferTransportConfiguration); - this.addCharacteristic(Characteristic.SetupTransferTransport); + this.addCharacteristic(Characteristic.SupportedTransferTransportConfiguration) + this.addCharacteristic(Characteristic.SetupTransferTransport) } } -Service.TransferTransportManagement = TransferTransportManagement; +Service.TransferTransportManagement = TransferTransportManagement /** * Service "Valve" */ export class Valve extends Service { - - public static readonly UUID: string = "000000D0-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '000000D0-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Valve.UUID, subtype); + super(displayName, Valve.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.Active); - this.addCharacteristic(Characteristic.InUse); - this.addCharacteristic(Characteristic.ValveType); + this.addCharacteristic(Characteristic.Active) + this.addCharacteristic(Characteristic.InUse) + this.addCharacteristic(Characteristic.ValveType) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.IsConfigured); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.RemainingDuration); - this.addOptionalCharacteristic(Characteristic.ServiceLabelIndex); - this.addOptionalCharacteristic(Characteristic.SetDuration); - this.addOptionalCharacteristic(Characteristic.StatusFault); + this.addOptionalCharacteristic(Characteristic.IsConfigured) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.RemainingDuration) + this.addOptionalCharacteristic(Characteristic.ServiceLabelIndex) + this.addOptionalCharacteristic(Characteristic.SetDuration) + this.addOptionalCharacteristic(Characteristic.StatusFault) } } -Service.Valve = Valve; +Service.Valve = Valve /** * Service "Wi-Fi Router" */ export class WiFiRouter extends Service { - - public static readonly UUID: string = "0000020A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000020A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, WiFiRouter.UUID, subtype); + super(displayName, WiFiRouter.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.ConfiguredName); - this.addCharacteristic(Characteristic.ManagedNetworkEnable); - this.addCharacteristic(Characteristic.NetworkAccessViolationControl); - this.addCharacteristic(Characteristic.NetworkClientProfileControl); - this.addCharacteristic(Characteristic.NetworkClientStatusControl); - this.addCharacteristic(Characteristic.RouterStatus); - this.addCharacteristic(Characteristic.SupportedRouterConfiguration); - this.addCharacteristic(Characteristic.WANConfigurationList); - this.addCharacteristic(Characteristic.WANStatusList); + this.addCharacteristic(Characteristic.ConfiguredName) + this.addCharacteristic(Characteristic.ManagedNetworkEnable) + this.addCharacteristic(Characteristic.NetworkAccessViolationControl) + this.addCharacteristic(Characteristic.NetworkClientProfileControl) + this.addCharacteristic(Characteristic.NetworkClientStatusControl) + this.addCharacteristic(Characteristic.RouterStatus) + this.addCharacteristic(Characteristic.SupportedRouterConfiguration) + this.addCharacteristic(Characteristic.WANConfigurationList) + this.addCharacteristic(Characteristic.WANStatusList) } } -Service.WiFiRouter = WiFiRouter; +Service.WiFiRouter = WiFiRouter /** * Service "Wi-Fi Satellite" */ export class WiFiSatellite extends Service { - - public static readonly UUID: string = "0000020F-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000020F-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, WiFiSatellite.UUID, subtype); + super(displayName, WiFiSatellite.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.WiFiSatelliteStatus); + this.addCharacteristic(Characteristic.WiFiSatelliteStatus) } } -Service.WiFiSatellite = WiFiSatellite; +Service.WiFiSatellite = WiFiSatellite /** * Service "Wi-Fi Transport" */ export class WiFiTransport extends Service { - - public static readonly UUID: string = "0000022A-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000022A-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, WiFiTransport.UUID, subtype); + super(displayName, WiFiTransport.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentTransport); - this.addCharacteristic(Characteristic.WiFiCapabilities); + this.addCharacteristic(Characteristic.CurrentTransport) + this.addCharacteristic(Characteristic.WiFiCapabilities) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.WiFiConfigurationControl); + this.addOptionalCharacteristic(Characteristic.WiFiConfigurationControl) } } -Service.WiFiTransport = WiFiTransport; +Service.WiFiTransport = WiFiTransport /** * Service "Window" */ export class Window extends Service { - - public static readonly UUID: string = "0000008B-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000008B-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, Window.UUID, subtype); + super(displayName, Window.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentPosition); - this.addCharacteristic(Characteristic.PositionState); - this.addCharacteristic(Characteristic.TargetPosition); + this.addCharacteristic(Characteristic.CurrentPosition) + this.addCharacteristic(Characteristic.PositionState) + this.addCharacteristic(Characteristic.TargetPosition) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.ObstructionDetected); - this.addOptionalCharacteristic(Characteristic.HoldPosition); + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.ObstructionDetected) + this.addOptionalCharacteristic(Characteristic.HoldPosition) } } -Service.Window = Window; +Service.Window = Window /** * Service "Window Covering" */ export class WindowCovering extends Service { - - public static readonly UUID: string = "0000008C-0000-1000-8000-0026BB765291"; + public static readonly UUID: string = '0000008C-0000-1000-8000-0026BB765291' constructor(displayName?: string, subtype?: string) { - super(displayName, WindowCovering.UUID, subtype); + super(displayName, WindowCovering.UUID, subtype) // Required Characteristics - this.addCharacteristic(Characteristic.CurrentPosition); - this.addCharacteristic(Characteristic.PositionState); - this.addCharacteristic(Characteristic.TargetPosition); + this.addCharacteristic(Characteristic.CurrentPosition) + this.addCharacteristic(Characteristic.PositionState) + this.addCharacteristic(Characteristic.TargetPosition) // Optional Characteristics - this.addOptionalCharacteristic(Characteristic.CurrentHorizontalTiltAngle); - this.addOptionalCharacteristic(Characteristic.TargetHorizontalTiltAngle); - this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.ObstructionDetected); - this.addOptionalCharacteristic(Characteristic.HoldPosition); - this.addOptionalCharacteristic(Characteristic.CurrentVerticalTiltAngle); - this.addOptionalCharacteristic(Characteristic.TargetVerticalTiltAngle); + this.addOptionalCharacteristic(Characteristic.CurrentHorizontalTiltAngle) + this.addOptionalCharacteristic(Characteristic.TargetHorizontalTiltAngle) + this.addOptionalCharacteristic(Characteristic.Name) + this.addOptionalCharacteristic(Characteristic.ObstructionDetected) + this.addOptionalCharacteristic(Characteristic.HoldPosition) + this.addOptionalCharacteristic(Characteristic.CurrentVerticalTiltAngle) + this.addOptionalCharacteristic(Characteristic.TargetVerticalTiltAngle) } } -Service.WindowCovering = WindowCovering; - +Service.WindowCovering = WindowCovering diff --git a/src/lib/definitions/generate-definitions.ts b/src/lib/definitions/generate-definitions.ts index 643863a2e..3ab316efd 100644 --- a/src/lib/definitions/generate-definitions.ts +++ b/src/lib/definitions/generate-definitions.ts @@ -1,13 +1,16 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import "./CharacteristicDefinitions"; - -import assert from "assert"; -import { Command } from "commander"; -import fs from "fs"; -import path from "path"; -import { readFileSync } from "simple-plist"; -import { Access, Characteristic, Formats, Units } from "../Characteristic"; -import { toLongForm } from "../util/uuid"; +/* eslint-disable no-console */ +import assert from 'node:assert' +import { createWriteStream, existsSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +import { Command } from 'commander' +import { readFileSync as readPlistFileSync } from 'simple-plist' + +import * as Characteristic from '../Characteristic.js' +import { toLongForm } from '../util/uuid.js' +import './CharacteristicDefinitions.js' import { CharacteristicClassAdditions, CharacteristicDeprecatedNames, @@ -22,268 +25,267 @@ import { ServiceManualAdditions, ServiceNameOverrides, ServiceSinceInformation, -} from "./generator-configuration"; +} from './generator-configuration.js' -// noinspection JSUnusedLocalSymbols -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const temp = Characteristic; // this to have "../Characteristic" not being only type import, otherwise this would not result in a require statement +console.log('Generating definitions...') -const command = new Command("generate-definitions") - .version("1.0.0") - .option("-f, --force") - .option("-m, --metadata ", "Define a custom location for the plain-metadata.config file", - "/System/Library/PrivateFrameworks/HomeKitDaemon.framework/Resources/plain-metadata.config") - .requiredOption("-s, --simulator ", "Define the path to the accessory simulator."); +// eslint-disable-next-line unused-imports/no-unused-vars +const temp = Characteristic // this to have "../Characteristic" not being only type import, otherwise this would not result in a require statement -command.parse(process.argv); -const options = command.opts(); +const __dirname = dirname(fileURLToPath(import.meta.url)) -const metadataFile: string = options.metadata; -const simulator: string = options.simulator; -if (!fs.existsSync(metadataFile)) { - console.warn(`The metadata file at '${metadataFile}' does not exist!`); - process.exit(1); +const command = new Command('generate-definitions') + .version('1.0.0') + .option('-f, --force') + .option('-m, --metadata ', 'Define a custom location for the plain-metadata.config file', '/System/Library/PrivateFrameworks/HomeKitDaemon.framework/Resources/plain-metadata.config') + .requiredOption('-s, --simulator ', 'Define the path to the accessory simulator.') + +command.parse(process.argv) +const options = command.opts() + +const metadataFile: string = options.metadata +const simulator: string = options.simulator +if (!existsSync(metadataFile)) { + console.warn(`The metadata file at '${metadataFile}' does not exist!`) + process.exit(1) } -if (!fs.existsSync(simulator)) { - console.warn(`The simulator app directory '${simulator}' does not exist!`); - process.exit(1); +if (!existsSync(simulator)) { + console.warn(`The simulator app directory '${simulator}' does not exist!`) + process.exit(1) } -const defaultPlist: string = path.resolve(simulator, "Contents/Frameworks/HAPAccessoryKit.framework/Resources/default.metadata.plist"); -const defaultMfiPlist: string = path.resolve(simulator, "Contents/Frameworks/HAPAccessoryKit.framework/Resources/default_mfi.metadata.plist"); +const defaultPlist: string = resolve(simulator, 'Contents/Frameworks/HAPAccessoryKit.framework/Resources/default.metadata.plist') +const defaultMfiPlist: string = resolve(simulator, 'Contents/Frameworks/HAPAccessoryKit.framework/Resources/default_mfi.metadata.plist') interface CharacteristicDefinition { - DefaultDescription: string, - Format: string, - LocalizationKey: string, - Properties: number, - ShortUUID: string, - MaxValue?: number, - MinValue?: number, - MaxLength?: number, + DefaultDescription: string + Format: string + LocalizationKey: string + Properties: number + ShortUUID: string + MaxValue?: number + MinValue?: number + MaxLength?: number // MinLength is another property present on the SerialNumber characteristic. Though we already have a special check for that - StepValue?: number, - Units?: string, + StepValue?: number + Units?: string } interface SimulatorCharacteristicDefinition { - UUID: string; - Name: string; - Format: string; - Constraints?: Constraints; - Permissions: string[]; // stuff like "securedRead", "securedWrite", "writeResponse" or "timedWrite" - Properties: string[]; // stuff like "read", "write", "cnotify", "uncnotify" + UUID: string + Name: string + Format: string + Constraints?: Constraints + Permissions: string[] // stuff like "securedRead", "securedWrite", "writeResponse" or "timedWrite" + Properties: string[] // stuff like "read", "write", "cnotify", "uncnotify" } interface Constraints { - StepValue?: number; - MaximumValue?: number; - MinimumValue?: number; - ValidValues?: Record; - ValidBits?: Record; + StepValue?: number + MaximumValue?: number + MinimumValue?: number + ValidValues?: Record + ValidBits?: Record } interface ServiceDefinition { Characteristics: { - Optional: string[], + Optional: string[] Required: string[] - }, - DefaultDescription: string, - LocalizationKey: string, - ShortUUID: string, + } + DefaultDescription: string + LocalizationKey: string + ShortUUID: string } interface PropertyDefinition { - DefaultDescription: string; - LocalizationKey: string; - Position: number; + DefaultDescription: string + LocalizationKey: string + Position: number } interface UnitDefinition { - DefaultDescription: string, - LocalizationKey: string; + DefaultDescription: string + LocalizationKey: string } interface CategoryDefinition { - DefaultDescription: string; - Identifier: number; - UUID: string; + DefaultDescription: string + Identifier: number + UUID: string } export interface GeneratedCharacteristic { - id: string; - UUID: string, - name: string, - className: string, - deprecatedClassName?: string; - since?: string, - deprecatedNotice?: string; + id: string + UUID: string + name: string + className: string + deprecatedClassName?: string + since?: string + deprecatedNotice?: string - format: string, - units?: string, - properties: number, - maxValue?: number, - minValue?: number, - stepValue?: number, - maxLength?: number, + format: string + units?: string + properties: number + maxValue?: number + minValue?: number + stepValue?: number + maxLength?: number - validValues?: Record; // - validBitMasks?: Record; + validValues?: Record // + validBitMasks?: Record - adminOnlyAccess?: Access[], + adminOnlyAccess?: Characteristic.Access[] - classAdditions?: string[], + classAdditions?: string[] } export interface GeneratedService { - id: string, - UUID: string, - name: string, - className: string, - deprecatedClassName?: string, - since?: string, - deprecatedNotice?: string, + id: string + UUID: string + name: string + className: string + deprecatedClassName?: string + since?: string + deprecatedNotice?: string - requiredCharacteristics: string[]; - optionalCharacteristics?: string[]; + requiredCharacteristics: string[] + optionalCharacteristics?: string[] } -/* eslint-disable @typescript-eslint/no-explicit-any */ -const plistData = readFileSync(metadataFile) as any; -const simulatorPlistData = readFileSync(defaultPlist)as any; -const simulatorMfiPlistData = fs.existsSync(defaultMfiPlist)? readFileSync(defaultMfiPlist): undefined as any; -/* eslint-enable @typescript-eslint/no-explicit-any */ +const plistData = readPlistFileSync(metadataFile) as any +const simulatorPlistData = readPlistFileSync(defaultPlist) as any +const simulatorMfiPlistData = existsSync(defaultMfiPlist) ? readPlistFileSync(defaultMfiPlist) : undefined as any if (plistData.SchemaVersion !== 1) { - console.warn(`Detected unsupported schema version ${plistData.SchemaVersion}!`); + console.warn(`Detected unsupported schema version ${plistData.SchemaVersion}!`) } if (plistData.PlistDictionary.SchemaVersion !== 1) { - console.warn(`Detect unsupported PlistDictionary schema version ${plistData.PlistDictionary.SchemaVersion}!`); + console.warn(`Detect unsupported PlistDictionary schema version ${plistData.PlistDictionary.SchemaVersion}!`) } -console.log(`Parsing version ${plistData.Version}...`); +console.log(`Parsing version ${plistData.Version}...`) -const shouldParseCharacteristics = checkWrittenVersion("./CharacteristicDefinitions.ts", plistData.Version); -const shouldParseServices = checkWrittenVersion("./ServiceDefinitions.ts", plistData.Version); +const shouldParseCharacteristics = checkWrittenVersion('./CharacteristicDefinitions.ts', plistData.Version) +const shouldParseServices = checkWrittenVersion('./ServiceDefinitions.ts', plistData.Version) if (!options.force && (!shouldParseCharacteristics || !shouldParseServices)) { - console.log("Parsed schema version " + plistData.Version + " is older than what's already generated. " + - "User --force option to generate and overwrite nonetheless!"); - process.exit(1); + console.log(`Parsed schema version ${plistData.Version} is older than what's already generated. ` + + `User --force option to generate and overwrite nonetheless!`) + process.exit(1) } -const undefinedUnits: string[] = ["micrograms/m^3", "ppm"]; +const undefinedUnits: string[] = ['micrograms/m^3', 'ppm'] -let characteristics: Record; -const simulatorCharacteristics: Map = new Map(); -let services: Record; -let units: Record; -let categories: Record; -const properties: Map = new Map(); +let characteristics: Record +const simulatorCharacteristics: Map = new Map() +let services: Record +let units: Record +let categories: Record +const properties: Map = new Map() try { - characteristics = checkDefined(plistData.PlistDictionary.HAP.Characteristics); - services = checkDefined(plistData.PlistDictionary.HAP.Services); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - units = checkDefined(plistData.PlistDictionary.HAP.Units); - categories = checkDefined(plistData.PlistDictionary.HomeKit.Categories); - - const props: Record = checkDefined(plistData.PlistDictionary.HAP.Properties); - // noinspection JSUnusedLocalSymbols - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [id, definition] of Object.entries(props).sort(([a, aDef], [b, bDef]) => aDef.Position - bDef.Position)) { - const perm = characteristicPerm(id); + characteristics = checkDefined(plistData.PlistDictionary.HAP.Characteristics) + services = checkDefined(plistData.PlistDictionary.HAP.Services) + + // eslint-disable-next-line unused-imports/no-unused-vars + units = checkDefined(plistData.PlistDictionary.HAP.Units) + categories = checkDefined(plistData.PlistDictionary.HomeKit.Categories) + + const props: Record = checkDefined(plistData.PlistDictionary.HAP.Properties) + for (const [id, definition] of Object.entries(props).sort(([, aDef], [, bDef]) => aDef.Position - bDef.Position)) { + const perm = characteristicPerm(id) if (perm) { - const num = 1 << definition.Position; - properties.set(num, perm); + const num = 1 << definition.Position + properties.set(num, perm) } } for (const characteristic of (simulatorPlistData.Characteristics as SimulatorCharacteristicDefinition[])) { - simulatorCharacteristics.set(characteristic.UUID, characteristic); + simulatorCharacteristics.set(characteristic.UUID, characteristic) } if (simulatorMfiPlistData) { for (const characteristic of (simulatorMfiPlistData.Characteristics as SimulatorCharacteristicDefinition[])) { - simulatorCharacteristics.set(characteristic.UUID, characteristic); + simulatorCharacteristics.set(characteristic.UUID, characteristic) } } } catch (error) { - console.log("Unexpected structure of the plist file!"); - throw error; + console.log('Unexpected structure of the plist file!') + throw error } // first step is to check if we are up to date on categories for (const definition of Object.values(categories)) { if (definition.Identifier > 36) { - console.log(`Detected a new category '${definition.DefaultDescription}' with id ${definition.Identifier}`); + console.log(`Detected a new category '${definition.DefaultDescription}' with id ${definition.Identifier}`) } } -const characteristicOutput = fs.createWriteStream(path.join(__dirname, "CharacteristicDefinitions.ts")); +const characteristicOutput = createWriteStream(join(__dirname, 'CharacteristicDefinitions.ts')) -characteristicOutput.write("// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n"); -characteristicOutput.write(`// V=${plistData.Version}\n`); -characteristicOutput.write("\n"); +characteristicOutput.write('// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n') +characteristicOutput.write(`// V=${plistData.Version}\n`) +characteristicOutput.write('\n') -characteristicOutput.write("import { Access, Characteristic, Formats, Perms, Units } from \"../Characteristic\";\n\n"); +characteristicOutput.write('import { Access, Characteristic, Formats, Perms, Units } from \'../Characteristic.js\'\n') /** * Characteristics */ -const generatedCharacteristics: Record = {}; // indexed by id -const writtenCharacteristicEntries: Record = {}; // indexed by class name +const generatedCharacteristics: Record = {} // indexed by id +const writtenCharacteristicEntries: Record = {} // indexed by class name for (const [id, definition] of Object.entries(characteristics)) { try { if (CharacteristicHidden.has(id)) { - continue; + continue } // "Carbon dioxide Detected" -> "Carbon Dioxide Detected" const name = (CharacteristicNameOverrides.get(id) - ?? definition.DefaultDescription).split(" ").map(entry => entry[0].toUpperCase() + entry.slice(1)).join(" "); - const deprecatedName = CharacteristicDeprecatedNames.get(id); + ?? definition.DefaultDescription).split(' ').map(entry => entry[0].toUpperCase() + entry.slice(1)).join(' ') + const deprecatedName = CharacteristicDeprecatedNames.get(id) // "Target Door State" -> "TargetDoorState", "PM2.5" -> "PM2_5" - const className = name.replace(/[\s-]/g, "").replace(/[.]/g, "_"); - const deprecatedClassName = deprecatedName?.replace(/[\s-]/g, "").replace(/[.]/g, "_"); - const longUUID = toLongForm(definition.ShortUUID); + const className = name.replace(/[\s-]/g, '').replace(/\./g, '_') + const deprecatedClassName = deprecatedName?.replace(/[\s-]/g, '').replace(/\./g, '_') + const longUUID = toLongForm(definition.ShortUUID) - const simulatorCharacteristic = simulatorCharacteristics.get(longUUID); + const simulatorCharacteristic = simulatorCharacteristics.get(longUUID) - const validValues = simulatorCharacteristic?.Constraints?.ValidValues || {}; - const validValuesOverride = CharacteristicValidValuesOverride.get(id); + const validValues = simulatorCharacteristic?.Constraints?.ValidValues || {} + const validValuesOverride = CharacteristicValidValuesOverride.get(id) if (validValuesOverride) { for (const [key, value] of Object.entries(validValuesOverride)) { - validValues[key] = value; + validValues[key] = value } } for (const [value, name] of Object.entries(validValues)) { - let constName = name.toUpperCase().replace(/[^\w]+/g, "_"); + let constName = name.toUpperCase().replace(/\W+/g, '_') if (/^[1-9]/.test(constName)) { - constName = "_" + constName; // variables can't start with a number + constName = `_${constName}` // variables can't start with a number } - validValues[value] = constName; + validValues[value] = constName } - const validBits = simulatorCharacteristic?.Constraints?.ValidBits; - let validBitMasks: Record | undefined = undefined; + const validBits = simulatorCharacteristic?.Constraints?.ValidBits + let validBitMasks: Record | undefined if (validBits) { - validBitMasks = {}; + validBitMasks = {} for (const [value, name] of Object.entries(validBits)) { - let constName = name.toUpperCase().replace(/[^\w]+/g, "_"); + let constName = name.toUpperCase().replace(/\W+/g, '_') if (/^[1-9]/.test(constName)) { - constName = "_" + constName; // variables can't start with a number + constName = `_${constName}` // variables can't start with a number } - validBitMasks["" + (1 << parseInt(value, 10))] = constName + "_BIT_MASK"; + validBitMasks[`${1 << Number.parseInt(value, 10)}`] = `${constName}_BIT_MASK` } } const generatedCharacteristic: GeneratedCharacteristic = { - id: id, + id, UUID: longUUID, - name: name, - className: className, - deprecatedClassName: deprecatedClassName, + name, + className, + deprecatedClassName, since: CharacteristicSinceInformation.get(id), format: definition.Format, @@ -295,166 +297,166 @@ for (const [id, definition] of Object.entries(characteristics)) { maxLength: definition.MaxLength, - validValues: validValues, - validBitMasks: validBitMasks, + validValues, + validBitMasks, classAdditions: CharacteristicClassAdditions.get(id), - }; + } // call any handler which wants to manually override properties of the generated characteristic - CharacteristicOverriding.get(id)?.(generatedCharacteristic); + CharacteristicOverriding.get(id)?.(generatedCharacteristic) - generatedCharacteristics[id] = generatedCharacteristic; - writtenCharacteristicEntries[className] = generatedCharacteristic; + generatedCharacteristics[id] = generatedCharacteristic + writtenCharacteristicEntries[className] = generatedCharacteristic if (deprecatedClassName) { - writtenCharacteristicEntries[deprecatedClassName] = generatedCharacteristic; + writtenCharacteristicEntries[deprecatedClassName] = generatedCharacteristic } } catch (error) { - throw new Error("Error thrown generating characteristic '" + id + "' (" + definition.DefaultDescription + "): " + error.message); + throw new Error(`Error thrown generating characteristic '${id}' (${definition.DefaultDescription}): ${error.message}`) } } for (const [id, generated] of CharacteristicManualAdditions) { - generatedCharacteristics[id] = generated; - writtenCharacteristicEntries[generated.className] = generated; + generatedCharacteristics[id] = generated + writtenCharacteristicEntries[generated.className] = generated if (generated.deprecatedClassName) { - writtenCharacteristicEntries[generated.deprecatedClassName] = generated; + writtenCharacteristicEntries[generated.deprecatedClassName] = generated } } for (const generated of Object.values(generatedCharacteristics) .sort((a, b) => a.className.localeCompare(b.className))) { try { - characteristicOutput.write("/**\n"); - characteristicOutput.write(" * Characteristic \"" + generated.name + "\"\n"); + characteristicOutput.write('\n/**\n') + characteristicOutput.write(` * Characteristic "${generated.name}"\n`) if (generated.since) { - characteristicOutput.write(" * @since iOS " + generated.since + "\n"); + characteristicOutput.write(` * @since iOS ${generated.since}\n`) } if (generated.deprecatedNotice) { - characteristicOutput.write(" * @deprecated " + generated.deprecatedNotice + "\n"); + characteristicOutput.write(` * @deprecated ${generated.deprecatedNotice}\n`) } - characteristicOutput.write(" */\n"); - + characteristicOutput.write(' */\n') - characteristicOutput.write("export class " + generated.className + " extends Characteristic {\n\n"); + characteristicOutput.write(`export class ${generated.className} extends Characteristic {\n`) - characteristicOutput.write(" public static readonly UUID: string = \"" + generated.UUID + "\";\n\n"); + characteristicOutput.write(` public static readonly UUID: string = '${generated.UUID}'\n\n`) - const classAdditions = generated.classAdditions; + const classAdditions = generated.classAdditions if (classAdditions) { - characteristicOutput.write(classAdditions.map(line => " " + line + "\n").join("") + "\n"); + characteristicOutput.write(`${classAdditions.map(line => ` ${line}\n`).join('')}\n`) } - const validValuesEntries = Object.entries(generated.validValues ?? {}); + const validValuesEntries = Object.entries(generated.validValues ?? {}) if (validValuesEntries.length) { for (const [value, name] of validValuesEntries) { if (!name) { - continue; + continue } - characteristicOutput.write(` public static readonly ${name} = ${value};\n`); + // fix a weird edge case + let printName = name + if (name === 'PROGRAM_SCHEDULED_MANUAL_MODE_') { + printName = 'PROGRAM_SCHEDULED_MANUAL_MODE' + } + characteristicOutput.write(` public static readonly ${printName} = ${value}\n`) } - characteristicOutput.write("\n"); + characteristicOutput.write('\n') } if (generated.validBitMasks) { for (const [value, name] of Object.entries(generated.validBitMasks)) { - characteristicOutput.write(` public static readonly ${name} = ${value};\n`); + characteristicOutput.write(` public static readonly ${name} = ${value}\n`) } - characteristicOutput.write("\n"); + characteristicOutput.write('\n') } - characteristicOutput.write(" constructor() {\n"); - characteristicOutput.write(" super(\"" + generated.name + "\", " + generated.className + ".UUID, {\n"); - characteristicOutput.write(" format: Formats." + characteristicFormat(generated.format) + ",\n"); - characteristicOutput.write(" perms: [" + generatePermsString(generated.id, generated.properties) + "],\n"); + characteristicOutput.write(' constructor() {\n') + characteristicOutput.write(` super('${generated.name}', ${generated.className}.UUID, {\n`) + characteristicOutput.write(` format: Formats.${characteristicFormat(generated.format)},\n`) + characteristicOutput.write(` perms: [${generatePermsString(generated.id, generated.properties)}],\n`) if (generated.units && !undefinedUnits.includes(generated.units)) { - characteristicOutput.write(" unit: Units." + characteristicUnit(generated.units) + ",\n"); + characteristicOutput.write(` unit: Units.${characteristicUnit(generated.units)},\n`) } if (generated.minValue != null) { - characteristicOutput.write(" minValue: " + generated.minValue + ",\n"); + characteristicOutput.write(` minValue: ${generated.minValue},\n`) } if (generated.maxValue != null) { - characteristicOutput.write(" maxValue: " + generated.maxValue + ",\n"); + characteristicOutput.write(` maxValue: ${generated.maxValue},\n`) } if (generated.stepValue != null) { - characteristicOutput.write(" minStep: " + generated.stepValue + ",\n"); + characteristicOutput.write(` minStep: ${generated.stepValue},\n`) } if (generated.maxLength != null) { - characteristicOutput.write(" maxLen: " + generated.maxLength + ",\n"); + characteristicOutput.write(` maxLen: ${generated.maxLength},\n`) } if (validValuesEntries.length) { - characteristicOutput.write(" validValues: [" + Object.keys(generated.validValues!).join(", ") + "],\n"); + characteristicOutput.write(` validValues: [${Object.keys(generated.validValues!).join(', ')}],\n`) } if (generated.adminOnlyAccess) { - characteristicOutput.write(" adminOnlyAccess: [" - + generated.adminOnlyAccess.map(value => "Access." + characteristicAccess(value)).join(", ") + "],\n"); + characteristicOutput.write(` adminOnlyAccess: [${ + generated.adminOnlyAccess.map(value => `Access.${characteristicAccess(value)}`).join(', ')}],\n`) } - characteristicOutput.write(" });\n"); - characteristicOutput.write(" this.value = this.getDefaultValue();\n"); - characteristicOutput.write(" }\n"); - characteristicOutput.write("}\n"); + characteristicOutput.write(' })\n') + characteristicOutput.write(' this.value = this.getDefaultValue()\n') + characteristicOutput.write(' }\n') + characteristicOutput.write('}\n') if (generated.deprecatedClassName) { - characteristicOutput.write("// noinspection JSDeprecatedSymbols\n"); - characteristicOutput.write("Characteristic." + generated.deprecatedClassName + " = " + generated.className + ";\n"); - } - if (generated.deprecatedNotice) { - characteristicOutput.write("// noinspection JSDeprecatedSymbols\n"); + characteristicOutput.write(`Characteristic.${generated.deprecatedClassName} = ${generated.className}\n`) } - characteristicOutput.write("Characteristic." + generated.className + " = " + generated.className + ";\n\n"); + characteristicOutput.write(`Characteristic.${generated.className} = ${generated.className}\n`) } catch (error) { - throw new Error("Error thrown writing characteristic '" + generated.id + "' (" + generated.className + "): " + error.message); + throw new Error(`Error thrown writing characteristic '${generated.id}' (${generated.className}): ${error.message}`) } } -characteristicOutput.end(); +characteristicOutput.end() -const characteristicProperties = Object.entries(writtenCharacteristicEntries).sort(([a], [b]) => a.localeCompare(b)); -rewriteProperties("Characteristic", characteristicProperties); -writeCharacteristicTestFile(); +const characteristicProperties = Object.entries(writtenCharacteristicEntries).sort(([a], [b]) => a.localeCompare(b)) +rewriteProperties('Characteristic', characteristicProperties) +writeCharacteristicTestFile() /** * Services */ -const serviceOutput = fs.createWriteStream(path.join(__dirname, "ServiceDefinitions.ts")); +const serviceOutput = createWriteStream(join(__dirname, 'ServiceDefinitions.ts')) -serviceOutput.write("// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n"); -serviceOutput.write(`// V=${plistData.Version}\n`); -serviceOutput.write("\n"); +serviceOutput.write('// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n') +serviceOutput.write(`// V=${plistData.Version}\n`) +serviceOutput.write('\n') -serviceOutput.write("import { Characteristic } from \"../Characteristic\";\n"); -serviceOutput.write("import { Service } from \"../Service\";\n\n"); +serviceOutput.write('import { Characteristic } from \'../Characteristic.js\'\n') +serviceOutput.write('import { Service } from \'../Service.js\'\n') -const generatedServices: Record = {}; // indexed by id -const writtenServiceEntries: Record = {}; // indexed by class name +const generatedServices: Record = {} // indexed by id +const writtenServiceEntries: Record = {} // indexed by class name for (const [id, definition] of Object.entries(services)) { try { // "Carbon dioxide Sensor" -> "Carbon Dioxide Sensor" - const name = (ServiceNameOverrides.get(id) ?? definition.DefaultDescription).split(" ").map(entry => entry[0].toUpperCase() + entry.slice(1)).join(" "); - const deprecatedName = ServiceDeprecatedNames.get(id); + const name = (ServiceNameOverrides.get(id) ?? definition.DefaultDescription).split(' ').map(entry => entry[0].toUpperCase() + entry.slice(1)).join(' ') + const deprecatedName = ServiceDeprecatedNames.get(id) - const className = name.replace(/[\s-]/g, "").replace(/[.]/g, "_"); - const deprecatedClassName = deprecatedName?.replace(/[\s-]/g, "").replace(/[.]/g, "_"); + const className = name.replace(/[\s-]/g, '').replace(/\./g, '_') + const deprecatedClassName = deprecatedName?.replace(/[\s-]/g, '').replace(/\./g, '_') - const longUUID = toLongForm(definition.ShortUUID); + const longUUID = toLongForm(definition.ShortUUID) - const requiredCharacteristics = definition.Characteristics.Required; - const optionalCharacteristics = definition.Characteristics.Optional; + const requiredCharacteristics = definition.Characteristics.Required + const optionalCharacteristics = definition.Characteristics.Optional - const configurationOverride = ServiceCharacteristicConfigurationOverrides.get(id); + const configurationOverride = ServiceCharacteristicConfigurationOverrides.get(id) if (configurationOverride) { if (configurationOverride.removedRequired) { for (const entry of configurationOverride.removedRequired) { - const index = requiredCharacteristics.indexOf(entry); + const index = requiredCharacteristics.indexOf(entry) if (index !== -1) { - requiredCharacteristics.splice(index, 1); + requiredCharacteristics.splice(index, 1) } } } if (configurationOverride.removedOptional) { for (const entry of configurationOverride.removedOptional) { - const index = optionalCharacteristics.indexOf(entry); + const index = optionalCharacteristics.indexOf(entry) if (index !== -1) { - optionalCharacteristics.splice(index, 1); + optionalCharacteristics.splice(index, 1) } } } @@ -462,379 +464,375 @@ for (const [id, definition] of Object.entries(services)) { if (configurationOverride.addedRequired) { for (const entry of configurationOverride.addedRequired) { if (!requiredCharacteristics.includes(entry)) { - requiredCharacteristics.push(entry); + requiredCharacteristics.push(entry) } } } if (configurationOverride.addedOptional) { for (const entry of configurationOverride.addedOptional) { if (!optionalCharacteristics.includes(entry)) { - optionalCharacteristics.push(entry); + optionalCharacteristics.push(entry) } } } } const generatedService: GeneratedService = { - id: id, + id, UUID: longUUID, - name: name, - className: className, - deprecatedClassName: deprecatedClassName, + name, + className, + deprecatedClassName, since: ServiceSinceInformation.get(id), - requiredCharacteristics: requiredCharacteristics, - optionalCharacteristics: optionalCharacteristics, - }; - generatedServices[id] = generatedService; - writtenServiceEntries[className] = generatedService; + requiredCharacteristics, + optionalCharacteristics, + } + generatedServices[id] = generatedService + writtenServiceEntries[className] = generatedService if (deprecatedClassName) { - writtenServiceEntries[deprecatedClassName] = generatedService; + writtenServiceEntries[deprecatedClassName] = generatedService } } catch (error) { - throw new Error("Error thrown generating service '" + id + "' (" + definition.DefaultDescription + "): " + error.message); + throw new Error(`Error thrown generating service '${id}' (${definition.DefaultDescription}): ${error.message}`) } } for (const [id, generated] of ServiceManualAdditions) { - generatedServices[id] = generated; - writtenServiceEntries[generated.className] = generated; + generatedServices[id] = generated + writtenServiceEntries[generated.className] = generated if (generated.deprecatedClassName) { - writtenServiceEntries[generated.deprecatedClassName] = generated; + writtenServiceEntries[generated.deprecatedClassName] = generated } } for (const generated of Object.values(generatedServices) .sort((a, b) => a.className.localeCompare(b.className))) { try { - serviceOutput.write("/**\n"); - serviceOutput.write(" * Service \"" + generated.name + "\"\n"); + serviceOutput.write('\n/**\n') + serviceOutput.write(` * Service "${generated.name}"\n`) if (generated.since) { - serviceOutput.write(" * @since iOS " + generated.since + "\n"); + serviceOutput.write(` * @since iOS ${generated.since}\n`) } if (generated.deprecatedNotice) { - serviceOutput.write(" * @deprecated " + generated.deprecatedNotice + "\n"); + serviceOutput.write(` * @deprecated ${generated.deprecatedNotice}\n`) } - serviceOutput.write(" */\n"); + serviceOutput.write(' */\n') - serviceOutput.write("export class " + generated.className + " extends Service {\n\n"); + serviceOutput.write(`export class ${generated.className} extends Service {\n`) - serviceOutput.write(" public static readonly UUID: string = \"" + generated.UUID + "\";\n\n"); + serviceOutput.write(` public static readonly UUID: string = '${generated.UUID}'\n\n`) - serviceOutput.write(" constructor(displayName?: string, subtype?: string) {\n"); - serviceOutput.write(" super(displayName, " + generated.className + ".UUID, subtype);\n\n"); + serviceOutput.write(' constructor(displayName?: string, subtype?: string) {\n') + serviceOutput.write(` super(displayName, ${generated.className}.UUID, subtype)\n\n`) - serviceOutput.write(" // Required Characteristics\n"); + serviceOutput.write(' // Required Characteristics\n') for (const required of generated.requiredCharacteristics) { - const characteristic = generatedCharacteristics[required]; + const characteristic = generatedCharacteristics[required] if (!characteristic) { - console.warn("Could not find required characteristic " + required + " for " + generated.className); - continue; + console.warn(`Could not find required characteristic ${required} for ${generated.className}`) + continue } - if (required === "name") { - serviceOutput.write(" if (!this.testCharacteristic(Characteristic.Name)) { // workaround for Name characteristic collision in constructor\n"); - serviceOutput.write(" this.addCharacteristic(Characteristic.Name).updateValue(\"Unnamed Service\");\n"); - serviceOutput.write(" }\n"); + if (required === 'name') { + serviceOutput.write(' if (!this.testCharacteristic(Characteristic.Name)) { // workaround for Name characteristic collision in constructor\n') + serviceOutput.write(' this.addCharacteristic(Characteristic.Name).updateValue(\'Unnamed Service\')\n') + serviceOutput.write(' }\n') } else { - serviceOutput.write(" this.addCharacteristic(Characteristic." + characteristic.className + ");\n"); + serviceOutput.write(` this.addCharacteristic(Characteristic.${characteristic.className})\n`) } } if (generated.optionalCharacteristics?.length) { - serviceOutput.write("\n // Optional Characteristics\n"); + serviceOutput.write('\n // Optional Characteristics\n') for (const optional of generated.optionalCharacteristics) { - const characteristic = generatedCharacteristics[optional]; + const characteristic = generatedCharacteristics[optional] if (!characteristic) { - console.warn("Could not find optional characteristic " + optional + " for " + generated.className); - continue; + console.warn(`Could not find optional characteristic ${optional} for ${generated.className}`) + continue } - serviceOutput.write(" this.addOptionalCharacteristic(Characteristic." + characteristic.className + ");\n"); + serviceOutput.write(` this.addOptionalCharacteristic(Characteristic.${characteristic.className})\n`) } } - serviceOutput.write(" }\n}\n"); + serviceOutput.write(' }\n}\n') if (generated.deprecatedClassName) { - serviceOutput.write("// noinspection JSDeprecatedSymbols\n"); - serviceOutput.write("Service." + generated.deprecatedClassName + " = " + generated.className + ";\n"); + serviceOutput.write(`Service.${generated.deprecatedClassName} = ${generated.className}\n`) } - if (generated.deprecatedNotice) { - serviceOutput.write("// noinspection JSDeprecatedSymbols\n"); - } - serviceOutput.write("Service." + generated.className + " = " + generated.className + ";\n\n"); + serviceOutput.write(`Service.${generated.className} = ${generated.className}\n`) } catch (error) { - throw new Error("Error thrown writing service '" + generated.id + "' (" + generated.className + "): " + error.message); + throw new Error(`Error thrown writing service '${generated.id}' (${generated.className}): ${error.message}`) } } -serviceOutput.end(); - +serviceOutput.end() -const serviceProperties = Object.entries(writtenServiceEntries).sort(([a], [b]) => a.localeCompare(b)); -rewriteProperties("Service", serviceProperties); -writeServicesTestFile(); +const serviceProperties = Object.entries(writtenServiceEntries).sort(([a], [b]) => a.localeCompare(b)) +rewriteProperties('Service', serviceProperties) +writeServicesTestFile() // ------------------------ utils ------------------------ function checkDefined(input: T): T { if (!input) { - throw new Error("value is undefined!"); + throw new Error('value is undefined!') } - return input; + return input } function characteristicFormat(format: string): string { // @ts-expect-error: forceConsistentCasingInFileNames compiler option - for (const [key, value] of Object.entries(Formats)) { + for (const [key, value] of Object.entries(Characteristic.Formats)) { if (value === format) { - return key; + return key } } - throw new Error("Unknown characteristic format '" + format + "'"); + throw new Error(`Unknown characteristic format '${format}'`) } function characteristicUnit(unit: string): string { // @ts-expect-error: forceConsistentCasingInFileNames compiler option - for (const [key, value] of Object.entries(Units)) { + for (const [key, value] of Object.entries(Characteristic.Units)) { if (value === unit) { - return key; + return key } } - throw new Error("Unknown characteristic format '" + unit + "'"); + throw new Error(`Unknown characteristic format '${unit}'`) } function characteristicAccess(access: number): string { // @ts-expect-error: forceConsistentCasingInFileNames compiler option - for (const [key, value] of Object.entries(Access)) { + for (const [key, value] of Object.entries(Characteristic.Access)) { if (value === access) { - return key; + return key } } - throw new Error("Unknown access for '" + access + "'"); + throw new Error(`Unknown access for '${access}'`) } function characteristicPerm(id: string): string | undefined { switch (id) { - case "aa": - return "ADDITIONAL_AUTHORIZATION"; - case "hidden": - return "HIDDEN"; - case "notify": - return "NOTIFY"; - case "read": - return "PAIRED_READ"; - case "timedWrite": - return "TIMED_WRITE"; - case "write": - return "PAIRED_WRITE"; - case "writeResponse": - return "WRITE_RESPONSE"; - case "broadcast": // used for bluetooth - return undefined; - case "adminOnly": - return undefined; // TODO add support for it (currently unused though) - default: - throw new Error("Received unknown perms id: " + id); + case 'aa': + return 'ADDITIONAL_AUTHORIZATION' + case 'hidden': + return 'HIDDEN' + case 'notify': + return 'NOTIFY' + case 'read': + return 'PAIRED_READ' + case 'timedWrite': + return 'TIMED_WRITE' + case 'write': + return 'PAIRED_WRITE' + case 'writeResponse': + return 'WRITE_RESPONSE' + case 'broadcast': // used for bluetooth + return undefined + case 'adminOnly': + return undefined // TODO add support for it (currently unused though) + default: + throw new Error(`Received unknown perms id: ${id}`) } } function generatePermsString(id: string, propertiesBitMap: number): string { - const perms: string [] = []; + const perms: string [] = [] for (const [bitMap, name] of properties) { - if (name === "ADDITIONAL_AUTHORIZATION") { - // aa set by homed just signals that aa may be supported. Setting up aa will always require a custom made app though - continue; + if (name === 'ADDITIONAL_AUTHORIZATION') { + // aa set by homed just signals that aa may be supported. Setting up aa will always require a custom-made app though + continue } if ((propertiesBitMap | bitMap) === propertiesBitMap) { // if it stays the same the bit is set - perms.push("Perms." + name); + perms.push(`Perms.${name}`) } } - const result = perms.join(", "); - assert(!!result, "perms string cannot be empty (" + propertiesBitMap + ")"); - return result; + const result = perms.join(', ') + assert(!!result, `perms string cannot be empty (${propertiesBitMap})`) + return result } function checkWrittenVersion(filePath: string, parsingVersion: number): boolean { - filePath = path.resolve(__dirname, filePath); + filePath = resolve(__dirname, filePath) - const content = fs.readFileSync(filePath, { encoding: "utf8" }).split("\n", 3); - const v = content[1]; - if (!v.startsWith("// V=")) { - throw new Error("Could not detect definition version for '" + filePath + "'"); + const content = readFileSync(filePath, { encoding: 'utf8' }).split('\n', 3) + const v = content[1] + if (!v.startsWith('// V=')) { + throw new Error(`Could not detect definition version for '${filePath}'`) } - const version = parseInt(v.replace("// V=", ""), 10); - return parsingVersion >= version; + const version = Number.parseInt(v.replace('// V=', ''), 10) + return parsingVersion >= version } function rewriteProperties(className: string, properties: [key: string, value: GeneratedCharacteristic | GeneratedService][]): void { - const filePath = path.resolve(__dirname, "../" + className + ".ts"); - if (!fs.existsSync(filePath)) { - throw new Error("File '" + filePath + "' does not exist!"); + const filePath = resolve(__dirname, `../${className}.ts`) + if (!existsSync(filePath)) { + throw new Error(`File '${filePath}' does not exist!`) } - const file = fs.readFileSync(filePath, { encoding: "utf8" }); - const lines = file.split("\n"); + const file = readFileSync(filePath, { encoding: 'utf8' }) + const lines = file.split('\n') - let i = 0; + let i = 0 - let importStart = -1; - let importEnd = -1; - let foundImport = false; + let importStart = -1 + let importEnd = -1 + let foundImport = false for (; i < lines.length; i++) { - const line = lines[i]; - if (line === "import type {") { - importStart = i; // save last import start; - } else if (line === "} from \"./definitions\";") { - importEnd = i; - foundImport = true; - break; + const line = lines[i] + if (line === 'import type {') { + importStart = i // save last import start; + } else if (line === '} from \'./definitions\'') { + importEnd = i + foundImport = true + break } } if (!foundImport) { - throw new Error("Could not find import section!"); + throw new Error('Could not find import section!') } - let startIndex = -1; - let stopIndex = -1; + let startIndex = -1 + let stopIndex = -1 for (; i < lines.length; i++) { - if (lines[i] === " // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-") { - startIndex = i; - break; + if (lines[i] === ' // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-') { + startIndex = i + break } } if (startIndex === -1) { - throw new Error("Could not find start pattern in file!"); + throw new Error('Could not find start pattern in file!') } for (; i < lines.length; i++) { - if (lines[i] === " // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=") { - stopIndex = i; - break; + if (lines[i] === ' // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=') { + stopIndex = i + break } } if (stopIndex === -1) { - throw new Error("Could not find stop pattern in file!"); + throw new Error('Could not find stop pattern in file!') } - const importSize = importEnd - importStart - 1; + const importSize = importEnd - importStart - 1 const newImports = properties .filter(([key, value]) => key === value.className) - .map(([key]) => " " + key + ","); - lines.splice(importStart + 1, importSize, ...newImports); // remove current imports + .map(([key]) => ` ${key},`) + lines.splice(importStart + 1, importSize, ...newImports) // remove current imports - const importDelta = newImports.length - importSize; + const importDelta = newImports.length - importSize - startIndex += importDelta; - stopIndex += importDelta; + startIndex += importDelta + stopIndex += importDelta - const amount = stopIndex - startIndex - 1; + const amount = stopIndex - startIndex - 1 const newContentLines = properties.map(([key, value]) => { - let line = ""; + let line = '' - let deprecatedNotice = value.deprecatedNotice; + let deprecatedNotice = value.deprecatedNotice if (key !== value.className) { - deprecatedNotice = "Please use {@link " + className + "." + value.className + "}." // prepend deprecated notice - + (deprecatedNotice? " " + deprecatedNotice: ""); + deprecatedNotice = `Please use {@link ${className}.${value.className}}.${// prepend deprecated notice + deprecatedNotice ? ` ${deprecatedNotice}` : ''}` } - line += " /**\n"; - line += " * @group " + className + " Definitions\n"; + line += ' /**\n' + line += ` * @group ${className} Definitions\n` if (deprecatedNotice) { - line += " * @deprecated " + deprecatedNotice + "\n"; + line += ` * @deprecated ${deprecatedNotice}\n` } - line += " */\n"; + line += ' */\n' - line += " public static " + key + ": typeof " + value.className + ";"; - return line; - }); - lines.splice(startIndex + 1, amount, ...newContentLines); // insert new lines + line += ` public static ${key}: typeof ${value.className}` + return line + }) + lines.splice(startIndex + 1, amount, ...newContentLines) // insert new lines - const resultContent = lines.join("\n"); - fs.writeFileSync(filePath, resultContent, { encoding: "utf8" }); + const resultContent = lines.join('\n') + writeFileSync(filePath, resultContent, { encoding: 'utf8' }) } function writeCharacteristicTestFile(): void { - const characteristics = Object.values(generatedCharacteristics).sort((a, b) => a.className.localeCompare(b.className)); + const characteristics = Object.values(generatedCharacteristics).sort((a, b) => a.className.localeCompare(b.className)) - const testOutput = fs.createWriteStream(path.resolve(__dirname, "./CharacteristicDefinitions.spec.ts"), { encoding: "utf8" }); - testOutput.write("// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n"); - testOutput.write("import \"./\";\n\n"); - testOutput.write("import { Characteristic } from \"../Characteristic\";\n\n"); - testOutput.write("describe(\"CharacteristicDefinitions\", () => {"); + const testOutput = createWriteStream(resolve(__dirname, './CharacteristicDefinitions.spec.ts'), { encoding: 'utf8' }) + testOutput.write('// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n') + testOutput.write('/* eslint-disable no-new */\n') + testOutput.write('import { describe, it } from \'vitest\'\n\n') + testOutput.write('import { Characteristic } from \'../Characteristic.js\'\n') + testOutput.write('import \'./index.js\'\n\n') + testOutput.write('describe(\'characteristicDefinitions\', () => {') for (const generated of characteristics) { - testOutput.write("\n"); - testOutput.write(" describe(\"" + generated.className + "\", () => {\n"); + testOutput.write('\n') + testOutput.write(` describe('${generated.className[0].toLowerCase()}${generated.className.slice(1)}', () => {\n`) // first test is just calling the constructor - testOutput.write(" it(\"should be able to construct\", () => {\n"); - testOutput.write(" new Characteristic." + generated.className + "();\n"); + testOutput.write(' it(\'should be able to construct\', () => {\n') + testOutput.write(` new Characteristic.${generated.className}()\n`) if (generated.deprecatedClassName) { - testOutput.write(" // noinspection JSDeprecatedSymbols\n"); - testOutput.write(" new Characteristic." + generated.deprecatedClassName + "();\n"); + testOutput.write(` new Characteristic.${generated.deprecatedClassName}()\n`) } - testOutput.write(" });\n"); + testOutput.write(' })\n') - testOutput.write(" });\n"); + testOutput.write(' })\n') } - testOutput.write("});\n"); - testOutput.end(); + testOutput.write('})\n') + testOutput.end() } function writeServicesTestFile(): void { - const services = Object.values(generatedServices).sort((a, b) => a.className.localeCompare(b.className)); + const services = Object.values(generatedServices).sort((a, b) => a.className.localeCompare(b.className)) - const testOutput = fs.createWriteStream(path.resolve(__dirname, "./ServiceDefinitions.spec.ts"), { encoding: "utf8" }); - testOutput.write("// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n"); - testOutput.write("import \"./\";\n\n"); - testOutput.write("import { Characteristic } from \"../Characteristic\";\n"); - testOutput.write("import { Service } from \"../Service\";\n\n"); - testOutput.write("describe(\"ServiceDefinitions\", () => {"); + const testOutput = createWriteStream(resolve(__dirname, './ServiceDefinitions.spec.ts'), { encoding: 'utf8' }) + testOutput.write('// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n') + testOutput.write('import { describe, expect, it } from \'vitest\'\n\n') + testOutput.write('import { Characteristic } from \'../Characteristic.js\'\n') + testOutput.write('import { Service } from \'../Service.js\'\n') + testOutput.write('import \'./index.js\'\n\n') + testOutput.write('describe(\'serviceDefinitions\', () => {') for (const generated of services) { - testOutput.write("\n"); - testOutput.write(" describe(\"" + generated.className + "\", () => {\n"); + testOutput.write('\n') + testOutput.write(` describe('${generated.className[0].toLowerCase()}${generated.className.slice(1)}', () => {\n`) // first test is just calling the constructor - testOutput.write(" it(\"should be able to construct\", () => {\n"); + testOutput.write(' it(\'should be able to construct\', () => {\n') - testOutput.write(" const service0 = new Service." + generated.className + "();\n"); - testOutput.write(" const service1 = new Service." + generated.className + "(\"test name\");\n"); - testOutput.write(" const service2 = new Service." + generated.className + "(\"test name\", \"test sub type\");\n\n"); + testOutput.write(` const service0 = new Service.${generated.className}()\n`) + testOutput.write(` const service1 = new Service.${generated.className}('test name')\n`) + testOutput.write(` const service2 = new Service.${generated.className}('test name', 'test sub type')\n\n`) - testOutput.write(" expect(service0.displayName).toBe(\"\");\n"); - testOutput.write(" expect(service0.testCharacteristic(Characteristic.Name)).toBe(" + generated.requiredCharacteristics.includes("name") + ");\n"); - testOutput.write(" expect(service0.subtype).toBeUndefined();\n\n"); + testOutput.write(' expect(service0.displayName).toBe(\'\')\n') + testOutput.write(` expect(service0.testCharacteristic(Characteristic.Name)).toBe(${generated.requiredCharacteristics.includes('name')})\n`) + testOutput.write(' expect(service0.subtype).toBeUndefined()\n\n') - testOutput.write(" expect(service1.displayName).toBe(\"test name\");\n"); - testOutput.write(" expect(service1.testCharacteristic(Characteristic.Name)).toBe(true);\n"); - testOutput.write(" expect(service1.getCharacteristic(Characteristic.Name).value).toBe(\"test name\");\n"); - testOutput.write(" expect(service1.subtype).toBeUndefined();\n\n"); + testOutput.write(' expect(service1.displayName).toBe(\'test name\')\n') + testOutput.write(' expect(service1.testCharacteristic(Characteristic.Name)).toBe(true)\n') + testOutput.write(' expect(service1.getCharacteristic(Characteristic.Name).value).toBe(\'test name\')\n') + testOutput.write(' expect(service1.subtype).toBeUndefined()\n\n') - testOutput.write(" expect(service2.displayName).toBe(\"test name\");\n"); - testOutput.write(" expect(service2.testCharacteristic(Characteristic.Name)).toBe(true);\n"); - testOutput.write(" expect(service2.getCharacteristic(Characteristic.Name).value).toBe(\"test name\");\n"); - testOutput.write(" expect(service2.subtype).toBe(\"test sub type\");\n"); + testOutput.write(' expect(service2.displayName).toBe(\'test name\')\n') + testOutput.write(' expect(service2.testCharacteristic(Characteristic.Name)).toBe(true)\n') + testOutput.write(' expect(service2.getCharacteristic(Characteristic.Name).value).toBe(\'test name\')\n') + testOutput.write(' expect(service2.subtype).toBe(\'test sub type\')\n') if (generated.deprecatedClassName) { - testOutput.write(" // noinspection JSDeprecatedSymbols\n"); - testOutput.write("\n new Service." + generated.deprecatedClassName + "();\n"); + testOutput.write(`\n new Service.${generated.deprecatedClassName}()\n`) } - testOutput.write(" });\n"); + testOutput.write(' })\n') - testOutput.write(" });\n"); + testOutput.write(' })\n') } - testOutput.write("});\n"); - testOutput.end(); + testOutput.write('})\n') + testOutput.end() } diff --git a/src/lib/definitions/generator-configuration.ts b/src/lib/definitions/generator-configuration.ts index 975ff3269..8c3b5d4e8 100644 --- a/src/lib/definitions/generator-configuration.ts +++ b/src/lib/definitions/generator-configuration.ts @@ -1,7 +1,10 @@ -import assert from "assert"; -import { Access } from "../Characteristic"; -import { GeneratedCharacteristic, GeneratedService } from "./generate-definitions"; +import type { GeneratedCharacteristic, GeneratedService } from './generate-definitions' +import assert from 'node:assert' + +import { Access } from '../Characteristic.js' + +// eslint-disable-next-line no-restricted-syntax const enum PropertyId { NOTIFY = 0x01, READ = 0x02, @@ -14,289 +17,286 @@ const enum PropertyId { } export const CharacteristicHidden: Set = new Set([ - "service-signature", // BLE -]); + 'service-signature', // BLE +]) export const CharacteristicNameOverrides: Map = new Map([ - ["air-quality", "Air Quality"], - ["app-matching-identifier", "App Matching Identifier"], - ["cloud-relay.control-point", "Relay Control Point"], - ["cloud-relay.current-state", "Relay State"], - ["cloud-relay.enabled", "Relay Enabled"], - ["density.voc", "VOC Density"], - ["filter.reset-indication", "Reset Filter Indication"], // Filter Reset Change Indication - ["light-level.current", "Current Ambient Light Level"], - ["network-client-control", "Network Client Profile Control"], - ["on", "On"], - ["selected-stream-configuration", "Selected RTP Stream Configuration"], - ["service-label-index", "Service Label Index"], - ["service-label-namespace", "Service Label Namespace"], - ["setup-stream-endpoint", "Setup Endpoints"], - ["snr", "Signal To Noise Ratio"], - ["supported-target-configuration", "Target Control Supported Configuration"], - ["target-list", "Target Control List"], - ["tunneled-accessory.advertising", "Tunneled Accessory Advertising"], - ["tunneled-accessory.connected", "Tunneled Accessory Connected"], - ["water-level", "Water Level"], -]); + ['air-quality', 'Air Quality'], + ['app-matching-identifier', 'App Matching Identifier'], + ['cloud-relay.control-point', 'Relay Control Point'], + ['cloud-relay.current-state', 'Relay State'], + ['cloud-relay.enabled', 'Relay Enabled'], + ['density.voc', 'VOC Density'], + ['filter.reset-indication', 'Reset Filter Indication'], // Filter Reset Change Indication + ['light-level.current', 'Current Ambient Light Level'], + ['network-client-control', 'Network Client Profile Control'], + ['on', 'On'], + ['selected-stream-configuration', 'Selected RTP Stream Configuration'], + ['service-label-index', 'Service Label Index'], + ['service-label-namespace', 'Service Label Namespace'], + ['setup-stream-endpoint', 'Setup Endpoints'], + ['snr', 'Signal To Noise Ratio'], + ['supported-target-configuration', 'Target Control Supported Configuration'], + ['target-list', 'Target Control List'], + ['tunneled-accessory.advertising', 'Tunneled Accessory Advertising'], + ['tunneled-accessory.connected', 'Tunneled Accessory Connected'], + ['water-level', 'Water Level'], +]) export const CharacteristicDeprecatedNames: Map = new Map([ // keep in mind that the displayName will change -]); +]) export const CharacteristicValidValuesOverride: Map> = new Map([ - ["closed-captions", { "0": "Disabled", "1": "Enabled" }], - ["input-device-type", { "0": "Other", "1": "TV", "2": "Recording", "3": "Tuner", "4": "Playback", "5": "Audio System" }], - ["input-source-type", { "0": "Other", "1": "Home Screen", "2": "Tuner", "3": "HDMI", "4": "Composite Video", "5": "S Video", - "6": "Component Video", "7": "DVI", "8": "AirPlay", "9": "USB", "10": "Application" }], - ["managed-network-enable", { "0": "Disabled", "1": "Enabled" }], - ["manually-disabled", { "0": "Enabled", "1": "Disabled" }], - ["media-state.current", { "0": "Play", "1": "Pause", "2": "Stop", "4": "LOADING", "5": "Interrupted" }], - ["media-state.target", { "0": "Play", "1": "Pause", "2": "Stop" }], - ["picture-mode", { "0": "Other", "1": "Standard", "2": "Calibrated", "3": "Calibrated Dark", "4": "Vivid", "5": "Game", "6": "Computer", "7": "Custom" }], - ["power-mode-selection", { "0": "Show", "1": "Hide" }], - ["recording-audio-active", { "0": "Disable", "1": "Enable" }], - ["remote-key", { "0": "Rewind", "1": "Fast Forward", "2": "Next Track", "3": "Previous Track", "4": "Arrow Up", "5": "Arrow Down", - "6": "Arrow Left", "7": "Arrow Right", "8": "Select", "9": "Back", "10": "Exit", "11": "Play Pause", "15": "Information" }], - ["router-status", { "0": "Ready", "1": "Not Ready" }], - ["siri-input-type", { "0": "Push Button Triggered Apple TV" }], - ["sleep-discovery-mode", { "0": "Not Discoverable", "1": "Always Discoverable" }], - ["visibility-state.current", { "0": "Shown", "1": "Hidden" }], - ["visibility-state.target", { "0": "Shown", "1": "Hidden" }], - ["volume-control-type", { "0": "None", "1": "Relative", "2": "Relative With Current", "3": "Absolute" }], - ["volume-selector", { "0": "Increment", "1": "Decrement" }], - ["wifi-satellite-status", { "0": "Unknown", "1": "Connected", "2": "Not Connected" }], -] as [string, Record][]); + ['closed-captions', { 0: 'Disabled', 1: 'Enabled' }], + ['input-device-type', { 0: 'Other', 1: 'TV', 2: 'Recording', 3: 'Tuner', 4: 'Playback', 5: 'Audio System' }], + ['input-source-type', { 0: 'Other', 1: 'Home Screen', 2: 'Tuner', 3: 'HDMI', 4: 'Composite Video', 5: 'S Video', 6: 'Component Video', 7: 'DVI', 8: 'AirPlay', 9: 'USB', 10: 'Application' }], + ['managed-network-enable', { 0: 'Disabled', 1: 'Enabled' }], + ['manually-disabled', { 0: 'Enabled', 1: 'Disabled' }], + ['media-state.current', { 0: 'Play', 1: 'Pause', 2: 'Stop', 4: 'LOADING', 5: 'Interrupted' }], + ['media-state.target', { 0: 'Play', 1: 'Pause', 2: 'Stop' }], + ['picture-mode', { 0: 'Other', 1: 'Standard', 2: 'Calibrated', 3: 'Calibrated Dark', 4: 'Vivid', 5: 'Game', 6: 'Computer', 7: 'Custom' }], + ['power-mode-selection', { 0: 'Show', 1: 'Hide' }], + ['recording-audio-active', { 0: 'Disable', 1: 'Enable' }], + ['remote-key', { 0: 'Rewind', 1: 'Fast Forward', 2: 'Next Track', 3: 'Previous Track', 4: 'Arrow Up', 5: 'Arrow Down', 6: 'Arrow Left', 7: 'Arrow Right', 8: 'Select', 9: 'Back', 10: 'Exit', 11: 'Play Pause', 15: 'Information' }], + ['router-status', { 0: 'Ready', 1: 'Not Ready' }], + ['siri-input-type', { 0: 'Push Button Triggered Apple TV' }], + ['sleep-discovery-mode', { 0: 'Not Discoverable', 1: 'Always Discoverable' }], + ['visibility-state.current', { 0: 'Shown', 1: 'Hidden' }], + ['visibility-state.target', { 0: 'Shown', 1: 'Hidden' }], + ['volume-control-type', { 0: 'None', 1: 'Relative', 2: 'Relative With Current', 3: 'Absolute' }], + ['volume-selector', { 0: 'Increment', 1: 'Decrement' }], + ['wifi-satellite-status', { 0: 'Unknown', 1: 'Connected', 2: 'Not Connected' }], +] as [string, Record][]) -export const CharacteristicClassAdditions: Map = new Map([]); +export const CharacteristicClassAdditions: Map = new Map([]) export const CharacteristicOverriding: Map void> = new Map([ - ["rotation.speed", generated => { - generated.units = "percentage"; + ['rotation.speed', (generated) => { + generated.units = 'percentage' }], - ["temperature.current", generated => { - generated.minValue = -270; + ['temperature.current', (generated) => { + generated.minValue = -270 }], - ["characteristic-value-transition-control", generated => { - generated.properties |= PropertyId.WRITE_RESPONSE; + ['characteristic-value-transition-control', (generated) => { + generated.properties |= PropertyId.WRITE_RESPONSE }], - ["setup-data-stream-transport", generated => { - generated.properties |= PropertyId.WRITE_RESPONSE; + ['setup-data-stream-transport', (generated) => { + generated.properties |= PropertyId.WRITE_RESPONSE }], - ["data-stream-hap-transport", generated => { - generated.properties |= PropertyId.WRITE_RESPONSE; + ['data-stream-hap-transport', (generated) => { + generated.properties |= PropertyId.WRITE_RESPONSE }], - ["lock-mechanism.last-known-action", generated => { - assert(generated.maxValue === 8, "LockLastKnownAction seems to have changed in metadata!"); - generated.maxValue = 10; - generated.validValues!["9"] = "SECURED_PHYSICALLY"; - generated.validValues!["10"] = "UNSECURED_PHYSICALLY"; + ['lock-mechanism.last-known-action', (generated) => { + assert(generated.maxValue === 8, 'LockLastKnownAction seems to have changed in metadata!') + generated.maxValue = 10 + generated.validValues!['9'] = 'SECURED_PHYSICALLY' + generated.validValues!['10'] = 'UNSECURED_PHYSICALLY' }], - ["configured-name", generated => { + ['configured-name', (generated) => { // the write permission on the configured name characteristic is actually optional and should only be supported // if a HomeKit controller should be able to change the name (e.g. for a TV Input). // As of legacy compatibility we just add that permission and tackle that problem later in a TVController (or something). - generated.properties |= PropertyId.WRITE; + generated.properties |= PropertyId.WRITE }], - ["is-configured", generated => { + ['is-configured', (generated) => { // write permission on is configured is optional (out of history it was present with HAP-NodeJS) // if the HomeKit controller is able to change the configured state, it can be set to write. - generated.properties |= PropertyId.WRITE; + generated.properties |= PropertyId.WRITE }], - ["display-order", generated => { + ['display-order', (generated) => { // write permission on display order is optional (out of history it was present with HAP-NodeJS) // if the HomeKit controller is able to change the configured state, it can be set to write. - generated.properties |= PropertyId.WRITE; + generated.properties |= PropertyId.WRITE }], - ["button-event", generated => { - generated.adminOnlyAccess = [Access.NOTIFY]; + ['button-event', (generated) => { + generated.adminOnlyAccess = [Access.NOTIFY] }], - ["target-list", generated => { - generated.adminOnlyAccess = [Access.READ, Access.WRITE]; + ['target-list', (generated) => { + generated.adminOnlyAccess = [Access.READ, Access.WRITE] }], - ["slat.state.current", generated => { - generated.maxValue = 2; + ['slat.state.current', (generated) => { + generated.maxValue = 2 }], - ["event-snapshots-active", generated => { - generated.format = "uint8"; - generated.minValue = 0; - generated.maxValue = 1; - generated.properties &= ~PropertyId.TIMED_WRITE; + ['event-snapshots-active', (generated) => { + generated.format = 'uint8' + generated.minValue = 0 + generated.maxValue = 1 + generated.properties &= ~PropertyId.TIMED_WRITE }], - ["homekit-camera-active", generated => { - generated.format = "uint8"; - generated.minValue = 0; - generated.maxValue = 1; - generated.properties &= ~PropertyId.TIMED_WRITE; + ['homekit-camera-active', (generated) => { + generated.format = 'uint8' + generated.minValue = 0 + generated.maxValue = 1 + generated.properties &= ~PropertyId.TIMED_WRITE }], - ["periodic-snapshots-active", generated => { - generated.format = "uint8"; - generated.properties &= ~PropertyId.TIMED_WRITE; + ['periodic-snapshots-active', (generated) => { + generated.format = 'uint8' + generated.properties &= ~PropertyId.TIMED_WRITE }], - ["third-party-camera-active", generated => { - generated.format = "uint8"; - + ['third-party-camera-active', (generated) => { + generated.format = 'uint8' }], - ["input-device-type", generated => { + ['input-device-type', (generated) => { // @ts-expect-error: undefined access - generated.validValues[6] = null; + generated.validValues[6] = null }], - ["pairing-features", generated => { - generated.properties &= ~PropertyId.WRITE; + ['pairing-features', (generated) => { + generated.properties &= ~PropertyId.WRITE }], - ["picture-mode", generated => { + ['picture-mode', (generated) => { // @ts-expect-error: undefined access - generated.validValues[8] = null; + generated.validValues[8] = null // @ts-expect-error: undefined access - generated.validValues[9] = null; + generated.validValues[9] = null // @ts-expect-error: undefined access - generated.validValues[10] = null; + generated.validValues[10] = null // @ts-expect-error: undefined access - generated.validValues[11] = null; + generated.validValues[11] = null // @ts-expect-error: undefined access - generated.validValues[12] = null; + generated.validValues[12] = null // @ts-expect-error: undefined access - generated.validValues[13] = null; + generated.validValues[13] = null }], - ["remote-key", generated => { + ['remote-key', (generated) => { // @ts-expect-error: undefined access - generated.validValues[12] = null; + generated.validValues[12] = null // @ts-expect-error: undefined access - generated.validValues[13] = null; + generated.validValues[13] = null // @ts-expect-error: undefined access - generated.validValues[14] = null; + generated.validValues[14] = null // @ts-expect-error: undefined access - generated.validValues[16] = null; + generated.validValues[16] = null }], - ["service-label-namespace", generated => { - generated.maxValue = 1; + ['service-label-namespace', (generated) => { + generated.maxValue = 1 }], - ["siri-input-type", generated => { - generated.maxValue = 0; + ['siri-input-type', (generated) => { + generated.maxValue = 0 }], - ["visibility-state.current", generated => { - generated.maxValue = 1; + ['visibility-state.current', (generated) => { + generated.maxValue = 1 }], - ["active-identifier", generated => { - generated.minValue = undefined; + ['active-identifier', (generated) => { + generated.minValue = undefined }], - ["identifier", generated => { - generated.minValue = undefined; + ['identifier', (generated) => { + generated.minValue = undefined }], - ["access-code-control-point", generated => { - generated.properties |= PropertyId.WRITE_RESPONSE; + ['access-code-control-point', (generated) => { + generated.properties |= PropertyId.WRITE_RESPONSE }], - ["nfc-access-control-point", generated => { - generated.properties |= PropertyId.WRITE_RESPONSE; + ['nfc-access-control-point', (generated) => { + generated.properties |= PropertyId.WRITE_RESPONSE }], -]); +]) export const CharacteristicManualAdditions: Map = new Map([ - ["diagonal-field-of-view", { - id: "diagonal-field-of-view", - UUID: "00000224-0000-1000-8000-0026BB765291", - name: "Diagonal Field Of View", - className: "DiagonalFieldOfView", - since: "13.2", + ['diagonal-field-of-view', { + id: 'diagonal-field-of-view', + UUID: '00000224-0000-1000-8000-0026BB765291', + name: 'Diagonal Field Of View', + className: 'DiagonalFieldOfView', + since: '13.2', - format: "float", - units: "arcdegrees", + format: 'float', + units: 'arcdegrees', properties: 3, // notify, paired read minValue: 0, maxValue: 360, }], - ["version", { // don't know why, but version has notify permission even if it shouldn't have one - id: "version", - UUID: "00000037-0000-1000-8000-0026BB765291", - name: "Version", - className: "Version", + ['version', { // don't know why, but version has 'notify' permission even if it shouldn't have one + id: 'version', + UUID: '00000037-0000-1000-8000-0026BB765291', + name: 'Version', + className: 'Version', - format: "string", + format: 'string', properties: 2, // paired read maxLength: 64, }], -]); +]) export const ServiceNameOverrides: Map = new Map([ - ["accessory-information", "Accessory Information"], - ["camera-rtp-stream-management", "Camera RTP Stream Management"], - ["fanv2", "Fanv2"], - ["service-label", "Service Label"], - ["smart-speaker", "Smart Speaker"], - ["speaker", "Television Speaker"], // has some additional accessories - ["nfc-access", "NFC Access"], -]); + ['accessory-information', 'Accessory Information'], + ['camera-rtp-stream-management', 'Camera RTP Stream Management'], + ['fanv2', 'Fanv2'], + ['service-label', 'Service Label'], + ['smart-speaker', 'Smart Speaker'], + ['speaker', 'Television Speaker'], // has some additional accessories + ['nfc-access', 'NFC Access'], +]) -export const ServiceDeprecatedNames: Map = new Map([]); +export const ServiceDeprecatedNames: Map = new Map([]) interface CharacteristicConfigurationOverride { - addedRequired?: string[], - removedRequired?: string[], - addedOptional?: string[], - removedOptional?: string[], + addedRequired?: string[] + removedRequired?: string[] + addedOptional?: string[] + removedOptional?: string[] } export const ServiceCharacteristicConfigurationOverrides: Map = new Map([ - ["accessory-information", { addedRequired: ["firmware.revision"], removedOptional: ["firmware.revision"] }], - ["camera-operating-mode", { addedOptional: ["diagonal-field-of-view"] }], -]); + ['accessory-information', { addedRequired: ['firmware.revision'], removedOptional: ['firmware.revision'] }], + ['camera-operating-mode', { addedOptional: ['diagonal-field-of-view'] }], +]) export const ServiceManualAdditions: Map = new Map([ - ["og-speaker", { // the normal speaker is considered to be the "TelevisionSpeaker" - id: "og-speaker", - UUID: "00000113-0000-1000-8000-0026BB765291", - name: "Speaker", - className: "Speaker", - since: "10", + ['og-speaker', { // the normal speaker is considered to be the "TelevisionSpeaker" + id: 'og-speaker', + UUID: '00000113-0000-1000-8000-0026BB765291', + name: 'Speaker', + className: 'Speaker', + since: '10', - requiredCharacteristics: ["mute"], - optionalCharacteristics: ["active", "volume"], + requiredCharacteristics: ['mute'], + optionalCharacteristics: ['active', 'volume'], }], -]); +]) export const CharacteristicSinceInformation: Map = new Map([ - ["activity-interval", "14"], - ["cca-energy-detect-threshold", "14"], - ["cca-signal-detect-threshold", "14"], - ["characteristic-value-active-transition-count", "14"], - ["characteristic-value-transition-control", "14"], - ["current-transport", "14"], - ["data-stream-hap-transport", "14"], - ["data-stream-hap-transport-interrupt", "14"], - ["event-retransmission-maximum", "14"], - ["event-transmission-counters", "14"], - ["heart-beat", "14"], - ["mac-retransmission-maximum", "14"], - ["mac-retransmission-counters", "14"], - ["operating-state-response", "14"], - ["ping", "14"], - ["receiver-sensitivity", "14"], - ["rssi", "14"], - ["setup-transfer-transport", "13.4"], - ["sleep-interval", "14"], - ["snr", "14"], - ["supported-characteristic-value-transition-configuration", "14"], - ["supported-diagnostics-snapshot", "14"], - ["supported-transfer-transport-configuration", "13.4"], - ["transmit-power", "14"], - ["transmit-power-maximum", "14"], - ["transfer-transport-management", "13.4"], - ["video-analysis-active", "14"], - ["wake-configuration", "13.4"], - ["wifi-capabilities", "14"], - ["wifi-configuration-control", "14"], + ['activity-interval', '14'], + ['cca-energy-detect-threshold', '14'], + ['cca-signal-detect-threshold', '14'], + ['characteristic-value-active-transition-count', '14'], + ['characteristic-value-transition-control', '14'], + ['current-transport', '14'], + ['data-stream-hap-transport', '14'], + ['data-stream-hap-transport-interrupt', '14'], + ['event-retransmission-maximum', '14'], + ['event-transmission-counters', '14'], + ['heart-beat', '14'], + ['mac-retransmission-maximum', '14'], + ['mac-retransmission-counters', '14'], + ['operating-state-response', '14'], + ['ping', '14'], + ['receiver-sensitivity', '14'], + ['rssi', '14'], + ['setup-transfer-transport', '13.4'], + ['sleep-interval', '14'], + ['snr', '14'], + ['supported-characteristic-value-transition-configuration', '14'], + ['supported-diagnostics-snapshot', '14'], + ['supported-transfer-transport-configuration', '13.4'], + ['transmit-power', '14'], + ['transmit-power-maximum', '14'], + ['transfer-transport-management', '13.4'], + ['video-analysis-active', '14'], + ['wake-configuration', '13.4'], + ['wifi-capabilities', '14'], + ['wifi-configuration-control', '14'], - ["access-code-control-point", "15"], - ["access-code-supported-configuration", "15"], - ["configuration-state", "15"], - ["hardware-finish", "15"], - ["nfc-access-control-point", "15"], - ["nfc-access-supported-configuration", "15"], -]); + ['access-code-control-point', '15'], + ['access-code-supported-configuration', '15'], + ['configuration-state', '15'], + ['hardware-finish', '15'], + ['nfc-access-control-point', '15'], + ['nfc-access-supported-configuration', '15'], +]) export const ServiceSinceInformation: Map = new Map([ - ["outlet", "13"], + ['outlet', '13'], - ["access-code", "15"], - ["nfc-access", "15"], -]); + ['access-code', '15'], + ['nfc-access', '15'], +]) diff --git a/src/lib/definitions/index.ts b/src/lib/definitions/index.ts index 9a5664a56..5bad8c198 100644 --- a/src/lib/definitions/index.ts +++ b/src/lib/definitions/index.ts @@ -1,2 +1,2 @@ -export * from "./CharacteristicDefinitions"; -export * from "./ServiceDefinitions"; +export * from './CharacteristicDefinitions.js' +export * from './ServiceDefinitions.js' diff --git a/src/lib/gen/HomeKit.ts b/src/lib/gen/HomeKit.ts deleted file mode 100644 index c1178a0fe..000000000 --- a/src/lib/gen/HomeKit.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Service and Characteristic definitions were moved to - * https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/definitions/CharacteristicDefinitions.ts - * and - * https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/definitions/ServiceDefinitions.ts - */ diff --git a/src/lib/model/AccessoryInfo.spec.ts b/src/lib/model/AccessoryInfo.spec.ts index 5f06cec40..9c9c9c2e9 100644 --- a/src/lib/model/AccessoryInfo.spec.ts +++ b/src/lib/model/AccessoryInfo.spec.ts @@ -1,32 +1,35 @@ -import { AccessoryInfo } from "./AccessoryInfo"; -import { AssertionError } from "assert"; +import { AssertionError } from 'node:assert' -describe("AccessoryInfo", () => { - describe("#assertValidUsername()", () => { - it("should verify correct device id", () => { - const VALUE = "0E:AE:FC:45:7B:91"; - expect(() => AccessoryInfo.assertValidUsername(VALUE)).not.toThrow(AssertionError); - }); +import { describe, expect, it } from 'vitest' - it("should fail to verify too long device id", () => { - const VALUE = "00:2c:44:f9:30:8f:d1:2e"; - expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError); - }); +import { AccessoryInfo } from './AccessoryInfo.js' - it("should fail to verify too short device id", () => { - const VALUE = "00:2c:d1:2e"; - expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError); - }); +describe('accessoryInfo', () => { + describe('#assertValidUsername()', () => { + it('should verify correct device id', () => { + const VALUE = '0E:AE:FC:45:7B:91' + expect(() => AccessoryInfo.assertValidUsername(VALUE)).not.toThrow(AssertionError) + }) - it("should fail to verify device id containing invalid characters", () => { - const VALUE = "0E:AG:FC:45:7B:91"; - expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError); - }); + it('should fail to verify too long device id', () => { + const VALUE = '00:2c:44:f9:30:8f:d1:2e' + expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError) + }) - it("should fail to verify undefined device id", () => { - const VALUE = undefined; + it('should fail to verify too short device id', () => { + const VALUE = '00:2c:d1:2e' + expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError) + }) + + it('should fail to verify device id containing invalid characters', () => { + const VALUE = '0E:AG:FC:45:7B:91' + expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError) + }) + + it('should fail to verify undefined device id', () => { + const VALUE = undefined // @ts-expect-error: deliberately test illegal value - expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError); - }); - }); -}); + expect(() => AccessoryInfo.assertValidUsername(VALUE)).toThrow(AssertionError) + }) + }) +}) diff --git a/src/lib/model/AccessoryInfo.ts b/src/lib/model/AccessoryInfo.ts index 758247704..d4c43245d 100644 --- a/src/lib/model/AccessoryInfo.ts +++ b/src/lib/model/AccessoryInfo.ts @@ -1,24 +1,30 @@ -import assert from "assert"; -import crypto from "crypto"; -import tweetnacl from "tweetnacl"; -import util from "util"; -import { AccessoryJsonObject, MacAddress } from "../../types"; -import { Categories } from "../Accessory"; -import { EventedHTTPServer, HAPConnection, HAPUsername } from "../util/eventedhttp"; -import { HAPStorage } from "./HAPStorage"; +import type { AccessoryJsonObject, MacAddress } from '../../types' +import type { HAPConnection, HAPUsername } from '../util/eventedhttp' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' +import { createRequire } from 'node:module' +import { format } from 'node:util' + +import tweetnacl from 'tweetnacl' + +import { Categories } from '../Accessory.js' +import { EventedHTTPServer } from '../util/eventedhttp.js' +import { HAPStorage } from './HAPStorage.js' + +const require = createRequire(import.meta.url) function getVersion(): string { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const packageJson = require("../../../package.json"); - return packageJson.version; + const packageJson = require('../../../package.json') + return packageJson.version } /** * @group Model */ +// eslint-disable-next-line no-restricted-syntax export const enum PermissionTypes { - // noinspection JSUnusedGlobalSymbols USER = 0x00, ADMIN = 0x01, // admins are the only ones who can add/remove/list pairings (additionally some characteristics are restricted) } @@ -27,9 +33,9 @@ export const enum PermissionTypes { * @group Model */ export interface PairingInformation { - username: HAPUsername, - publicKey: Buffer, - permission: PermissionTypes, + username: HAPUsername + publicKey: Buffer + permission: PermissionTypes } /** @@ -38,36 +44,35 @@ export interface PairingInformation { * @group Model */ export class AccessoryInfo { - - static readonly deviceIdPattern: RegExp = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; - - username: MacAddress; - displayName: string; - model: string; // this property is currently not saved to disk - category: Categories; - pincode: string; - signSk: Buffer; - signPk: Buffer; - pairedClients: Record; - pairedAdminClients: number; - private configVersion = 1; - private configHash: string; - setupID: string; - private lastFirmwareVersion = ""; + static readonly deviceIdPattern: RegExp = /^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$/i + + username: MacAddress + displayName: string + model: string // this property is currently not saved to disk + category: Categories + pincode: string + signSk: Buffer + signPk: Buffer + pairedClients: Record + pairedAdminClients: number + private configVersion = 1 + private configHash: string + setupID: string + private lastFirmwareVersion = '' private constructor(username: MacAddress) { - this.username = username; - this.displayName = ""; - this.model = ""; - this.category = Categories.OTHER; - this.pincode = ""; - this.signSk = Buffer.alloc(0); - this.signPk = Buffer.alloc(0); - this.pairedClients = {}; - this.pairedAdminClients = 0; - this.configHash = ""; - - this.setupID = ""; + this.username = username + this.displayName = '' + this.model = '' + this.category = Categories.OTHER + this.pincode = '' + this.signSk = Buffer.alloc(0) + this.signPk = Buffer.alloc(0) + this.pairedClients = {} + this.pairedAdminClients = 0 + this.configHash = '' + + this.setupID = '' } /** @@ -78,39 +83,39 @@ export class AccessoryInfo { */ public addPairedClient(username: HAPUsername, publicKey: Buffer, permission: PermissionTypes): void { this.pairedClients[username] = { - username: username, - publicKey: publicKey, - permission: permission, - }; + username, + publicKey, + permission, + } if (permission === PermissionTypes.ADMIN) { - this.pairedAdminClients++; + this.pairedAdminClients++ } } public updatePermission(username: HAPUsername, permission: PermissionTypes): void { - const pairingInformation = this.pairedClients[username]; + const pairingInformation = this.pairedClients[username] if (pairingInformation) { - const oldPermission = pairingInformation.permission; - pairingInformation.permission = permission; + const oldPermission = pairingInformation.permission + pairingInformation.permission = permission if (oldPermission === PermissionTypes.ADMIN && permission !== PermissionTypes.ADMIN) { - this.pairedAdminClients--; + this.pairedAdminClients-- } else if (oldPermission !== PermissionTypes.ADMIN && permission === PermissionTypes.ADMIN) { - this.pairedAdminClients++; + this.pairedAdminClients++ } } } public listPairings(): PairingInformation[] { - const array: PairingInformation[] = []; + const array: PairingInformation[] = [] for (const pairingInformation of Object.values(this.pairedClients)) { - array.push(pairingInformation); + array.push(pairingInformation) } - return array; + return array } /** @@ -119,22 +124,22 @@ export class AccessoryInfo { * @param {string} username */ public removePairedClient(connection: HAPConnection, username: HAPUsername): void { - this._removePairedClient0(connection, username); + this._removePairedClient0(connection, username) if (this.pairedAdminClients === 0) { // if we don't have any admin clients left paired it is required to kill all normal clients for (const username0 of Object.keys(this.pairedClients)) { - this._removePairedClient0(connection, username0); + this._removePairedClient0(connection, username0) } } } private _removePairedClient0(connection: HAPConnection, username: HAPUsername): void { if (this.pairedClients[username] && this.pairedClients[username].permission === PermissionTypes.ADMIN) { - this.pairedAdminClients--; + this.pairedAdminClients-- } - delete this.pairedClients[username]; + delete this.pairedClients[username] - EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection, username); + EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection, username) } /** @@ -142,31 +147,31 @@ export class AccessoryInfo { * @param username */ public isPaired(username: HAPUsername): boolean { - return !!this.pairedClients[username]; + return !!this.pairedClients[username] } public hasAdminPermissions(username: HAPUsername): boolean { if (!username) { - return false; + return false } - const pairingInformation = this.pairedClients[username]; - return !!pairingInformation && pairingInformation.permission === PermissionTypes.ADMIN; + const pairingInformation = this.pairedClients[username] + return !!pairingInformation && pairingInformation.permission === PermissionTypes.ADMIN } // Gets the public key for a paired client as a Buffer, or falsy value if not paired. public getClientPublicKey(username: HAPUsername): Buffer | undefined { - const pairingInformation = this.pairedClients[username]; + const pairingInformation = this.pairedClients[username] if (pairingInformation) { - return pairingInformation.publicKey; + return pairingInformation.publicKey } else { - return undefined; + return undefined } } // Returns a boolean indicating whether this accessory has been paired with a client. paired = (): boolean => { - return Object.keys(this.pairedClients).length > 0; // if we have any paired clients, we're paired. - }; + return Object.keys(this.pairedClients).length > 0 // if we have any paired clients, we're paired. + } /** * Checks based on the current accessory configuration if the current configuration number needs to be incremented. @@ -177,47 +182,47 @@ export class AccessoryInfo { * @returns True if the current configuration number was incremented and thus a new TXT must be advertised. */ public checkForCurrentConfigurationNumberIncrement(configuration: AccessoryJsonObject[], checkFirmwareIncrement?: boolean): boolean { - const shasum = crypto.createHash("sha1"); - shasum.update(JSON.stringify(configuration)); - const configHash = shasum.digest("hex"); + const shasum = createHash('sha1') + shasum.update(JSON.stringify(configuration)) + const configHash = shasum.digest('hex') - let changed = false; + let changed = false if (configHash !== this.configHash) { - this.configVersion++; - this.configHash = configHash; + this.configVersion++ + this.configHash = configHash - this.ensureConfigVersionBounds(); - changed = true; + this.ensureConfigVersionBounds() + changed = true } if (checkFirmwareIncrement) { - const version = getVersion(); + const version = getVersion() if (this.lastFirmwareVersion !== version) { // we only check if it is different and not only if it is incremented // HomeKit spec prohibits firmware downgrades, but with hap-nodejs it's possible lol - this.lastFirmwareVersion = version; - changed = true; + this.lastFirmwareVersion = version + changed = true } } if (changed) { - this.save(); + this.save() } - return changed; + return changed } public getConfigVersion(): number { - return this.configVersion; + return this.configVersion } private ensureConfigVersionBounds(): void { // current configuration number must be in the range of 1-65535 and wrap to 1 when it overflows - this.configVersion = this.configVersion % (0xFFFF + 1); + this.configVersion = this.configVersion % (0xFFFF + 1) if (this.configVersion === 0) { - this.configVersion = 1; + this.configVersion = 1 } } @@ -226,8 +231,8 @@ export class AccessoryInfo { displayName: this.displayName, category: this.category, pincode: this.pincode, - signSk: this.signSk.toString("hex"), - signPk: this.signPk.toString("hex"), + signSk: this.signSk.toString('hex'), + signPk: this.signPk.toString('hex'), pairedClients: {}, // moving permissions into an extra object, so there is nothing to migrate from old files. // if the legacy node-persist storage should be upgraded some time, it would be reasonable to combine the storage @@ -237,99 +242,97 @@ export class AccessoryInfo { configHash: this.configHash, setupID: this.setupID, lastFirmwareVersion: this.lastFirmwareVersion, - }; + } - for (const [ username, pairingInformation ] of Object.entries(this.pairedClients)) { + for (const [username, pairingInformation] of Object.entries(this.pairedClients)) { // @ts-expect-error: missing typing, object instead of Record - saved.pairedClients[username] = pairingInformation.publicKey.toString("hex"); + saved.pairedClients[username] = pairingInformation.publicKey.toString('hex') // @ts-expect-error: missing typing, object instead of Record - saved.pairedClientsPermission[username] = pairingInformation.permission; + saved.pairedClientsPermission[username] = pairingInformation.permission } - const key = AccessoryInfo.persistKey(this.username); + const key = AccessoryInfo.persistKey(this.username) - HAPStorage.storage().setItemSync(key, saved); + HAPStorage.storage().setItemSync(key, saved) } // Gets a key for storing this AccessoryInfo in the filesystem, like "AccessoryInfo.CC223DE3CEF3.json" static persistKey(username: MacAddress): string { - return util.format("AccessoryInfo.%s.json", username.replace(/:/g, "").toUpperCase()); + return format('AccessoryInfo.%s.json', username.replace(/:/g, '').toUpperCase()) } static create(username: MacAddress): AccessoryInfo { - AccessoryInfo.assertValidUsername(username); - const accessoryInfo = new AccessoryInfo(username); + AccessoryInfo.assertValidUsername(username) + const accessoryInfo = new AccessoryInfo(username) - accessoryInfo.lastFirmwareVersion = getVersion(); + accessoryInfo.lastFirmwareVersion = getVersion() // Create a new unique key pair for this accessory. - const keyPair = tweetnacl.sign.keyPair(); + const keyPair = tweetnacl.sign.keyPair() - accessoryInfo.signSk = Buffer.from(keyPair.secretKey); - accessoryInfo.signPk = Buffer.from(keyPair.publicKey); + accessoryInfo.signSk = Buffer.from(keyPair.secretKey) + accessoryInfo.signPk = Buffer.from(keyPair.publicKey) - return accessoryInfo; + return accessoryInfo } static load(username: MacAddress): AccessoryInfo | null { - AccessoryInfo.assertValidUsername(username); + AccessoryInfo.assertValidUsername(username) - const key = AccessoryInfo.persistKey(username); - const saved = HAPStorage.storage().getItem(key); + const key = AccessoryInfo.persistKey(username) + const saved = HAPStorage.storage().getItem(key) if (saved) { - const info = new AccessoryInfo(username); - info.displayName = saved.displayName || ""; - info.category = saved.category || ""; - info.pincode = saved.pincode || ""; - info.signSk = Buffer.from(saved.signSk || "", "hex"); - info.signPk = Buffer.from(saved.signPk || "", "hex"); - - info.pairedClients = {}; + const info = new AccessoryInfo(username) + info.displayName = saved.displayName || '' + info.category = saved.category || '' + info.pincode = saved.pincode || '' + info.signSk = Buffer.from(saved.signSk || '', 'hex') + info.signPk = Buffer.from(saved.signPk || '', 'hex') + + info.pairedClients = {} for (const username of Object.keys(saved.pairedClients || {})) { - const publicKey = saved.pairedClients[username]; - let permission = saved.pairedClientsPermission? saved.pairedClientsPermission[username]: undefined; + const publicKey = saved.pairedClients[username] + let permission = saved.pairedClientsPermission ? saved.pairedClientsPermission[username] : undefined if (permission === undefined) { - permission = PermissionTypes.ADMIN; + permission = PermissionTypes.ADMIN } // defaulting to admin permissions is the only suitable solution, there is no way to recover permissions info.pairedClients[username] = { - username: username, - publicKey: Buffer.from(publicKey, "hex"), - permission: permission, - }; + username, + publicKey: Buffer.from(publicKey, 'hex'), + permission, + } if (permission === PermissionTypes.ADMIN) { - info.pairedAdminClients++; + info.pairedAdminClients++ } } - info.configVersion = saved.configVersion || 1; - info.configHash = saved.configHash || ""; + info.configVersion = saved.configVersion || 1 + info.configHash = saved.configHash || '' - info.setupID = saved.setupID || ""; + info.setupID = saved.setupID || '' - info.lastFirmwareVersion = saved.lastFirmwareVersion || getVersion(); + info.lastFirmwareVersion = saved.lastFirmwareVersion || getVersion() - info.ensureConfigVersionBounds(); + info.ensureConfigVersionBounds() - return info; + return info } else { - return null; + return null } } static remove(username: MacAddress): void { - const key = AccessoryInfo.persistKey(username); - HAPStorage.storage().removeItemSync(key); + const key = AccessoryInfo.persistKey(username) + HAPStorage.storage().removeItemSync(key) } static assertValidUsername(username: MacAddress): void { - assert.ok(AccessoryInfo.deviceIdPattern.test(username), - "The supplied username (" + username + ") is not valid " + - "(expected a format like 'XX:XX:XX:XX:XX:XX' with XX being a valid hexadecimal string). " + - "Note that, if you had this accessory already paired with the invalid username, you will need to repair " + - "the accessory and reconfigure your services in the Home app. " + - "Using an invalid username will lead to unexpected behaviour."); + assert.ok(AccessoryInfo.deviceIdPattern.test(username), `The supplied username (${username}) is not valid ` + + `(expected a format like 'XX:XX:XX:XX:XX:XX' with XX being a valid hexadecimal string). ` + + `Note that, if you had this accessory already paired with the invalid username, you will need to repair ` + + `the accessory and reconfigure your services in the Home app. ` + + `Using an invalid username will lead to unexpected behaviour.`) } } - diff --git a/src/lib/model/ControllerStorage.ts b/src/lib/model/ControllerStorage.ts index 29c801fe3..bc0664e63 100644 --- a/src/lib/model/ControllerStorage.ts +++ b/src/lib/model/ControllerStorage.ts @@ -1,82 +1,83 @@ -import { MacAddress } from "../../types"; -import util from "util"; -import createDebug from "debug"; -import { ControllerIdentifier, SerializableController } from "../controller"; -import { Accessory } from "../Accessory"; -import { HAPStorage } from "./HAPStorage"; +/* global NodeJS */ +import type { MacAddress } from '../../types' +import type { Accessory } from '../Accessory' +import type { ControllerIdentifier, SerializableController } from '../controller' +import { format } from 'node:util' -const debug = createDebug("HAP-NodeJS:ControllerStorage"); +import createDebug from 'debug' + +import { HAPStorage } from './HAPStorage.js' + +const debug = createDebug('HAP-NodeJS:ControllerStorage') interface StorageLayout { - accessories: Record, // indexed by accessory UUID + accessories: Record // indexed by accessory UUID } interface StoredControllerData { - type: ControllerIdentifier, // this field is called type out of history - controllerData: ControllerData, + type: ControllerIdentifier // this field is called type out of history + controllerData: ControllerData } interface ControllerData { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any, + data: any /* This property and the exact sequence this property is accessed solves the following problems: - Orphaned ControllerData won't be there forever and gets cleared at some point - When storage is loaded, there is no fixed time frame after which Controllers need to be configured */ - purgeOnNextLoad?: boolean, + purgeOnNextLoad?: boolean } /** * @group Model */ export class ControllerStorage { - - private readonly accessoryUUID: string; - private initialized = false; + private readonly accessoryUUID: string + private initialized = false // ----- properties only set in parent storage object ------ - private username?: MacAddress; - private fileCreated = false; - purgeUnidentifiedAccessoryData = true; + private username?: MacAddress + private fileCreated = false + purgeUnidentifiedAccessoryData = true // --------------------------------------------------------- - private trackedControllers: SerializableController[] = []; // used to track controllers before data was loaded from disk - private controllerData: Record = {}; - private restoredAccessories?: Record; // indexed by accessory UUID + private trackedControllers: SerializableController[] = [] // used to track controllers before data was loaded from disk + private controllerData: Record = {} + private restoredAccessories?: Record // indexed by accessory UUID - private parent?: ControllerStorage; - private linkedAccessories?: ControllerStorage[]; + private parent?: ControllerStorage + private linkedAccessories?: ControllerStorage[] - private queuedSaveTimeout?: NodeJS.Timeout; - private queuedSaveTime?: number; + private queuedSaveTimeout?: NodeJS.Timeout + private queuedSaveTime?: number public constructor(accessory: Accessory) { - this.accessoryUUID = accessory.UUID; + this.accessoryUUID = accessory.UUID } private enqueueSaveRequest(timeout = 0): void { if (this.parent) { - this.parent.enqueueSaveRequest(timeout); - return; + this.parent.enqueueSaveRequest(timeout) + return } - const plannedTime = Date.now() + timeout; + const plannedTime = Date.now() + timeout if (this.queuedSaveTimeout) { if (plannedTime <= (this.queuedSaveTime ?? 0)) { - return; + return } - clearTimeout(this.queuedSaveTimeout); + clearTimeout(this.queuedSaveTimeout) } this.queuedSaveTimeout = setTimeout(() => { - this.queuedSaveTimeout = this.queuedSaveTime = undefined; - this.save(); - }, timeout).unref(); - this.queuedSaveTime = Date.now() + timeout; + this.queuedSaveTimeout = this.queuedSaveTime = undefined + this.save() + }, timeout).unref() + this.queuedSaveTime = Date.now() + timeout } /** @@ -86,88 +87,87 @@ export class ControllerStorage { */ public linkAccessory(accessory: Accessory): void { if (!this.linkedAccessories) { - this.linkedAccessories = []; + this.linkedAccessories = [] } - const storage = accessory.controllerStorage; - this.linkedAccessories.push(storage); - storage.parent = this; + const storage = accessory.controllerStorage + this.linkedAccessories.push(storage) + storage.parent = this - const saved = this.restoredAccessories && this.restoredAccessories[accessory.UUID]; + const saved = this.restoredAccessories && this.restoredAccessories[accessory.UUID] if (this.initialized) { - storage.init(saved); + storage.init(saved) } } public trackController(controller: SerializableController): void { - controller.setupStateChangeDelegate(this.handleStateChange.bind(this, controller)); // setup delegate + controller.setupStateChangeDelegate(this.handleStateChange.bind(this, controller)) // setup delegate if (!this.initialized) { // track controller if data isn't loaded yet - this.trackedControllers.push(controller); + this.trackedControllers.push(controller) } else { - this.restoreController(controller); + this.restoreController(controller) } } public untrackController(controller: SerializableController): void { - const index = this.trackedControllers.indexOf(controller); + const index = this.trackedControllers.indexOf(controller) if (index !== -1) { // remove from trackedControllers if storage wasn't initialized yet - this.trackedControllers.splice(index, 1); + this.trackedControllers.splice(index, 1) } - controller.setupStateChangeDelegate(undefined); // remove association with this storage object + controller.setupStateChangeDelegate(undefined) // remove association with this storage object - this.purgeControllerData(controller); + this.purgeControllerData(controller) } public purgeControllerData(controller: SerializableController): void { - delete this.controllerData[controller.controllerId()]; + delete this.controllerData[controller.controllerId()] if (this.initialized) { - this.enqueueSaveRequest(100); + this.enqueueSaveRequest(100) } } private handleStateChange(controller: SerializableController) { - const id = controller.controllerId(); - const serialized = controller.serialize(); + const id = controller.controllerId() + const serialized = controller.serialize() if (!serialized) { // can be undefined when controller wishes to delete data - delete this.controllerData[id]; + delete this.controllerData[id] } else { - const controllerData = this.controllerData[id]; + const controllerData = this.controllerData[id] if (!controllerData) { this.controllerData[id] = { data: serialized, - }; + } } else { - controllerData.data = serialized; + controllerData.data = serialized } } if (this.initialized) { // only save if data was loaded // run save data "async", as handleStateChange call will probably always be caused by a http request // this should improve our response time - this.enqueueSaveRequest(100); + this.enqueueSaveRequest(100) } } - private restoreController(controller: SerializableController) { if (!this.initialized) { - throw new Error("Illegal state. Controller data wasn't loaded yet!"); + throw new Error('Illegal state. Controller data wasn\'t loaded yet!') } - const controllerData = this.controllerData[controller.controllerId()]; + const controllerData = this.controllerData[controller.controllerId()] if (controllerData) { try { - controller.deserialize(controllerData.data); + controller.deserialize(controllerData.data) } catch (error) { - console.warn(`Could not initialize controller of type '${controller.controllerId()}' from data stored on disk. Resetting to default: ${error.stack}`); - controller.handleFactoryReset(); + console.warn(`Could not initialize controller of type '${controller.controllerId()}' from data stored on disk. Resetting to default: ${error.stack}`) + controller.handleFactoryReset() } - controllerData.purgeOnNextLoad = undefined; + controllerData.purgeOnNextLoad = undefined } } @@ -179,131 +179,130 @@ export class ControllerStorage { */ private init(data?: StoredControllerData[]) { if (this.initialized) { - throw new Error(`ControllerStorage for accessory ${this.accessoryUUID} was already initialized!`); + throw new Error(`ControllerStorage for accessory ${this.accessoryUUID} was already initialized!`) } - this.initialized = true; + this.initialized = true // storing data into our local controllerData Record - data?.forEach(saved => this.controllerData[saved.type] = saved.controllerData); + data?.forEach(saved => this.controllerData[saved.type] = saved.controllerData) - const restoredControllers: ControllerIdentifier[] = []; - this.trackedControllers.forEach(controller => { - this.restoreController(controller); - restoredControllers.push(controller.controllerId()); - }); - this.trackedControllers.splice(0, this.trackedControllers.length); // clear tracking list + const restoredControllers: ControllerIdentifier[] = [] + this.trackedControllers.forEach((controller) => { + this.restoreController(controller) + restoredControllers.push(controller.controllerId()) + }) + this.trackedControllers.splice(0, this.trackedControllers.length) // clear tracking list - let purgedData = false; + let purgedData = false Object.entries(this.controllerData).forEach(([id, data]) => { if (data.purgeOnNextLoad) { - delete this.controllerData[id]; - purgedData = true; - return; + delete this.controllerData[id] + purgedData = true + return } if (!restoredControllers.includes(id)) { - data.purgeOnNextLoad = true; + data.purgeOnNextLoad = true } - }); + }) if (purgedData) { - this.enqueueSaveRequest(500); + this.enqueueSaveRequest(500) } } public load(username: MacAddress): void { // will be called once accessory gets published if (this.username) { - throw new Error("ControllerStorage was already loaded!"); + throw new Error('ControllerStorage was already loaded!') } - this.username = username; + this.username = username - const key = ControllerStorage.persistKey(username); - const saved: StorageLayout | undefined = HAPStorage.storage().getItem(key); + const key = ControllerStorage.persistKey(username) + const saved: StorageLayout | undefined = HAPStorage.storage().getItem(key) - let ownData; + let ownData if (saved) { - this.fileCreated = true; + this.fileCreated = true - ownData = saved.accessories[this.accessoryUUID]; - delete saved.accessories[this.accessoryUUID]; + ownData = saved.accessories[this.accessoryUUID] + delete saved.accessories[this.accessoryUUID] } - this.init(ownData); + this.init(ownData) if (this.linkedAccessories) { - this.linkedAccessories.forEach(linkedStorage => { - const savedData = saved && saved.accessories[linkedStorage.accessoryUUID]; - linkedStorage.init(savedData); + this.linkedAccessories.forEach((linkedStorage) => { + const savedData = saved && saved.accessories[linkedStorage.accessoryUUID] + linkedStorage.init(savedData) if (saved) { - delete saved.accessories[linkedStorage.accessoryUUID]; + delete saved.accessories[linkedStorage.accessoryUUID] } - }); + }) } if (saved && Object.keys(saved.accessories).length > 0) { if (!this.purgeUnidentifiedAccessoryData) { - this.restoredAccessories = saved.accessories; // save data for controllers which aren't linked yet + this.restoredAccessories = saved.accessories // save data for controllers which aren't linked yet } else { - debug("Purging unidentified controller data for bridge %s", username); + debug('Purging unidentified controller data for bridge %s', username) } } } public save(): void { if (this.parent) { - this.parent.save(); - return; + this.parent.save() + return } if (!this.initialized) { - throw new Error("ControllerStorage has not yet been loaded!"); + throw new Error('ControllerStorage has not yet been loaded!') } if (!this.username) { - throw new Error("Cannot save controllerData for a storage without a username!"); + throw new Error('Cannot save controllerData for a storage without a username!') } const accessories: Record> = { [this.accessoryUUID]: this.controllerData, - }; + } if (this.linkedAccessories) { // grab data from all linked storage objects - this.linkedAccessories.forEach(accessory => accessories[accessory.accessoryUUID] = accessory.controllerData); + this.linkedAccessories.forEach(accessory => accessories[accessory.accessoryUUID] = accessory.controllerData) } // TODO removed accessories won't ever be deleted? - const accessoryData: Record = this.restoredAccessories || {}; + const accessoryData: Record = this.restoredAccessories || {} Object.entries(accessories).forEach(([uuid, controllerData]) => { - const entries = Object.entries(controllerData); + const entries = Object.entries(controllerData) if (entries.length > 0) { accessoryData[uuid] = entries.map(([id, data]) => ({ type: id, controllerData: data, - })); + })) } - }); + }) - const key = ControllerStorage.persistKey(this.username); + const key = ControllerStorage.persistKey(this.username) if (Object.keys(accessoryData).length > 0) { const saved: StorageLayout = { accessories: accessoryData, - }; + } - this.fileCreated = true; - HAPStorage.storage().setItemSync(key, saved); + this.fileCreated = true + HAPStorage.storage().setItemSync(key, saved) } else if (this.fileCreated) { - this.fileCreated = false; - HAPStorage.storage().removeItemSync(key); + this.fileCreated = false + HAPStorage.storage().removeItemSync(key) } } static persistKey(username: MacAddress): string { - return util.format("ControllerStorage.%s.json", username.replace(/:/g, "").toUpperCase()); + return format('ControllerStorage.%s.json', username.replace(/:/g, '').toUpperCase()) } static remove(username: MacAddress): void { - const key = ControllerStorage.persistKey(username); - HAPStorage.storage().removeItemSync(key); + const key = ControllerStorage.persistKey(username) + HAPStorage.storage().removeItemSync(key) } - } diff --git a/src/lib/model/HAPStorage.spec.ts b/src/lib/model/HAPStorage.spec.ts index 64be6c326..c0458db14 100644 --- a/src/lib/model/HAPStorage.spec.ts +++ b/src/lib/model/HAPStorage.spec.ts @@ -1,46 +1,48 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment +import { describe, expect, it, vi } from 'vitest' + +// eslint-disable-next-line ts/ban-ts-comment // @ts-ignore -import nodePersist from "node-persist"; -import { HAPStorage } from "./HAPStorage"; +import nodePersist from 'node-persist' -describe(HAPStorage, () => { +import { HAPStorage } from './HAPStorage.js' - describe("storage", () => { - it("should init storage correctly and only once", () => { - const storage = new HAPStorage(); +vi.mock('node-persist') + +describe(HAPStorage, () => { + describe('storage', () => { + it('should init storage correctly and only once', () => { + const storage = new HAPStorage() // @ts-expect-error: private access - expect(storage.localStore).toBeUndefined(); - const localStore = storage.storage(); // init first time - expect(nodePersist.create).toHaveBeenCalledTimes(1); - expect(localStore.initSync).toHaveBeenCalledTimes(1); + expect(storage.localStore).toBeUndefined() + const localStore = storage.storage() // init first time + expect(nodePersist.create).toHaveBeenCalledTimes(1) + expect(localStore.initSync).toHaveBeenCalledTimes(1) // @ts-expect-error: private access - expect(storage.localStore).toBeDefined(); - const localStore2 = storage.storage(); // init first time - expect(nodePersist.create).toHaveBeenCalledTimes(1); - expect(localStore2).toEqual(localStore); - expect(localStore2.initSync).toHaveBeenCalledTimes(1); - }); - - }); - - describe("setCustomStoragePath", () => { - it("should init storage correctly with custom storage path", () => { - const storage = new HAPStorage(); - - storage.setCustomStoragePath("asdfPath"); - const localStore = storage.storage(); - expect(localStore.initSync).toHaveBeenCalledTimes(1); - expect(localStore.initSync).toHaveBeenLastCalledWith({ dir: "asdfPath" }); - }); - - it("should reject setCustomStoragePath after storage has already been initialized", () => { - const storage = new HAPStorage(); - - storage.storage(); - expect(() => storage.setCustomStoragePath("customPath")).toThrow(Error); - }); - }); - -}); + expect(storage.localStore).toBeDefined() + const localStore2 = storage.storage() // init first time + expect(nodePersist.create).toHaveBeenCalledTimes(1) + expect(localStore2).toEqual(localStore) + expect(localStore2.initSync).toHaveBeenCalledTimes(1) + }) + }) + + describe('setCustomStoragePath', () => { + it('should init storage correctly with custom storage path', () => { + const storage = new HAPStorage() + + storage.setCustomStoragePath('asdfPath') + const localStore = storage.storage() + expect(localStore.initSync).toHaveBeenCalledTimes(1) + expect(localStore.initSync).toHaveBeenLastCalledWith({ dir: 'asdfPath' }) + }) + + it('should reject setCustomStoragePath after storage has already been initialized', () => { + const storage = new HAPStorage() + + storage.storage() + expect(() => storage.setCustomStoragePath('customPath')).toThrow(Error) + }) + }) +}) diff --git a/src/lib/model/HAPStorage.ts b/src/lib/model/HAPStorage.ts index 45c066fc0..df7340aff 100644 --- a/src/lib/model/HAPStorage.ts +++ b/src/lib/model/HAPStorage.ts @@ -1,47 +1,45 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import storage, { LocalStorage } from "node-persist"; +import type { LocalStorage } from 'node-persist' + +import storage from 'node-persist' /** * @group Model */ export class HAPStorage { + private static readonly INSTANCE = new HAPStorage() - private static readonly INSTANCE = new HAPStorage(); - - private localStore?: LocalStorage; - private customStoragePath?: string; + private localStore?: LocalStorage + private customStoragePath?: string public static storage(): LocalStorage { - return this.INSTANCE.storage(); + return this.INSTANCE.storage() } public static setCustomStoragePath(path: string): void { - this.INSTANCE.setCustomStoragePath(path); + this.INSTANCE.setCustomStoragePath(path) } public storage(): LocalStorage { if (!this.localStore) { - this.localStore = storage.create(); + this.localStore = storage.create() if (this.customStoragePath) { this.localStore.initSync({ dir: this.customStoragePath, - }); + }) } else { - this.localStore.initSync(); + this.localStore.initSync() } } - return this.localStore; + return this.localStore } public setCustomStoragePath(path: string): void { if (this.localStore) { - throw new Error("Cannot change storage path after it has already been initialized!"); + throw new Error('Cannot change storage path after it has already been initialized!') } - this.customStoragePath = path; + this.customStoragePath = path } - } diff --git a/src/lib/model/IdentifierCache.spec.ts b/src/lib/model/IdentifierCache.spec.ts index 5e8c7c158..fa0145ad9 100644 --- a/src/lib/model/IdentifierCache.spec.ts +++ b/src/lib/model/IdentifierCache.spec.ts @@ -1,126 +1,129 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// eslint-disable-next-line ts/ban-ts-comment // @ts-ignore -import { LocalStorage } from "node-persist"; -import { IdentifierCache } from "./IdentifierCache"; -import { HAPStorage } from "./HAPStorage"; +import type { LocalStorage } from 'node-persist' + +import { describe, expect, it, vi } from 'vitest' + +import { HAPStorage } from './HAPStorage.js' +import { IdentifierCache } from './IdentifierCache.js' function pullOutLocalStore(): LocalStorage { // @ts-expect-error: private access - return HAPStorage.INSTANCE.localStore; + return HAPStorage.INSTANCE.localStore } -const createIdentifierCache = (username = "username") => { - return new IdentifierCache(username); -}; - -describe("IdentifierCache", () => { - - describe("#startTrackingUsage()", () => { +function createIdentifierCache(username = 'username') { + return new IdentifierCache(username) +} - it ("creates a cache to track usage and expiring keys", () => { - const identifierCache = createIdentifierCache(); +vi.mock('node-persist') - expect(identifierCache._usedCache).toBeNull(); - identifierCache.startTrackingUsage(); - expect(identifierCache._usedCache).toEqual({}); - }); - }); - - describe("#stopTrackingUsageAndExpireUnused()", () => { - it ("creates a cache to track usage and expiring keys", () => { - const identifierCache = createIdentifierCache(); - - expect(identifierCache._usedCache).toBeNull(); - identifierCache.startTrackingUsage(); - expect(identifierCache._usedCache).toEqual({}); - identifierCache.stopTrackingUsageAndExpireUnused(); - expect(identifierCache._usedCache).toBeNull(); - }); - }); - - describe("#getCache()", () => { - it ("retrieves an item from the cache", () => { - const identifierCache = createIdentifierCache(); - - const VALUE = 1; - identifierCache.setCache("foo", VALUE); - - expect(identifierCache.getCache("foo")).toEqual(VALUE); - }); - - it ("returns undefined if an item is not found in the cache", () => { - const identifierCache = createIdentifierCache(); - - expect(identifierCache.getCache("foo")).toBeUndefined(); - }); - }); - - describe("#setCache()", () => { - it ("overwrites an existing item in the cache", () => { - const identifierCache = createIdentifierCache(); - - const VALUE = 2; - identifierCache.setCache("foo", 1); - identifierCache.setCache("foo", VALUE); - - expect(identifierCache.getCache("foo")).toEqual(VALUE); - }); - }); - - describe("#getAID()", () => { - it("creates an entry in the cache if the key is not found", () => { - const identifierCache = createIdentifierCache(); - - const result = identifierCache.getAID("00"); - expect(result).toEqual(2); - }); - }); - - describe("#getIID()", () => { - it("creates an entry in the cache if the key is not found", () => { - const identifierCache = createIdentifierCache(); - - const result = identifierCache.getIID("00", "11", "subtype", "99"); - expect(result).toEqual(2); - }); - - it("creates an entry in the cache if the key is not found, without a characteristic UUID", () => { - const identifierCache = createIdentifierCache(); - - const result = identifierCache.getIID("00", "11", "subtype"); - expect(result).toEqual(2); - }); - - it("creates an entry in the cache if the key is not found, without a service subtype or characteristic UUID", () => { - const identifierCache = createIdentifierCache(); - - const result = identifierCache.getIID("00", "11"); - expect(result).toEqual(2); - }); - }); - - describe("#save()", () => { - it("persists the cache to file storage", () => { - const identifierCache = createIdentifierCache(); - identifierCache.save(); - - expect(pullOutLocalStore().setItemSync).toHaveBeenCalledTimes(1); - }); - }); - - describe("#remove()", () => { - it("removes the cache from file storage", () => { - const identifierCache = createIdentifierCache(); - IdentifierCache.remove(identifierCache.username); - - expect(pullOutLocalStore().removeItemSync).toHaveBeenCalledTimes(1); - }); - }); - - describe("persistKey()", () => { - it("returns a correctly formatted key for persistence", () => { - const key = IdentifierCache.persistKey("username"); - expect(key).toEqual("IdentifierCache.USERNAME.json"); - }); - }); -}); +describe('identifierCache', () => { + describe('#startTrackingUsage()', () => { + it ('creates a cache to track usage and expiring keys', () => { + const identifierCache = createIdentifierCache() + + expect(identifierCache._usedCache).toBeNull() + identifierCache.startTrackingUsage() + expect(identifierCache._usedCache).toEqual({}) + }) + }) + + describe('#stopTrackingUsageAndExpireUnused()', () => { + it ('creates a cache to track usage and expiring keys', () => { + const identifierCache = createIdentifierCache() + + expect(identifierCache._usedCache).toBeNull() + identifierCache.startTrackingUsage() + expect(identifierCache._usedCache).toEqual({}) + identifierCache.stopTrackingUsageAndExpireUnused() + expect(identifierCache._usedCache).toBeNull() + }) + }) + + describe('#getCache()', () => { + it ('retrieves an item from the cache', () => { + const identifierCache = createIdentifierCache() + + const VALUE = 1 + identifierCache.setCache('foo', VALUE) + + expect(identifierCache.getCache('foo')).toEqual(VALUE) + }) + + it ('returns undefined if an item is not found in the cache', () => { + const identifierCache = createIdentifierCache() + + expect(identifierCache.getCache('foo')).toBeUndefined() + }) + }) + + describe('#setCache()', () => { + it ('overwrites an existing item in the cache', () => { + const identifierCache = createIdentifierCache() + + const VALUE = 2 + identifierCache.setCache('foo', 1) + identifierCache.setCache('foo', VALUE) + + expect(identifierCache.getCache('foo')).toEqual(VALUE) + }) + }) + + describe('#getAID()', () => { + it('creates an entry in the cache if the key is not found', () => { + const identifierCache = createIdentifierCache() + + const result = identifierCache.getAID('00') + expect(result).toEqual(2) + }) + }) + + describe('#getIID()', () => { + it('creates an entry in the cache if the key is not found', () => { + const identifierCache = createIdentifierCache() + + const result = identifierCache.getIID('00', '11', 'subtype', '99') + expect(result).toEqual(2) + }) + + it('creates an entry in the cache if the key is not found, without a characteristic UUID', () => { + const identifierCache = createIdentifierCache() + + const result = identifierCache.getIID('00', '11', 'subtype') + expect(result).toEqual(2) + }) + + it('creates an entry in the cache if the key is not found, without a service subtype or characteristic UUID', () => { + const identifierCache = createIdentifierCache() + + const result = identifierCache.getIID('00', '11') + expect(result).toEqual(2) + }) + }) + + describe('#save()', () => { + it('persists the cache to file storage', () => { + const identifierCache = createIdentifierCache() + identifierCache.save() + + expect(pullOutLocalStore().setItemSync).toHaveBeenCalledTimes(1) + }) + }) + + describe('#remove()', () => { + it('removes the cache from file storage', () => { + const identifierCache = createIdentifierCache() + IdentifierCache.remove(identifierCache.username) + + expect(pullOutLocalStore().removeItemSync).toHaveBeenCalledTimes(1) + }) + }) + + describe('persistKey()', () => { + it('returns a correctly formatted key for persistence', () => { + const key = IdentifierCache.persistKey('username') + expect(key).toEqual('IdentifierCache.USERNAME.json') + }) + }) +}) diff --git a/src/lib/model/IdentifierCache.ts b/src/lib/model/IdentifierCache.ts index a885446d8..0b93b6bee 100644 --- a/src/lib/model/IdentifierCache.ts +++ b/src/lib/model/IdentifierCache.ts @@ -1,7 +1,9 @@ -import crypto from "crypto"; -import util from "util"; -import { MacAddress } from "../../types"; -import { HAPStorage } from "./HAPStorage"; +import type { MacAddress } from '../../types' + +import { createHash } from 'node:crypto' +import { format } from 'node:util' + +import { HAPStorage } from './HAPStorage.js' /** * IdentifierCache is a model class that manages a system of associating HAP "Accessory IDs" and "Instance IDs" @@ -13,81 +15,81 @@ import { HAPStorage } from "./HAPStorage"; * @group Model */ export class IdentifierCache { - _cache: Record = {}; // cache[key:string] = id:number - _usedCache: Record | null = null; // for usage tracking and expiring old keys - _savedCacheHash = ""; // for checking if new cache need to be saved + _cache: Record = {} // cache[key:string] = id:number + _usedCache: Record | null = null // for usage tracking and expiring old keys + _savedCacheHash = '' // for checking if new cache need to be saved constructor(public username: MacAddress) { } startTrackingUsage(): void { - this._usedCache = {}; + this._usedCache = {} } stopTrackingUsageAndExpireUnused(): void { // simply rotate in the new cache that was built during our normal getXYZ() calls. - this._cache = this._usedCache || this._cache; - this._usedCache = null; + this._cache = this._usedCache || this._cache + this._usedCache = null } getCache(key: string): number { - const value = this._cache[key]; + const value = this._cache[key] // track this cache item if needed - if (this._usedCache && typeof value !== "undefined") { - this._usedCache[key] = value; + if (this._usedCache && typeof value !== 'undefined') { + this._usedCache[key] = value } - return value; + return value } setCache(key: string, value: number): number { - this._cache[key] = value; + this._cache[key] = value // track this cache item if needed if (this._usedCache) { - this._usedCache[key] = value; + this._usedCache[key] = value } - return value; + return value } getAID(accessoryUUID: string): number { - const key = accessoryUUID; + const key = accessoryUUID // ensure that our "next AID" field is not expired - this.getCache("|nextAID"); - return this.getCache(key) || this.setCache(key, this.getNextAID()); + this.getCache('|nextAID') + return this.getCache(key) || this.setCache(key, this.getNextAID()) } getIID(accessoryUUID: string, serviceUUID: string, serviceSubtype?: string, characteristicUUID?: string): number { - const key = accessoryUUID - + "|" + serviceUUID - + (serviceSubtype ? "|" + serviceSubtype : "") - + (characteristicUUID ? "|" + characteristicUUID : ""); + const key = `${accessoryUUID + }|${serviceUUID + }${serviceSubtype ? `|${serviceSubtype}` : '' + }${characteristicUUID ? `|${characteristicUUID}` : ''}` // ensure that our "next IID" field for this accessory is not expired - this.getCache(accessoryUUID + "|nextIID"); - return this.getCache(key) || this.setCache(key, this.getNextIID(accessoryUUID)); + this.getCache(`${accessoryUUID}|nextIID`) + return this.getCache(key) || this.setCache(key, this.getNextIID(accessoryUUID)) } getNextAID(): number { - const key = "|nextAID"; - const nextAID = this.getCache(key) || 2; // start at 2 because the root Accessory or Bridge must be 1 - this.setCache(key, nextAID + 1); // increment - return nextAID; + const key = '|nextAID' + const nextAID = this.getCache(key) || 2 // start at 2 because the root Accessory or Bridge must be 1 + this.setCache(key, nextAID + 1) // increment + return nextAID } getNextIID(accessoryUUID: string): number { - const key = accessoryUUID + "|nextIID"; - const nextIID = this.getCache(key) || 2; // iid 1 is reserved for the Accessory Information service - this.setCache(key, nextIID + 1); // increment - return nextIID; + const key = `${accessoryUUID}|nextIID` + const nextIID = this.getCache(key) || 2 // iid 1 is reserved for the Accessory Information service + this.setCache(key, nextIID + 1) // increment + return nextIID } save(): void { - const newCacheHash = crypto.createHash("sha1").update(JSON.stringify(this._cache)).digest("hex"); //calculate hash of new cache - if (newCacheHash !== this._savedCacheHash) { //check if cache need to be saved and proceed accordingly + const newCacheHash = createHash('sha1').update(JSON.stringify(this._cache)).digest('hex') // calculate hash of new cache + if (newCacheHash !== this._savedCacheHash) { // check if cache need to be saved and proceed accordingly const saved = { cache: this._cache, - }; - const key = IdentifierCache.persistKey(this.username); - HAPStorage.storage().setItemSync(key, saved); - this._savedCacheHash = newCacheHash; //update hash of saved cache for future use + } + const key = IdentifierCache.persistKey(this.username) + HAPStorage.storage().setItemSync(key, saved) + this._savedCacheHash = newCacheHash // update hash of saved cache for future use } } @@ -96,38 +98,25 @@ export class IdentifierCache { */ // Gets a key for storing this IdentifierCache in the filesystem, like "IdentifierCache.CC223DE3CEF3.json" static persistKey(username: MacAddress): string { - return util.format("IdentifierCache.%s.json", username.replace(/:/g, "").toUpperCase()); + return format('IdentifierCache.%s.json', username.replace(/:/g, '').toUpperCase()) } static load(username: MacAddress): IdentifierCache | null { - const key = IdentifierCache.persistKey(username); - const saved = HAPStorage.storage().getItem(key); + const key = IdentifierCache.persistKey(username) + const saved = HAPStorage.storage().getItem(key) if (saved) { - const info = new IdentifierCache(username); - info._cache = saved.cache; + const info = new IdentifierCache(username) + info._cache = saved.cache // calculate hash of the saved hash to decide in future if saving of new cache is needed - info._savedCacheHash = crypto.createHash("sha1").update(JSON.stringify(info._cache)).digest("hex"); - return info; + info._savedCacheHash = createHash('sha1').update(JSON.stringify(info._cache)).digest('hex') + return info } else { - return null; + return null } } static remove(username: MacAddress): void { - const key = this.persistKey(username); - HAPStorage.storage().removeItemSync(key); + const key = this.persistKey(username) + HAPStorage.storage().removeItemSync(key) } } - - - - - - - - - - - - - diff --git a/src/lib/tv/AccessControlManagement.ts b/src/lib/tv/AccessControlManagement.ts index d3e2fdb5d..bd08c538d 100644 --- a/src/lib/tv/AccessControlManagement.ts +++ b/src/lib/tv/AccessControlManagement.ts @@ -1,9 +1,13 @@ -import { EventEmitter } from "events"; -import { Characteristic } from "../Characteristic"; -import type { AccessControl } from "../definitions"; -import { Service } from "../Service"; -import * as tlv from "../util/tlv"; +import type { AccessControl } from '../definitions' +import { Buffer } from 'node:buffer' +import { EventEmitter } from 'node:events' + +import { Characteristic } from '../Characteristic.js' +import { Service } from '../Service.js' +import { decode } from '../util/tlv.js' + +// eslint-disable-next-line no-restricted-syntax const enum AccessControlTypes { PASSWORD = 0x01, PASSWORD_REQUIRED = 0x02, @@ -15,18 +19,18 @@ const enum AccessControlTypes { * * @group Television */ +// eslint-disable-next-line no-restricted-syntax export const enum AccessLevel { - // noinspection JSUnusedGlobalSymbols /** - * This access level is set when the users selects "Anyone" or "Anyone On The Same Network" + * This access level is set when the user selects "Anyone" or "Anyone On The Same Network" * in the Access Control settings. */ ANYONE = 0, /** - * This access level is set when the users selects "Only People Sharing this Home" in the + * This access level is set when the user selects "Only People Sharing this Home" in the * Access Control settings. * On this level password setting is ignored. - * Requests to the HAPServer can only come from Home members anyways, so there is no real use to it. + * Requests to the HAPServer can only come from Home members anyway, so there is no real use to it. * This is pretty much only used for the AirPlay 2 protocol. */ HOME_MEMBERS_ONLY = 1, @@ -38,78 +42,79 @@ export const enum AccessLevel { /** * @group Television */ +// eslint-disable-next-line no-restricted-syntax export const enum AccessControlEvent { - ACCESS_LEVEL_UPDATED = "update-control-level", - PASSWORD_SETTING_UPDATED = "update-password", + ACCESS_LEVEL_UPDATED = 'update-control-level', + PASSWORD_SETTING_UPDATED = 'update-password', } /** * @group Television */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface AccessControlManagement { - on(event: "update-control-level", listener: (accessLevel: AccessLevel) => void): this; - on(event: "update-password", listener: (password: string | undefined, passwordRequired: boolean) => void): this; - - emit(event: "update-control-level", accessLevel: AccessLevel): boolean; - emit(event: "update-password", password: string | undefined, passwordRequired: boolean): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'update-control-level', listener: (accessLevel: AccessLevel) => void): this + on(event: 'update-password', listener: (password: string | undefined, passwordRequired: boolean) => void): this + emit(event: 'update-control-level', accessLevel: AccessLevel): boolean + emit(event: 'update-password', password: string | undefined, passwordRequired: boolean): boolean + /* eslint-enable ts/method-signature-style */ } /** * @group Television */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class AccessControlManagement extends EventEmitter { - - private readonly accessControlService: AccessControl; + private readonly accessControlService: AccessControl /** * The current access level set for the Home */ - private accessLevel: AccessLevel = 0; + private accessLevel: AccessLevel = 0 - private passwordRequired = false; - private password?: string; // undefined if passwordRequired = false + private passwordRequired = false + private password?: string // undefined if passwordRequired = false /** * Instantiates a new AccessControlManagement. * * @param {boolean} password - if set to true the service will listen for password settings */ - constructor(password?: boolean); + constructor(password?: boolean) /** * Instantiates a new AccessControlManagement. * * @param {boolean} password - if set to true the service will listen for password settings * @param {AccessControl} service - supply your own instance to sideload the AccessControl service */ - constructor(password?: boolean, service?: AccessControl); + constructor(password?: boolean, service?: AccessControl) constructor(password?: boolean, service?: AccessControl) { - super(); + super() - this.accessControlService = service || new Service.AccessControl(); - this.setupServiceHandlers(password); + this.accessControlService = service || new Service.AccessControl() + this.setupServiceHandlers(password) } /** * @returns the AccessControl service */ public getService(): AccessControl { - return this.accessControlService; + return this.accessControlService } /** * @returns the current {@link AccessLevel} configured for the Home */ public getAccessLevel(): AccessLevel { - return this.accessLevel; + return this.accessLevel } /** * @returns the current password configured for the Home or `undefined` if no password is required. */ public getPassword(): string | undefined { - return this.passwordRequired? this.password: undefined; + return this.passwordRequired ? this.password : undefined } /** @@ -118,36 +123,36 @@ export class AccessControlManagement extends EventEmitter { * It removes all event handlers which were registered to this object. */ public destroy(): void { - this.removeAllListeners(); + this.removeAllListeners() - this.accessControlService.getCharacteristic(Characteristic.AccessControlLevel).removeOnSet(); + this.accessControlService.getCharacteristic(Characteristic.AccessControlLevel).removeOnSet() if (this.accessControlService.testCharacteristic(Characteristic.PasswordSetting)) { - this.accessControlService.getCharacteristic(Characteristic.PasswordSetting).removeOnSet(); + this.accessControlService.getCharacteristic(Characteristic.PasswordSetting).removeOnSet() } } private handleAccessLevelChange(value: number) { - this.accessLevel = value; + this.accessLevel = value setTimeout(() => { // timeout this so any action won't be executed on sync to the HAP request - this.emit(AccessControlEvent.ACCESS_LEVEL_UPDATED, this.accessLevel); - }, 0).unref(); + this.emit(AccessControlEvent.ACCESS_LEVEL_UPDATED, this.accessLevel) + }, 0).unref() } private handlePasswordChange(value: string) { - const data = Buffer.from(value, "base64"); - const objects = tlv.decode(data); + const data = Buffer.from(value, 'base64') + const objects = decode(data) if (objects[AccessControlTypes.PASSWORD]) { - this.password = objects[AccessControlTypes.PASSWORD].toString("utf8"); + this.password = objects[AccessControlTypes.PASSWORD].toString('utf8') } else { - this.password = undefined; + this.password = undefined } - this.passwordRequired = !!objects[AccessControlTypes.PASSWORD_REQUIRED][0]; + this.passwordRequired = !!objects[AccessControlTypes.PASSWORD_REQUIRED][0] setTimeout(() => { // timeout this so any action won't be executed on sync to the HAP request - this.emit(AccessControlEvent.PASSWORD_SETTING_UPDATED, this.password, this.passwordRequired); - }, 0).unref(); + this.emit(AccessControlEvent.PASSWORD_SETTING_UPDATED, this.password, this.passwordRequired) + }, 0).unref() } private setupServiceHandlers(enabledPasswordCharacteristics?: boolean) { @@ -155,13 +160,12 @@ export class AccessControlManagement extends EventEmitter { this.accessControlService.getCharacteristic(Characteristic.AccessControlLevel) .onSet(value => this.handleAccessLevelChange(value as number)) - .updateValue(0); + .updateValue(0) if (enabledPasswordCharacteristics) { this.accessControlService.getCharacteristic(Characteristic.PasswordSetting) .onSet(value => this.handlePasswordChange(value as string)) - .updateValue(""); + .updateValue('') } } - } diff --git a/src/lib/util/checkName.spec.ts b/src/lib/util/checkName.spec.ts index f4f9858a6..78be4b487 100644 --- a/src/lib/util/checkName.spec.ts +++ b/src/lib/util/checkName.spec.ts @@ -1,51 +1,55 @@ -import { checkName } from "./checkName"; +import type { MockInstance } from 'vitest' -describe("#checkName()", () => { - let consoleWarnSpy: jest.SpyInstance; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { checkName } from './checkName.js' + +describe('#checkName()', () => { + let consoleWarnSpy: MockInstance beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); + consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) afterEach(() => { - consoleWarnSpy.mockRestore(); - }); + consoleWarnSpy.mockRestore() + }) + + it('accessory Name ending with !', async () => { + checkName('displayName', 'Name', 'bad name!') + + expect(consoleWarnSpy).toBeCalledTimes(1) + + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'displayName\' has an invalid \'Name\' characteristic (\'bad name!\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') + }) + + it('accessory Name beginning with !', async () => { + checkName('displayName', 'Name', '!bad name') - test("Accessory Name ending with !", async () => { - checkName("displayName", "Name", "bad name!"); + expect(consoleWarnSpy).toBeCalledTimes(1) - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'displayName' has an invalid 'Name' characteristic ('bad name!'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); - }); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'displayName\' has an invalid \'Name\' characteristic (\'!bad name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') + }) - test("Accessory Name beginning with !", async () => { - checkName("displayName", "Name", "!bad name"); + it('accessory Name containing !', async () => { + checkName('displayName', 'Name', 'bad ! name') - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'displayName' has an invalid 'Name' characteristic ('!bad name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); - }); + expect(consoleWarnSpy).toBeCalledTimes(1) - test("Accessory Name containing !", async () => { - checkName("displayName", "Name", "bad ! name"); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'displayName\' has an invalid \'Name\' characteristic (\'bad ! name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') + }) - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'displayName' has an invalid 'Name' characteristic ('bad ! name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); - }); + it('accessory Name beginning with apostrophe', async () => { + checkName('displayName', 'Name', ' \'bad name') - test("Accessory Name beginning with '", async () => { - checkName("displayName", "Name", " 'bad name"); + expect(consoleWarnSpy).toBeCalledTimes(1) - expect(consoleWarnSpy).toBeCalledTimes(1); - // eslint-disable-next-line max-len - expect(consoleWarnSpy).toHaveBeenCalledWith("HAP-NodeJS WARNING: The accessory 'displayName' has an invalid 'Name' characteristic (' 'bad name'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness."); - }); + expect(consoleWarnSpy).toHaveBeenCalledWith('HAP-NodeJS WARNING: The accessory \'displayName\' has an invalid \'Name\' characteristic (\' \'bad name\'). Please use only alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the accessory from being added in the Home App or cause unresponsiveness.') + }) - test("Accessory Name containing '", async () => { - checkName("displayName", "Name", "good ' name"); + it('accessory Name containing apostrophe', async () => { + checkName('displayName', 'Name', 'good \' name') - expect(consoleWarnSpy).toBeCalledTimes(0); - }); -}); + expect(consoleWarnSpy).toBeCalledTimes(0) + }) +}) diff --git a/src/lib/util/checkName.ts b/src/lib/util/checkName.ts index e6c586d45..d6c9427cd 100644 --- a/src/lib/util/checkName.ts +++ b/src/lib/util/checkName.ts @@ -1,17 +1,16 @@ -import { CharacteristicValue, Nullable } from "../../types"; +import type { CharacteristicValue, Nullable } from '../../types' /** * Checks that supplied field meets Apple HomeKit naming rules * https://developer.apple.com/design/human-interface-guidelines/homekit#Help-people-choose-useful-names - * @private Private API + * @private */ export function checkName(displayName: string, name: string, value: Nullable): void { - // Ensure the string starts and ends with a Unicode letter or number and allow any combination of letters, numbers, spaces, and apostrophes in the middle. - if (typeof value === "string" && !(new RegExp(/^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u)).test(value)) { - console.warn("HAP-NodeJS WARNING: The accessory '" + displayName + "' has an invalid '" + name + "' characteristic ('" + value + "'). Please use only " + - "alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent " + - "the accessory from being added in the Home App or cause unresponsiveness."); + if (typeof value === 'string' && !/^[\p{L}\p{N}][\p{L}\p{N} ']*[\p{L}\p{N}]$/u.test(value)) { + console.warn(`HAP-NodeJS WARNING: The accessory '${displayName}' has an invalid '${name}' characteristic ('${value}'). Please use only ` + + `alphanumeric, space, and apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent ` + + `the accessory from being added in the Home App or cause unresponsiveness.`) } -} \ No newline at end of file +} diff --git a/src/lib/util/clone.ts b/src/lib/util/clone.ts index a0d467bba..74404518b 100644 --- a/src/lib/util/clone.ts +++ b/src/lib/util/clone.ts @@ -4,18 +4,17 @@ * @group Utils */ export function clone(object: T, extend?: U): T & U { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const cloned = {} as Record; + const cloned = {} as Record - for (const [ key, value ] of Object.entries(object)) { - cloned[key] = value; + for (const [key, value] of Object.entries(object)) { + cloned[key] = value } if (extend) { - for (const [ key, value ] of Object.entries(extend)) { - cloned[key] = value; + for (const [key, value] of Object.entries(extend)) { + cloned[key] = value } } - return cloned; + return cloned } diff --git a/src/lib/util/color-utils.ts b/src/lib/util/color-utils.ts index 32e1e985d..8f837a458 100644 --- a/src/lib/util/color-utils.ts +++ b/src/lib/util/color-utils.ts @@ -1,4 +1,4 @@ -import assert from "assert"; +import assert from 'node:assert' const lookupTable: Map = new Map([ // [100, [19, 222.1]], @@ -402,40 +402,38 @@ const lookupTable: Map = new Map([ // 500) { - colorTemperature = 500; + colorTemperature = 500 } else if (colorTemperature < 100) { - colorTemperature = 100; + colorTemperature = 100 } - colorTemperature = Math.round(colorTemperature); // ensure integer - const hueAndTemperature = lookupTable.get(colorTemperature); - assert(colorTemperature != null, "lookup for temperature " + colorTemperature + " did not yield any results"); + colorTemperature = Math.round(colorTemperature) // ensure integer + const hueAndTemperature = lookupTable.get(colorTemperature) + assert(colorTemperature != null, `lookup for temperature ${colorTemperature} did not yield any results`) if (roundResults) { - hueAndTemperature![0] = Math.round(hueAndTemperature![0]); - hueAndTemperature![1] = Math.round(hueAndTemperature![1]); + hueAndTemperature![0] = Math.round(hueAndTemperature![0]) + hueAndTemperature![1] = Math.round(hueAndTemperature![1]) } return { saturation: hueAndTemperature![0], hue: hueAndTemperature![1], - }; + } } - } diff --git a/src/lib/util/eventedhttp.spec.ts b/src/lib/util/eventedhttp.spec.ts index 9729317bd..82131b848 100644 --- a/src/lib/util/eventedhttp.spec.ts +++ b/src/lib/util/eventedhttp.spec.ts @@ -1,277 +1,279 @@ -import axios, { AxiosResponse } from "axios"; -import { Agent, IncomingMessage, ServerResponse } from "http"; -import { HAPHTTPClient } from "../../test-utils/HAPHTTPClient"; -import { HAPHTTPCode } from "../HAPServer"; -import { EventedHTTPServer, EventedHTTPServerEvent, HAPConnection, HAPConnectionEvent } from "./eventedhttp"; -import { awaitEventOnce, PromiseTimeout } from "./promise-utils"; - -describe("eventedhttp", () => { - let httpAgent: Agent; - let server: EventedHTTPServer; - let baseUrl: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const defaultRequestHandler = (connection: any, request: any, response: ServerResponse) => { - response.writeHead(HAPHTTPCode.OK, { "Content-Type": "plain/text" }); - response.end("Hello World", "ascii"); - }; +import type { IncomingMessage, ServerResponse } from 'node:http' + +import type { AxiosResponse } from 'axios' + +import type { HAPConnection } from './eventedhttp' + +import { Agent } from 'node:http' + +import axios from 'axios' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { HAPHTTPClient } from '../../test-utils/HAPHTTPClient.js' +import { HAPHTTPCode } from '../HAPServer.js' +import { EventedHTTPServer, EventedHTTPServerEvent, HAPConnectionEvent } from './eventedhttp.js' +import { awaitEventOnce, PromiseTimeout } from './promise-utils.js' + +function defaultRequestHandler(connection: any, request: any, response: ServerResponse) { + response.writeHead(HAPHTTPCode.OK, { 'Content-Type': 'plain/text' }) + response.end('Hello World', 'ascii') +} + +describe('eventedhttp', () => { + let httpAgent: Agent + let server: EventedHTTPServer + let baseUrl: string beforeEach(async () => { // used to do long living http connections without own tcp interface httpAgent = new Agent({ keepAlive: true, - }); + }) - server = new EventedHTTPServer(); + server = new EventedHTTPServer() // @ts-expect-error: private access - server.tcpServer.unref(); + server.tcpServer.unref() - server.listen(0); - await awaitEventOnce(server, EventedHTTPServerEvent.LISTENING); + server.listen(0) + await awaitEventOnce(server, EventedHTTPServerEvent.LISTENING) - const address = server.address(); - baseUrl = `http://0.0.0.0:${address.port}`; - }); + const address = server.address() + baseUrl = `http://0.0.0.0:${address.port}` + }) afterEach(() => { - server.stop(); - server.destroy(); - }); + server.stop() + server.destroy() + }) - test("simple http request", async () => { - const connectionOpened: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED); - const connectionClosed: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_CLOSED); + it('simple http request', async () => { + const connectionOpened: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED) + const connectionClosed: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_CLOSED) server.on(EventedHTTPServerEvent.REQUEST, (connection, request, response) => { - expect(request.method).toBe("GET"); - expect(request.url!.endsWith("/test?query=true")).toBeTruthy(); - response.writeHead(HAPHTTPCode.OK, { "Content-Type": "plain/text" }); - response.end("Hello World", "ascii"); - }); + expect(request.method).toBe('GET') + expect(request.url!.endsWith('/test?query=true')).toBeTruthy() + response.writeHead(HAPHTTPCode.OK, { 'Content-Type': 'plain/text' }) + response.end('Hello World', 'ascii') + }) - const result: AxiosResponse = await axios.get(`${baseUrl}/test?query=true`, { httpAgent }); - expect(result.data).toEqual("Hello World"); - const connection = await connectionOpened; + const result: AxiosResponse = await axios.get(`${baseUrl}/test?query=true`, { httpAgent }) + expect(result.data).toEqual('Hello World') + const connection = await connectionOpened // we simulate the connection getting authenticated (e.g. through pair-verify) - const username = "XX:XX:XX:XX:XX"; - const authenticatedEvent: Promise = awaitEventOnce(connection, HAPConnectionEvent.AUTHENTICATED); - connection.connectionAuthenticated(username); - await expect(authenticatedEvent).resolves.toBe(username); + const username = 'XX:XX:XX:XX:XX' + const authenticatedEvent: Promise = awaitEventOnce(connection, HAPConnectionEvent.AUTHENTICATED) + connection.connectionAuthenticated(username) + await expect(authenticatedEvent).resolves.toBe(username) - expect(connection.getLocalAddress("ipv4")).toBe("127.0.0.1"); - expect(connection.getLocalAddress("ipv6")).toBe("::1"); + expect(connection.getLocalAddress('ipv4')).toBe('127.0.0.1') + expect(connection.getLocalAddress('ipv6')).toBe('::1') - httpAgent.destroy(); // disconnect HAPConnection + httpAgent.destroy() // disconnect HAPConnection - await connectionClosed; - }); + await connectionClosed + }) - test("ensure connection handling respects nature of unpair", async () => { + it('ensure connection handling respects nature of unpair', async () => { // evented http server records the username of every authenticated HAPConnection. - // For the same Apple ID (username) there might be multiple connections (iDevices, hubs, etc). + // For the same Apple ID (username) there might be multiple connections (iDevices, hubs, etc.). // Once an unpair request happens, all other connections need to be torn down while the connection // that made the request must persist till the response is sent out! - const username = "XX:XX:XX:XX:XX:XX"; - const secondAgent = new Agent({ keepAlive: true }); + const username = 'XX:XX:XX:XX:XX:XX' + const secondAgent = new Agent({ keepAlive: true }) // OPEN CONNECTIONS - server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler); - const connectionOpened0: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED); - const result0: AxiosResponse = await axios.get(baseUrl, { httpAgent }); - expect(result0.data).toEqual("Hello World"); - const connection0 = await connectionOpened0; - - server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler); - const connectionOpened1: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED); - const result1: AxiosResponse = await axios.get(baseUrl, { httpAgent: secondAgent }); - expect(result1.data).toEqual("Hello World"); - const connection1 = await connectionOpened1; + server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler) + const connectionOpened0: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED) + const result0: AxiosResponse = await axios.get(baseUrl, { httpAgent }) + expect(result0.data).toEqual('Hello World') + const connection0 = await connectionOpened0 + + server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler) + const connectionOpened1: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED) + const result1: AxiosResponse = await axios.get(baseUrl, { httpAgent: secondAgent }) + expect(result1.data).toEqual('Hello World') + const connection1 = await connectionOpened1 // AUTHENTICATE CONNECTIONS - const authenticatedEvent0: Promise = awaitEventOnce(connection0, HAPConnectionEvent.AUTHENTICATED); - connection0.connectionAuthenticated(username); - await expect(authenticatedEvent0).resolves.toBe(username); + const authenticatedEvent0: Promise = awaitEventOnce(connection0, HAPConnectionEvent.AUTHENTICATED) + connection0.connectionAuthenticated(username) + await expect(authenticatedEvent0).resolves.toBe(username) - const authenticatedEvent1: Promise = awaitEventOnce(connection1, HAPConnectionEvent.AUTHENTICATED); - connection1.connectionAuthenticated(username); - await expect(authenticatedEvent1).resolves.toBe(username); + const authenticatedEvent1: Promise = awaitEventOnce(connection1, HAPConnectionEvent.AUTHENTICATED) + connection1.connectionAuthenticated(username) + await expect(authenticatedEvent1).resolves.toBe(username) // we make a request that we don't answer yet! - const queuedRequestPromise: Promise<[HAPConnection, IncomingMessage, ServerResponse]> = awaitEventOnce(server, EventedHTTPServerEvent.REQUEST); - const pendingRequest = axios.get(baseUrl, { httpAgent }); // simulate a "unpair" request! - const queuedResponse = (await queuedRequestPromise)[2]; + const queuedRequestPromise: Promise<[HAPConnection, IncomingMessage, ServerResponse]> = awaitEventOnce(server, EventedHTTPServerEvent.REQUEST) + const pendingRequest = axios.get(baseUrl, { httpAgent }) // simulate a "unpair" request! + const queuedResponse = (await queuedRequestPromise)[2] - // do the unpair!! - const connectionClosed: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_CLOSED); - EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection0, username); - await expect(connectionClosed).resolves.toBe(connection1); + // do the unpairing!! + const connectionClosed: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_CLOSED) + EventedHTTPServer.destroyExistingConnectionsAfterUnpair(connection0, username) + await expect(connectionClosed).resolves.toBe(connection1) - await PromiseTimeout(200); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((secondAgent as any).totalSocketCount <= 0).toBeTruthy(); // assert that the client side was closed! - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((httpAgent as any).totalSocketCount).toBe(1); // the other socket shouldn't be closed yet + await PromiseTimeout(200) + expect((secondAgent as any).totalSocketCount <= 0).toBeTruthy() // assert that the client side was closed! + expect((httpAgent as any).totalSocketCount).toBe(1) // the other socket shouldn't be closed yet // now finish the http request from above! - defaultRequestHandler(undefined, undefined, queuedResponse!); // just reuse the request handler from above! - const pendingRequestResult = await pendingRequest; - expect(pendingRequestResult.data).toBe("Hello World"); + defaultRequestHandler(undefined, undefined, queuedResponse!) // just reuse the request handler from above! + const pendingRequestResult = await pendingRequest + expect(pendingRequestResult.data).toBe('Hello World') - await PromiseTimeout(200); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((httpAgent as any).totalSocketCount <= 0).toBeTruthy(); // the other socket shall now be closed! - }); + await PromiseTimeout(200) + expect((httpAgent as any).totalSocketCount <= 0).toBeTruthy() // the other socket shall now be closed! + }) - test("event notifications", async () => { - const address = server.address(); + it('event notifications', async () => { + const address = server.address() - server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler); + server.once(EventedHTTPServerEvent.REQUEST, defaultRequestHandler) - const connectionOpened: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED); - const result: AxiosResponse = await axios.get(`${baseUrl}/test?query=true`, { httpAgent }); - expect(result.data).toEqual("Hello World"); - const connection = await connectionOpened; + const connectionOpened: Promise = awaitEventOnce(server, EventedHTTPServerEvent.CONNECTION_OPENED) + const result: AxiosResponse = await axios.get(`${baseUrl}/test?query=true`, { httpAgent }) + expect(result.data).toEqual('Hello World') + const connection = await connectionOpened - const client = new HAPHTTPClient(httpAgent, address.address, address.port); - client.attachSocket(); // capture the free socket of the http agent! + const client = new HAPHTTPClient(httpAgent, address.address, address.port) + client.attachSocket() // capture the free socket of the http agent! - connection.enableEventNotifications(1, 1); + connection.enableEventNotifications(1, 1) // we implicitly test below that this event won't be delivered! - connection.sendEvent(0, 0, "string"); - - expect(connection.hasEventNotifications(1, 1)).toBeTruthy(); - expect(connection.hasEventNotifications(0, 0)).toBeFalsy(); - expect(connection.getRegisteredEvents()).toEqual(new Set(["1.1"])); + connection.sendEvent(0, 0, 'string') + expect(connection.hasEventNotifications(1, 1)).toBeTruthy() + expect(connection.hasEventNotifications(0, 0)).toBeFalsy() + expect(connection.getRegisteredEvents()).toEqual(new Set(['1.1'])) - connection.sendEvent(1, 1, "Hello World!"); - connection.sendEvent(1, 1, "Hello World!"); // won't be sent (duplicate) - connection.sendEvent(1, 1, "Hello Mars!"); - connection.sendEvent(1, 1, "Hello World!"); // should send + connection.sendEvent(1, 1, 'Hello World!') + connection.sendEvent(1, 1, 'Hello World!') // won't be sent (duplicate) + connection.sendEvent(1, 1, 'Hello Mars!') + connection.sendEvent(1, 1, 'Hello World!') // should send // no immediate delivery! - await PromiseTimeout(50); - expect(client.receiveBufferCount).toBe(0); - - await PromiseTimeout(300); - expect(client.receiveBufferCount).toBe(1); - expect(client.popReceiveBuffer().toString()).toBe("EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: 143\r\n" + - "\r\n" + - "{\"characteristics\":[" + - "{\"aid\":1,\"iid\":1,\"value\":\"Hello World!\"}" + - ",{\"aid\":1,\"iid\":1,\"value\":\"Hello Mars!\"}," + - "{\"aid\":1,\"iid\":1,\"value\":\"Hello World!\"}" + - "]}"); - - - server.broadcastEvent(1, 1, "Hello Sun!", undefined, false); + await PromiseTimeout(50) + expect(client.receiveBufferCount).toBe(0) + + await PromiseTimeout(300) + expect(client.receiveBufferCount).toBe(1) + expect(client.popReceiveBuffer().toString()).toBe('EVENT/1.0 200 OK\r\n' + + 'Content-Type: application/hap+json\r\n' + + 'Content-Length: 143\r\n' + + '\r\n' + + '{"characteristics":[' + + '{"aid":1,"iid":1,"value":"Hello World!"}' + + ',{"aid":1,"iid":1,"value":"Hello Mars!"},' + + '{"aid":1,"iid":1,"value":"Hello World!"}' + + ']}') + + server.broadcastEvent(1, 1, 'Hello Sun!', undefined, false) // event with immediate delivery shall send currently queued events. Nothing gets out of order! - connection.sendEvent(1, 1, "Hello Mars!", true); - await PromiseTimeout(10); - expect(client.receiveBufferCount).toBe(1); - expect(client.popReceiveBuffer().toString()).toBe("EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: 100\r\n" + - "\r\n" + - "{\"characteristics\":[{\"aid\":1,\"iid\":1,\"value\":\"Hello Mars!\"},{\"aid\":1,\"iid\":1,\"value\":\"Hello Sun!\"}]}"); - - - server.broadcastEvent(1, 1, "Hello Sun!", connection, true); - await PromiseTimeout(10); - expect(client.receiveBufferCount).toBe(0); + connection.sendEvent(1, 1, 'Hello Mars!', true) + await PromiseTimeout(10) + expect(client.receiveBufferCount).toBe(1) + expect(client.popReceiveBuffer().toString()).toBe('EVENT/1.0 200 OK\r\n' + + 'Content-Type: application/hap+json\r\n' + + 'Content-Length: 100\r\n' + + '\r\n' + + '{"characteristics":[{"aid":1,"iid":1,"value":"Hello Mars!"},{"aid":1,"iid":1,"value":"Hello Sun!"}]}') + + server.broadcastEvent(1, 1, 'Hello Sun!', connection, true) + await PromiseTimeout(10) + expect(client.receiveBufferCount).toBe(0) // NOW we test event delivery when there is ongoing request const testEventDelivery = async (sendEvents: () => Promise, assertResult: () => Promise) => { - const queuedRequestPromise: Promise<[HAPConnection, IncomingMessage, ServerResponse]> = awaitEventOnce(server, EventedHTTPServerEvent.REQUEST); + const queuedRequestPromise: Promise<[HAPConnection, IncomingMessage, ServerResponse]> = awaitEventOnce(server, EventedHTTPServerEvent.REQUEST) // we can't use axios here, as our special EVENT http message is involved! - client.write(client.formatHTTPRequest("GET", "/")); - const queuedResponse: ServerResponse = (await queuedRequestPromise)[2]; + client.write(client.formatHTTPRequest('GET', '/')) + const queuedResponse: ServerResponse = (await queuedRequestPromise)[2] - await sendEvents(); + await sendEvents() - defaultRequestHandler(undefined, undefined, queuedResponse!); // just reuse the request handler from above! + defaultRequestHandler(undefined, undefined, queuedResponse!) // just reuse the request handler from above! - await PromiseTimeout(20); - await assertResult(); - }; + await PromiseTimeout(20) + await assertResult() + } await testEventDelivery( async () => { // we expect both events to be delivered immediately as there is one which is required to be delivered immediately - connection.sendEvent(1, 1, "Hello World!", true); - connection.sendEvent(1, 1, "Hello Mars!"); + connection.sendEvent(1, 1, 'Hello World!', true) + connection.sendEvent(1, 1, 'Hello Mars!') }, async () => { - expect(client.receiveBufferCount > 0).toBeTruthy(); + expect(client.receiveBufferCount > 0).toBeTruthy() - let eventMessage = ""; + let eventMessage = '' // sometimes the HTTP response message and the EVENT message are combined // into a single TCP segment, sometimes they get sent separately. while (client.receiveBufferCount > 0) { - eventMessage = client.popReceiveBuffer().toString(); + eventMessage = client.popReceiveBuffer().toString() } - expect(eventMessage.includes("EVENT/1.0")).toBeTruthy(); - const event = eventMessage.substring(eventMessage.indexOf("EVENT")); // splicing away the http response! - expect(event).toBe("EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: 102\r\n" + - "\r\n" + - "{\"characteristics\":[{\"aid\":1,\"iid\":1,\"value\":\"Hello Mars!\"},{\"aid\":1,\"iid\":1,\"value\":\"Hello World!\"}]}"); + expect(eventMessage.includes('EVENT/1.0')).toBeTruthy() + const event = eventMessage.substring(eventMessage.indexOf('EVENT')) // splicing away the http response! + expect(event).toBe('EVENT/1.0 200 OK\r\n' + + 'Content-Type: application/hap+json\r\n' + + 'Content-Length: 102\r\n' + + '\r\n' + + '{"characteristics":[{"aid":1,"iid":1,"value":"Hello Mars!"},{"aid":1,"iid":1,"value":"Hello World!"}]}') }, - ); + ) await testEventDelivery( async () => { - connection.sendEvent(1, 1, "Hello Mars!"); - connection.sendEvent(1, 1, "Hello Sun!"); + connection.sendEvent(1, 1, 'Hello Mars!') + connection.sendEvent(1, 1, 'Hello Sun!') }, async () => { - expect(client.receiveBufferCount).toBe(1); - expect(client.popReceiveBuffer().toString().includes("EVENT")).toBeFalsy(); - - await PromiseTimeout(300); - expect(client.receiveBufferCount).toBe(1); - expect(client.popReceiveBuffer().toString()).toBe("EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: 100\r\n" + - "\r\n" + - "{\"characteristics\":[{\"aid\":1,\"iid\":1,\"value\":\"Hello Sun!\"},{\"aid\":1,\"iid\":1,\"value\":\"Hello Mars!\"}]}"); + expect(client.receiveBufferCount).toBe(1) + expect(client.popReceiveBuffer().toString().includes('EVENT')).toBeFalsy() + + await PromiseTimeout(300) + expect(client.receiveBufferCount).toBe(1) + expect(client.popReceiveBuffer().toString()).toBe('EVENT/1.0 200 OK\r\n' + + 'Content-Type: application/hap+json\r\n' + + 'Content-Length: 100\r\n' + + '\r\n' + + '{"characteristics":[{"aid":1,"iid":1,"value":"Hello Sun!"},{"aid":1,"iid":1,"value":"Hello Mars!"}]}') }, - ); + ) await testEventDelivery( async () => { - connection.sendEvent(1, 1, "Hello Mars!"); - await PromiseTimeout(300); // the timeout runs out while the request is still processed + connection.sendEvent(1, 1, 'Hello Mars!') + await PromiseTimeout(300) // the timeout runs out while the request is still processed }, async () => { - expect(client.receiveBufferCount > 0).toBeTruthy(); + expect(client.receiveBufferCount > 0).toBeTruthy() - let eventMessage = ""; + let eventMessage = '' // sometimes the HTTP response message and the EVENT message are combined // into a single TCP segment, sometimes they get sent separately. while (client.receiveBufferCount > 0) { - eventMessage = client.popReceiveBuffer().toString(); + eventMessage = client.popReceiveBuffer().toString() } - expect(eventMessage.includes("EVENT/1.0")).toBeTruthy(); - const event = eventMessage.substring(eventMessage.indexOf("EVENT")); // splicing away the http response! - expect(event).toBe("EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: 61\r\n" + - "\r\n" + - "{\"characteristics\":[{\"aid\":1,\"iid\":1,\"value\":\"Hello Mars!\"}]}"); + expect(eventMessage.includes('EVENT/1.0')).toBeTruthy() + const event = eventMessage.substring(eventMessage.indexOf('EVENT')) // splicing away the http response! + expect(event).toBe('EVENT/1.0 200 OK\r\n' + + 'Content-Type: application/hap+json\r\n' + + 'Content-Length: 61\r\n' + + '\r\n' + + '{"characteristics":[{"aid":1,"iid":1,"value":"Hello Mars!"}]}') }, - ); + ) - client.releaseSocket(); - }); + client.releaseSocket() + }) // TODO test events not delivered while in a request! -}); +}) diff --git a/src/lib/util/eventedhttp.ts b/src/lib/util/eventedhttp.ts index 3117ce530..3a6598311 100644 --- a/src/lib/util/eventedhttp.ts +++ b/src/lib/util/eventedhttp.ts @@ -1,30 +1,38 @@ -import { getNetAddress } from "@homebridge/ciao/lib/util/domain-formatter"; -import assert from "assert"; -import createDebug from "debug"; -import { EventEmitter } from "events"; -import { SrpServer } from "fast-srp-hap"; -import http, { IncomingMessage, ServerResponse } from "http"; -import net, { AddressInfo, Socket } from "net"; -import os from "os"; -import { CharacteristicEventNotification, EventNotification } from "../../internal-types"; -import { CharacteristicValue, Nullable, SessionIdentifier } from "../../types"; -import * as hapCrypto from "./hapCrypto"; -import { getOSLoopbackAddressIfAvailable } from "./net-utils"; -import * as uuid from "./uuid"; - - -const debug = createDebug("HAP-NodeJS:EventedHTTPServer"); -const debugCon = createDebug("HAP-NodeJS:EventedHTTPServer:Connection"); -const debugEvents = createDebug("HAP-NodeJS:EventEmitter"); +/* global NodeJS */ +import type { Server as httpServer, IncomingMessage, ServerResponse } from 'node:http' +import type { AddressInfo, Server, Socket } from 'node:net' + +import type { SrpServer } from 'fast-srp-hap' + +import type { CharacteristicEventNotification, EventNotification } from '../../internal-types' +import type { CharacteristicValue, Nullable, SessionIdentifier } from '../../types' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { EventEmitter } from 'node:events' +import { createServer as createHttpServer } from 'node:http' +import { createConnection, createServer, isIPv4 } from 'node:net' +import { networkInterfaces } from 'node:os' + +import { getNetAddress } from '@homebridge/ciao/lib/util/domain-formatter.js' +import createDebug from 'debug' + +import { layerDecrypt, layerEncrypt } from './hapCrypto.js' +import { getOSLoopbackAddressIfAvailable } from './net-utils.js' +import { generate } from './uuid.js' + +const debug = createDebug('HAP-NodeJS:EventedHTTPServer') +const debugCon = createDebug('HAP-NodeJS:EventedHTTPServer:Connection') +const debugEvents = createDebug('HAP-NodeJS:EventEmitter') /** * @group HAP Accessory Server */ -export type HAPUsername = string; +export type HAPUsername = string /** * @group HAP Accessory Server */ -export type EventName = string; // "." +export type EventName = string // "." /** * Simple struct to hold vars needed to support HAP encryption. @@ -32,58 +40,57 @@ export type EventName = string; // "." * @group Cryptography */ export class HAPEncryption { + readonly clientPublicKey: Buffer + readonly secretKey: Buffer + readonly publicKey: Buffer + readonly sharedSecret: Buffer + readonly hkdfPairEncryptionKey: Buffer - readonly clientPublicKey: Buffer; - readonly secretKey: Buffer; - readonly publicKey: Buffer; - readonly sharedSecret: Buffer; - readonly hkdfPairEncryptionKey: Buffer; - - accessoryToControllerCount = 0; - controllerToAccessoryCount = 0; - accessoryToControllerKey: Buffer; - controllerToAccessoryKey: Buffer; + accessoryToControllerCount = 0 + controllerToAccessoryCount = 0 + accessoryToControllerKey: Buffer + controllerToAccessoryKey: Buffer - incompleteFrame?: Buffer; + incompleteFrame?: Buffer public constructor(clientPublicKey: Buffer, secretKey: Buffer, publicKey: Buffer, sharedSecret: Buffer, hkdfPairEncryptionKey: Buffer) { - this.clientPublicKey = clientPublicKey; - this.secretKey = secretKey; - this.publicKey = publicKey; - this.sharedSecret = sharedSecret; - this.hkdfPairEncryptionKey = hkdfPairEncryptionKey; + this.clientPublicKey = clientPublicKey + this.secretKey = secretKey + this.publicKey = publicKey + this.sharedSecret = sharedSecret + this.hkdfPairEncryptionKey = hkdfPairEncryptionKey - this.accessoryToControllerKey = Buffer.alloc(0); - this.controllerToAccessoryKey = Buffer.alloc(0); + this.accessoryToControllerKey = Buffer.alloc(0) + this.controllerToAccessoryKey = Buffer.alloc(0) } } /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum EventedHTTPServerEvent { - LISTENING = "listening", - CONNECTION_OPENED = "connection-opened", - REQUEST = "request", - CONNECTION_CLOSED = "connection-closed", + LISTENING = 'listening', + CONNECTION_OPENED = 'connection-opened', + REQUEST = 'request', + CONNECTION_CLOSED = 'connection-closed', } /** * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface EventedHTTPServer { - - on(event: "listening", listener: (port: number, address: string) => void): this; - on(event: "connection-opened", listener: (connection: HAPConnection) => void): this; - on(event: "request", listener: (connection: HAPConnection, request: IncomingMessage, response: ServerResponse) => void): this; - on(event: "connection-closed", listener: (connection: HAPConnection) => void): this; - - emit(event: "listening", port: number, address: string): boolean; - emit(event: "connection-opened", connection: HAPConnection): boolean; - emit(event: "request", connection: HAPConnection, request: IncomingMessage, response: ServerResponse): boolean; - emit(event: "connection-closed", connection: HAPConnection): boolean; - + /* eslint-disable ts/method-signature-style */ + on(event: 'listening', listener: (port: number, address: string) => void): this + on(event: 'connection-opened', listener: (connection: HAPConnection) => void): this + on(event: 'request', listener: (connection: HAPConnection, request: IncomingMessage, response: ServerResponse) => void): this + on(event: 'connection-closed', listener: (connection: HAPConnection) => void): this + emit(event: 'listening', port: number, address: string): boolean + emit(event: 'connection-opened', connection: HAPConnection): boolean + emit(event: 'request', connection: HAPConnection, request: IncomingMessage, response: ServerResponse): boolean + emit(event: 'connection-closed', connection: HAPConnection): boolean + /* eslint-enable ts/method-signature-style */ } /** @@ -105,103 +112,102 @@ export declare interface EventedHTTPServer { * * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class EventedHTTPServer extends EventEmitter { + private static readonly CONNECTION_TIMEOUT_LIMIT = 16 // if we have more (or equal) # connections we start the timeout + private static readonly MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000 // 1h - private static readonly CONNECTION_TIMEOUT_LIMIT = 16; // if we have more (or equal) # connections we start the timeout - private static readonly MAX_CONNECTION_IDLE_TIME = 60 * 60 * 1000; // 1h - - private readonly tcpServer: net.Server; + private readonly tcpServer: Server /** * Set of all currently connected HAP connections. */ - private readonly connections: Set = new Set(); + private readonly connections: Set = new Set() /** * Session dictionary indexed by username/identifier. The username uniquely identifies every person added to the home. * So there can be multiple sessions open for a single username (multiple devices connected to the same Apple ID). */ - private readonly connectionsByUsername: Map = new Map(); - private connectionIdleTimeout?: NodeJS.Timeout; - private connectionLoggingInterval?: NodeJS.Timeout; + private readonly connectionsByUsername: Map = new Map() + private connectionIdleTimeout?: NodeJS.Timeout + private connectionLoggingInterval?: NodeJS.Timeout constructor() { - super(); - this.tcpServer = net.createServer(); + super() + this.tcpServer = createServer() } private scheduleNextConnectionIdleTimeout(): void { - this.connectionIdleTimeout = undefined; + this.connectionIdleTimeout = undefined if (!this.tcpServer.listening) { - return; + return } - debug("Running idle timeout timer..."); + debug('Running idle timeout timer...') - const currentTime = new Date().getTime(); - let nextTimeout = -1; + const currentTime = new Date().getTime() + let nextTimeout = -1 for (const connection of this.connections) { - const timeDelta = currentTime - connection.lastSocketOperation; + const timeDelta = currentTime - connection.lastSocketOperation if (timeDelta >= EventedHTTPServer.MAX_CONNECTION_IDLE_TIME) { - debug("[%s] Closing connection as it was inactive for " + timeDelta + "ms"); - connection.close(); + debug(`[%s] Closing connection as it was inactive for ${timeDelta}ms`) + connection.close() } else { - nextTimeout = Math.max(nextTimeout, EventedHTTPServer.MAX_CONNECTION_IDLE_TIME - timeDelta); + nextTimeout = Math.max(nextTimeout, EventedHTTPServer.MAX_CONNECTION_IDLE_TIME - timeDelta) } } if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT) { - this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout); + this.connectionIdleTimeout = setTimeout(this.scheduleNextConnectionIdleTimeout.bind(this), nextTimeout) } } public address(): AddressInfo { - return this.tcpServer.address() as AddressInfo; + return this.tcpServer.address() as AddressInfo } public listen(targetPort: number, hostname?: string): void { this.tcpServer.listen(targetPort, hostname, () => { - const address = this.tcpServer.address() as AddressInfo; // address() is only a string when listening to unix domain sockets + const address = this.tcpServer.address() as AddressInfo // address() is only a string when listening to unix domain sockets - debug("Server listening on %s:%s", address.family === "IPv6"? `[${address.address}]`: address.address, address.port); + debug('Server listening on %s:%s', address.family === 'IPv6' ? `[${address.address}]` : address.address, address.port) this.connectionLoggingInterval = setInterval(() => { const connectionInformation = [...this.connections] .map(connection => `${connection.remoteAddress}:${connection.remotePort}`) - .join(", "); - debug("Currently %d hap connections open: %s", this.connections.size, connectionInformation); - }, 60_000); - this.connectionLoggingInterval.unref(); + .join(', ') + debug('Currently %d hap connections open: %s', this.connections.size, connectionInformation) + }, 60_000) + this.connectionLoggingInterval.unref() - this.emit(EventedHTTPServerEvent.LISTENING, address.port, address.address); - }); + this.emit(EventedHTTPServerEvent.LISTENING, address.port, address.address) + }) - this.tcpServer.on("connection", this.onConnection.bind(this)); + this.tcpServer.on('connection', this.onConnection.bind(this)) } public stop(): void { if (this.connectionLoggingInterval != null) { - clearInterval(this.connectionLoggingInterval); - this.connectionLoggingInterval = undefined; + clearInterval(this.connectionLoggingInterval) + this.connectionLoggingInterval = undefined } if (this.connectionIdleTimeout != null) { - clearTimeout(this.connectionIdleTimeout); - this.connectionIdleTimeout = undefined; + clearTimeout(this.connectionIdleTimeout) + this.connectionIdleTimeout = undefined } - this.tcpServer.close(); + this.tcpServer.close() for (const connection of this.connections) { - connection.close(); + connection.close() } } public destroy(): void { - this.stop(); - this.removeAllListeners(); + this.stop() + this.removeAllListeners() } /** @@ -213,64 +219,64 @@ export class EventedHTTPServer extends EventEmitter { * @param value - The newly set value of the characteristic. * @param originator - If specified, the connection will not get an event message. * @param immediateDelivery - The HAP spec requires some characteristics to be delivery immediately. - * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. + * Namely, for the {@link Characteristic.ButtonEvent} and the {@link Characteristic.ProgrammableSwitchEvent} characteristics. */ public broadcastEvent(aid: number, iid: number, value: Nullable, originator?: HAPConnection, immediateDelivery?: boolean): void { for (const connection of this.connections) { if (connection === originator) { - debug("[%s] Muting event '%s' notification for this connection since it originated here.", connection.remoteAddress, aid + "." + iid); - continue; + debug('[%s] Muting event \'%s\' notification for this connection since it originated here.', connection.remoteAddress, `${aid}.${iid}`) + continue } - connection.sendEvent(aid, iid, value, immediateDelivery); + connection.sendEvent(aid, iid, value, immediateDelivery) } } private onConnection(socket: Socket): void { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const connection = new HAPConnection(this, socket); - + /* eslint-disable ts/no-use-before-define */ + const connection = new HAPConnection(this, socket) connection.on(HAPConnectionEvent.REQUEST, (request, response) => { - this.emit(EventedHTTPServerEvent.REQUEST, connection, request, response); - }); - connection.on(HAPConnectionEvent.AUTHENTICATED, this.handleConnectionAuthenticated.bind(this, connection)); - connection.on(HAPConnectionEvent.CLOSED, this.handleConnectionClose.bind(this, connection)); + this.emit(EventedHTTPServerEvent.REQUEST, connection, request, response) + }) + connection.on(HAPConnectionEvent.AUTHENTICATED, this.handleConnectionAuthenticated.bind(this, connection)) + connection.on(HAPConnectionEvent.CLOSED, this.handleConnectionClose.bind(this, connection)) + /* eslint-enable ts/no-use-before-define */ - this.connections.add(connection); + this.connections.add(connection) - debug("[%s] New connection from client on interface %s (%s)", connection.remoteAddress, connection.networkInterface, connection.localAddress); + debug('[%s] New connection from client on interface %s (%s)', connection.remoteAddress, connection.networkInterface, connection.localAddress) - this.emit(EventedHTTPServerEvent.CONNECTION_OPENED, connection); + this.emit(EventedHTTPServerEvent.CONNECTION_OPENED, connection) if (this.connections.size >= EventedHTTPServer.CONNECTION_TIMEOUT_LIMIT && !this.connectionIdleTimeout) { - this.scheduleNextConnectionIdleTimeout(); + this.scheduleNextConnectionIdleTimeout() } } private handleConnectionAuthenticated(connection: HAPConnection, username: HAPUsername): void { - const connections: HAPConnection[] | undefined = this.connectionsByUsername.get(username); + const connections: HAPConnection[] | undefined = this.connectionsByUsername.get(username) if (!connections) { - this.connectionsByUsername.set(username, [connection]); + this.connectionsByUsername.set(username, [connection]) } else if (!connections.includes(connection)) { // ensure this doesn't get added more than one time - connections.push(connection); + connections.push(connection) } } private handleConnectionClose(connection: HAPConnection): void { - this.emit(EventedHTTPServerEvent.CONNECTION_CLOSED, connection); + this.emit(EventedHTTPServerEvent.CONNECTION_CLOSED, connection) - this.connections.delete(connection); + this.connections.delete(connection) if (connection.username) { // aka connection was authenticated - const connections = this.connectionsByUsername.get(connection.username); + const connections = this.connectionsByUsername.get(connection.username) if (connections) { - const index = connections.indexOf(connection); + const index = connections.indexOf(connection) if (index !== -1) { - connections.splice(index, 1); + connections.splice(index, 1) } if (connections.length === 0) { - this.connectionsByUsername.delete(connection.username); + this.connectionsByUsername.delete(connection.username) } } } @@ -287,21 +293,21 @@ export class EventedHTTPServer extends EventEmitter { * @param username - The username for which all connections shall be closed. */ public static destroyExistingConnectionsAfterUnpair(initiator: HAPConnection, username: HAPUsername): void { - const connections: HAPConnection[] | undefined = initiator.server.connectionsByUsername.get(username); + const connections: HAPConnection[] | undefined = initiator.server.connectionsByUsername.get(username) if (connections) { for (const connection of connections) { - connection.closeConnectionAsOfUnpair(initiator); + connection.closeConnectionAsOfUnpair(initiator) } } } - } /** * @private * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPConnectionState { CONNECTING, // initial state, setup is going on FULLY_SET_UP, // internal http server is running and connection is established @@ -317,142 +323,139 @@ export const enum HAPConnectionState { /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum HAPConnectionEvent { - REQUEST = "request", - AUTHENTICATED = "authenticated", - CLOSED = "closed", + REQUEST = 'request', + AUTHENTICATED = 'authenticated', + CLOSED = 'closed', } /** * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export declare interface HAPConnection { - on(event: "request", listener: (request: IncomingMessage, response: ServerResponse) => void): this; - on(event: "authenticated", listener: (username: HAPUsername) => void): this; - on(event: "closed", listener: () => void): this; - - emit(event: "request", request: IncomingMessage, response: ServerResponse): boolean; - emit(event: "authenticated", username: HAPUsername): boolean; - emit(event: "closed"): boolean; + /* eslint-disable ts/method-signature-style */ + on(event: 'request', listener: (request: IncomingMessage, response: ServerResponse) => void): this + on(event: 'authenticated', listener: (username: HAPUsername) => void): this + on(event: 'closed', listener: () => void): this + emit(event: 'request', request: IncomingMessage, response: ServerResponse): boolean + emit(event: 'authenticated', username: HAPUsername): boolean + emit(event: 'closed'): boolean + /* eslint-enable ts/method-signature-style */ } /** * Manages a single iOS-initiated HTTP connection during its lifetime. * @group HAP Accessory Server */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +// eslint-disable-next-line ts/no-unsafe-declaration-merging export class HAPConnection extends EventEmitter { /** - * @private file-private API + * @private */ - readonly server: EventedHTTPServer; - - readonly sessionID: SessionIdentifier; // uuid unique to every HAP connection - private state: HAPConnectionState = HAPConnectionState.CONNECTING; - readonly localAddress: string; - readonly remoteAddress: string; // cache because it becomes undefined in 'onClientSocketClose' - readonly remotePort: number; - readonly networkInterface: string; - - private readonly tcpSocket: Socket; - private readonly internalHttpServer: http.Server; - private httpSocket?: Socket; // set when in state FULLY_SET_UP - private internalHttpServerPort?: number; - private internalHttpServerAddress?: string; - - lastSocketOperation: number = new Date().getTime(); - - private pendingClientSocketData?: Buffer = Buffer.alloc(0); // data received from client before HTTP proxy is fully setup - private handlingRequest = false; // true while we are composing an HTTP response (so events can wait) - - username?: HAPUsername; // username is unique to every user in the home, basically identifies an Apple ID - encryption?: HAPEncryption; // created in handlePairVerifyStepOne - srpServer?: SrpServer; - _pairSetupState?: number; // TODO ensure those two states are always correctly reset? - _pairVerifyState?: number; - - private registeredEvents: Set = new Set(); - private eventsTimer?: NodeJS.Timeout; - private readonly queuedEvents: CharacteristicEventNotification[] = []; + readonly server: EventedHTTPServer + + readonly sessionID: SessionIdentifier // uuid unique to every HAP connection + private state: HAPConnectionState = HAPConnectionState.CONNECTING + readonly localAddress: string + readonly remoteAddress: string // cache because it becomes undefined in 'onClientSocketClose' + readonly remotePort: number + readonly networkInterface: string + + private readonly tcpSocket: Socket + private readonly internalHttpServer: httpServer + private httpSocket?: Socket // set when in state FULLY_SET_UP + private internalHttpServerPort?: number + private internalHttpServerAddress?: string + + lastSocketOperation: number = new Date().getTime() + + private pendingClientSocketData?: Buffer = Buffer.alloc(0) // data received from client before HTTP proxy is fully setup + private handlingRequest = false // true while we are composing an HTTP response (so events can wait) + + username?: HAPUsername // username is unique to every user in the home, basically identifies an Apple ID + encryption?: HAPEncryption // created in handlePairVerifyStepOne + srpServer?: SrpServer + _pairSetupState?: number // TODO ensure those two states are always correctly reset? + _pairVerifyState?: number + + private registeredEvents: Set = new Set() + private eventsTimer?: NodeJS.Timeout + private readonly queuedEvents: CharacteristicEventNotification[] = [] /** * If true, the above {@link queuedEvents} contains events which are set to be delivered immediately! */ - private eventsQueuedForImmediateDelivery = false; + private eventsQueuedForImmediateDelivery = false - timedWritePid?: number; - timedWriteTimeout?: NodeJS.Timeout; + timedWritePid?: number + timedWriteTimeout?: NodeJS.Timeout constructor(server: EventedHTTPServer, clientSocket: Socket) { - super(); + super() - this.server = server; - this.sessionID = uuid.generate(clientSocket.remoteAddress + ":" + clientSocket.remotePort); - this.localAddress = clientSocket.localAddress as string; - this.remoteAddress = clientSocket.remoteAddress!; // cache because it becomes undefined in 'onClientSocketClose' - this.remotePort = clientSocket.remotePort!; - this.networkInterface = HAPConnection.getLocalNetworkInterface(clientSocket); + this.server = server + this.sessionID = generate(`${clientSocket.remoteAddress}:${clientSocket.remotePort}`) + this.localAddress = clientSocket.localAddress as string + this.remoteAddress = clientSocket.remoteAddress! // cache because it becomes undefined in 'onClientSocketClose' + this.remotePort = clientSocket.remotePort! + this.networkInterface = HAPConnection.getLocalNetworkInterface(clientSocket) // clientSocket is the socket connected to the actual iOS device - this.tcpSocket = clientSocket; - this.tcpSocket.on("data", this.onTCPSocketData.bind(this)); - this.tcpSocket.on("close", this.onTCPSocketClose.bind(this)); + this.tcpSocket = clientSocket + this.tcpSocket.on('data', this.onTCPSocketData.bind(this)) + this.tcpSocket.on('close', this.onTCPSocketClose.bind(this)) // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. - this.tcpSocket.on("error", this.onTCPSocketError.bind(this)); - this.tcpSocket.setNoDelay(true); // disable Nagle algorithm + this.tcpSocket.on('error', this.onTCPSocketError.bind(this)) + this.tcpSocket.setNoDelay(true) // disable Nagle algorithm // "HAP accessory servers must not use keepalive messages, which periodically wake up iOS devices". // Thus, we don't configure any tcp keepalive // create our internal HTTP server for this connection that we will proxy data to and from - this.internalHttpServer = http.createServer(); - this.internalHttpServer.timeout = 0; // clients expect to hold connections open as long as they want - this.internalHttpServer.keepAliveTimeout = 0; // workaround for https://github.com/nodejs/node/issues/13391 - this.internalHttpServer.on("listening", this.onHttpServerListening.bind(this)); - this.internalHttpServer.on("request", this.handleHttpServerRequest.bind(this)); - this.internalHttpServer.on("error", this.onHttpServerError.bind(this)); + this.internalHttpServer = createHttpServer() + this.internalHttpServer.timeout = 0 // clients expect to hold connections open as long as they want + this.internalHttpServer.keepAliveTimeout = 0 // workaround for https://github.com/nodejs/node/issues/13391 + this.internalHttpServer.on('listening', this.onHttpServerListening.bind(this)) + this.internalHttpServer.on('request', this.handleHttpServerRequest.bind(this)) + this.internalHttpServer.on('error', this.onHttpServerError.bind(this)) // close event is added later on the "connect" event as possible listen retries would throw unnecessary close events - this.internalHttpServer.listen(0, this.internalHttpServerAddress = getOSLoopbackAddressIfAvailable()); + this.internalHttpServer.listen(0, this.internalHttpServerAddress = getOSLoopbackAddressIfAvailable()) } private debugListenerRegistration(event: string | symbol, registration = true, beforeCount = -1): void { - const stackTrace = new Error().stack!.split("\n")[3]; - const eventCount = this.listeners(event).length; + const stackTrace = new Error().stack!.split('\n')[3] // eslint-disable-line unicorn/error-message + const eventCount = this.listeners(event).length - const tabs1 = event === HAPConnectionEvent.AUTHENTICATED ? "\t" : "\t\t"; - const tabs2 = !registration ? "\t" : "\t\t"; + const tabs1 = event === HAPConnectionEvent.AUTHENTICATED ? '\t' : '\t\t' + const tabs2 = !registration ? '\t' : '\t\t' - // eslint-disable-next-line max-len - debugEvents(`[${this.remoteAddress}] ${registration ? "Registered" : "Unregistered"} event '${String(event).toUpperCase()}' ${tabs1}(total: ${eventCount}${!registration ? " Before: " + beforeCount : ""}) ${tabs2}${stackTrace}`); + debugEvents(`[${this.remoteAddress}] ${registration ? 'Registered' : 'Unregistered'} event '${String(event).toUpperCase()}' ${tabs1}(total: ${eventCount}${!registration ? ` Before: ${beforeCount}` : ''}) ${tabs2}${stackTrace}`) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event: string | symbol, listener: (...args: any[]) => void): this { - const result = super.on(event, listener); - this.debugListenerRegistration(event); - return result; + const result = super.on(event, listener) + this.debugListenerRegistration(event) + return result } - // eslint-disable-next-line @typescript-eslint/no-explicit-any addListener(event: string | symbol, listener: (...args: any[]) => void): this { - const result = super.addListener(event, listener); - this.debugListenerRegistration(event); - return result; + const result = super.addListener(event, listener) + this.debugListenerRegistration(event) + return result } - // eslint-disable-next-line @typescript-eslint/no-explicit-any removeListener(event: string | symbol, listener: (...args: any[]) => void): this { - const beforeCount = this.listeners(event).length; - const result = super.removeListener(event, listener); - this.debugListenerRegistration(event, false, beforeCount); - return result; + const beforeCount = this.listeners(event).length + const result = super.removeListener(event, listener) + this.debugListenerRegistration(event, false, beforeCount) + return result } - // eslint-disable-next-line @typescript-eslint/no-explicit-any off(event: string | symbol, listener: (...args: any[]) => void): this { - const result = super.off(event, listener); - const beforeCount = this.listeners(event).length; - this.debugListenerRegistration(event, false, beforeCount); - return result; + const result = super.off(event, listener) + const beforeCount = this.listeners(event).length + this.debugListenerRegistration(event, false, beforeCount) + return result } /** @@ -463,127 +466,127 @@ export class HAPConnection extends EventEmitter { * Once this method has been called, the connection is authenticated and encryption is turned on. */ public connectionAuthenticated(username: HAPUsername): void { - this.state = HAPConnectionState.AUTHENTICATED; - this.username = username; + this.state = HAPConnectionState.AUTHENTICATED + this.username = username - this.emit(HAPConnectionEvent.AUTHENTICATED, username); + this.emit(HAPConnectionEvent.AUTHENTICATED, username) } public isAuthenticated(): boolean { - return this.state === HAPConnectionState.AUTHENTICATED; + return this.state === HAPConnectionState.AUTHENTICATED } public close(): void { if (this.state >= HAPConnectionState.CLOSING) { - return; // already closed/closing + return // already closed/closing } - this.state = HAPConnectionState.CLOSING; - this.tcpSocket.destroy(); + this.state = HAPConnectionState.CLOSING + this.tcpSocket.destroy() } public closeConnectionAsOfUnpair(initiator: HAPConnection): void { if (this === initiator) { // the initiator of the unpair request is this connection, meaning it unpaired itself. // we still need to send the response packet to the unpair request. - this.state = HAPConnectionState.TO_BE_TEARED_DOWN; + this.state = HAPConnectionState.TO_BE_TEARED_DOWN } else { // as HomeKit requires it, destroy any active session which got unpaired - this.close(); + this.close() } } public sendEvent(aid: number, iid: number, value: Nullable, immediateDelivery?: boolean): void { - assert(aid != null, "HAPConnection.sendEvent: aid must be defined!"); - assert(iid != null, "HAPConnection.sendEvent: iid must be defined!"); + assert(aid != null, 'HAPConnection.sendEvent: aid must be defined!') + assert(iid != null, 'HAPConnection.sendEvent: iid must be defined!') - const eventName = aid + "." + iid; + const eventName = `${aid}.${iid}` if (!this.registeredEvents.has(eventName)) { // non verified connections can't register events, so this case is covered! - return; + return } const event: CharacteristicEventNotification = { - aid: aid, - iid: iid, - value: value, - }; + aid, + iid, + value, + } if (immediateDelivery) { // some characteristics are required to deliver notifications immediately // we will flush all other events too, on that occasion. - this.queuedEvents.push(event); - this.eventsQueuedForImmediateDelivery = true; + this.queuedEvents.push(event) + this.eventsQueuedForImmediateDelivery = true if (this.eventsTimer) { - clearTimeout(this.eventsTimer); - this.eventsTimer = undefined; + clearTimeout(this.eventsTimer) + this.eventsTimer = undefined } - this.handleEventsTimeout(); - return; + this.handleEventsTimeout() + return } // we search the list of queued events in reverse order. // if the last element with the same aid and iid has the same value we don't want to send the event notification twice. // BUT, we do not want to override previous event notifications which have a different value. Automations must be executed! for (let i = this.queuedEvents.length - 1; i >= 0; i--) { - const queuedEvent = this.queuedEvents[i]; + const queuedEvent = this.queuedEvents[i] if (queuedEvent.aid === aid && queuedEvent.iid === iid) { if (queuedEvent.value === value) { - return; // the same event was already queued. do not add it again! + return // the same event was already queued. do not add it again! } - break; // we break in any case + break // we break in any case } } - this.queuedEvents.push(event); + this.queuedEvents.push(event) // if there is already a timer running we just add it in the queue. if (!this.eventsTimer) { - this.eventsTimer = setTimeout(this.handleEventsTimeout.bind(this), 250); - this.eventsTimer.unref(); + this.eventsTimer = setTimeout(this.handleEventsTimeout.bind(this), 250) + this.eventsTimer.unref() } } private handleEventsTimeout(): void { - this.eventsTimer = undefined; + this.eventsTimer = undefined if (this.state > HAPConnectionState.AUTHENTICATED) { // connection is closed or about to be closed. no need to send any further events - return; + return } - this.writeQueuedEventNotifications(); + this.writeQueuedEventNotifications() } private writeQueuedEventNotifications(): void { if (this.queuedEvents.length === 0 || this.handlingRequest) { - return; // don't send empty event notifications or if we are currently handling a request + return // don't send empty event notifications or if we are currently handling a request } if (this.eventsTimer) { // this method might be called when we have enqueued data AND data that is queued for immediate delivery! - clearTimeout(this.eventsTimer); - this.eventsTimer = undefined; + clearTimeout(this.eventsTimer) + this.eventsTimer = undefined } const eventData: EventNotification = { characteristics: [], - }; + } for (const queuedEvent of this.queuedEvents) { - if (!this.registeredEvents.has(queuedEvent.aid + "." + queuedEvent.iid)) { - continue; // client unregistered that event in the meantime + if (!this.registeredEvents.has(`${queuedEvent.aid}.${queuedEvent.iid}`)) { + continue // client unregistered that event in the meantime } - eventData.characteristics.push(queuedEvent); + eventData.characteristics.push(queuedEvent) } - this.queuedEvents.splice(0, this.queuedEvents.length); - this.eventsQueuedForImmediateDelivery = false; + this.queuedEvents.splice(0, this.queuedEvents.length) + this.eventsQueuedForImmediateDelivery = false - this.writeEventNotification(eventData); + this.writeEventNotification(eventData) } /** @@ -594,44 +597,44 @@ export class HAPConnection extends EventEmitter { * @param notification - The event which should be sent out */ private writeEventNotification(notification: EventNotification): void { - debugCon("[%s] Sending HAP event notifications %o", this.remoteAddress, notification.characteristics); - assert(!this.handlingRequest, "Can't write event notifications while handling a request!"); + debugCon('[%s] Sending HAP event notifications %o', this.remoteAddress, notification.characteristics) + assert(!this.handlingRequest, 'Can\'t write event notifications while handling a request!') // Apple backend processes events in reverse order, so we need to reverse the array // so that events are processed in chronological order. - notification.characteristics.reverse(); + notification.characteristics.reverse() - const dataBuffer = Buffer.from(JSON.stringify(notification), "utf8"); + const dataBuffer = Buffer.from(JSON.stringify(notification), 'utf8') const header = Buffer.from( - "EVENT/1.0 200 OK\r\n" + - "Content-Type: application/hap+json\r\n" + - "Content-Length: " + dataBuffer.length + "\r\n" + - "\r\n", - "utf8", // buffer encoding - ); + `EVENT/1.0 200 OK\r\n` + + `Content-Type: application/hap+json\r\n` + + `Content-Length: ${dataBuffer.length}\r\n` + + `\r\n`, + 'utf8', // buffer encoding + ) - const buffer = Buffer.concat([header, dataBuffer]); - this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this)); + const buffer = Buffer.concat([header, dataBuffer]) + this.tcpSocket.write(this.encrypt(buffer), this.handleTCPSocketWriteFulfilled.bind(this)) } public enableEventNotifications(aid: number, iid: number): void { - this.registeredEvents.add(aid + "." + iid); + this.registeredEvents.add(`${aid}.${iid}`) } public disableEventNotifications(aid: number, iid: number): void { - this.registeredEvents.delete(aid + "." + iid); + this.registeredEvents.delete(`${aid}.${iid}`) } public hasEventNotifications(aid: number, iid: number): boolean { - return this.registeredEvents.has(aid + "." + iid); + return this.registeredEvents.has(`${aid}.${iid}`) } public getRegisteredEvents(): Set { - return this.registeredEvents; + return this.registeredEvents } public clearRegisteredEvents(): void { - this.registeredEvents.clear(); + this.registeredEvents.clear() } private encrypt(data: Buffer): Buffer { @@ -640,50 +643,50 @@ export class HAPConnection extends EventEmitter { // Since all communication calls are asynchronous, we could easily receive this 'encrypt' event for those bytes. // So we want to make sure that we aren't encrypting data until we have *received* some encrypted data from the client first. if (this.encryption && this.encryption.accessoryToControllerKey.length > 0 && this.encryption.controllerToAccessoryCount > 0) { - return hapCrypto.layerEncrypt(data, this.encryption); + return layerEncrypt(data, this.encryption) } - return data; // otherwise, we don't encrypt and return plaintext + return data // otherwise, we don't encrypt and return plaintext } private decrypt(data: Buffer): Buffer { if (this.encryption && this.encryption.controllerToAccessoryKey.length > 0) { // below call may throw an error if decryption failed - return hapCrypto.layerDecrypt(data, this.encryption); + return layerDecrypt(data, this.encryption) } - return data; // otherwise, we don't decrypt and return plaintext + return data // otherwise, we don't decrypt and return plaintext } private onHttpServerListening() { - const addressInfo = this.internalHttpServer.address() as AddressInfo; // address() is only a string when listening to unix domain sockets - const addressString = addressInfo.family === "IPv6"? `[${addressInfo.address}]`: addressInfo.address; - this.internalHttpServerPort = addressInfo.port; + const addressInfo = this.internalHttpServer.address() as AddressInfo // address() is only a string when listening to unix domain sockets + const addressString = addressInfo.family === 'IPv6' ? `[${addressInfo.address}]` : addressInfo.address + this.internalHttpServerPort = addressInfo.port - debugCon("[%s] Internal HTTP server listening on %s:%s", this.remoteAddress, addressString, addressInfo.port); + debugCon('[%s] Internal HTTP server listening on %s:%s', this.remoteAddress, addressString, addressInfo.port) - this.internalHttpServer.on("close", this.onHttpServerClose.bind(this)); + this.internalHttpServer.on('close', this.onHttpServerClose.bind(this)) // now we can establish a connection to this running HTTP server for proxying data - this.httpSocket = net.createConnection(this.internalHttpServerPort, this.internalHttpServerAddress); // previously we used addressInfo.address - this.httpSocket.setNoDelay(true); // disable Nagle algorithm + this.httpSocket = createConnection(this.internalHttpServerPort, this.internalHttpServerAddress) // previously we used addressInfo.address + this.httpSocket.setNoDelay(true) // disable Nagle algorithm - this.httpSocket.on("data", this.handleHttpServerResponse.bind(this)); + this.httpSocket.on('data', this.handleHttpServerResponse.bind(this)) // we MUST register for this event, otherwise the error will bubble up to the top and crash the node process entirely. - this.httpSocket.on("error", this.onHttpSocketError.bind(this)); - this.httpSocket.on("close", this.onHttpSocketClose.bind(this)); - this.httpSocket.on("connect", () => { + this.httpSocket.on('error', this.onHttpSocketError.bind(this)) + this.httpSocket.on('close', this.onHttpSocketClose.bind(this)) + this.httpSocket.on('connect', () => { // we are now fully set up: // - clientSocket is connected to the iOS device // - serverSocket is connected to the httpServer // - ready to proxy data! - this.state = HAPConnectionState.FULLY_SET_UP; - debugCon("[%s] Internal HTTP socket connected. HAPConnection now fully set up!", this.remoteAddress); + this.state = HAPConnectionState.FULLY_SET_UP + debugCon('[%s] Internal HTTP socket connected. HAPConnection now fully set up!', this.remoteAddress) // start by flushing any pending buffered data received from the client while we were setting up if (this.pendingClientSocketData && this.pendingClientSocketData.length > 0) { - this.httpSocket!.write(this.pendingClientSocketData); + this.httpSocket!.write(this.pendingClientSocketData) } - this.pendingClientSocketData = undefined; - }); + this.pendingClientSocketData = undefined + }) } /** @@ -693,24 +696,24 @@ export class HAPConnection extends EventEmitter { private onTCPSocketData(data: Buffer): void { if (this.state > HAPConnectionState.AUTHENTICATED) { // don't accept data of a connection which is about to be closed or already closed - return; + return } - this.handlingRequest = true; // reverted to false once response was sent out - this.lastSocketOperation = new Date().getTime(); + this.handlingRequest = true // reverted to false once response was sent out + this.lastSocketOperation = new Date().getTime() try { - data = this.decrypt(data); + data = this.decrypt(data) } catch (error) { // decryption and/or verification failed, disconnect the client - debugCon("[%s] Error occurred trying to decrypt incoming packet: %s", this.remoteAddress, error.message); - this.close(); - return; + debugCon('[%s] Error occurred trying to decrypt incoming packet: %s', this.remoteAddress, error.message) + this.close() + return } if (this.state < HAPConnectionState.FULLY_SET_UP) { // we're not setup yet, so add this data to our intermediate buffer - this.pendingClientSocketData = Buffer.concat([this.pendingClientSocketData!, data]); + this.pendingClientSocketData = Buffer.concat([this.pendingClientSocketData!, data]) } else { - this.httpSocket!.write(data); // proxy it along to the HTTP server + this.httpSocket!.write(data) // proxy it along to the HTTP server } } @@ -722,14 +725,14 @@ export class HAPConnection extends EventEmitter { private handleHttpServerRequest(request: IncomingMessage, response: ServerResponse): void { if (this.state > HAPConnectionState.AUTHENTICATED) { // don't accept data of a connection which is about to be closed or already closed - return; + return } - debugCon("[%s] HTTP request: %s", this.remoteAddress, request.url); + debugCon('[%s] HTTP request: %s', this.remoteAddress, request.url) - request.socket.setNoDelay(true); + request.socket.setNoDelay(true) - this.emit(HAPConnectionEvent.REQUEST, request, response); + this.emit(HAPConnectionEvent.REQUEST, request, response) } /** @@ -738,154 +741,154 @@ export class HAPConnection extends EventEmitter { * In this method we have to encrypt and forward the message back to the HomeKit controller. */ private handleHttpServerResponse(data: Buffer): void { - data = this.encrypt(data); - this.tcpSocket.write(data, this.handleTCPSocketWriteFulfilled.bind(this)); + data = this.encrypt(data) + this.tcpSocket.write(data, this.handleTCPSocketWriteFulfilled.bind(this)) - debugCon("[%s] HTTP Response is finished", this.remoteAddress); - this.handlingRequest = false; + debugCon('[%s] HTTP Response is finished', this.remoteAddress) + this.handlingRequest = false if (this.state === HAPConnectionState.TO_BE_TEARED_DOWN) { - setTimeout(() => this.close(), 10); + setTimeout(() => this.close(), 10) } else if (this.state < HAPConnectionState.TO_BE_TEARED_DOWN) { if (!this.eventsTimer || this.eventsQueuedForImmediateDelivery) { // we deliver events if there is no eventsTimer (meaning it ran out in the meantime) // or when the queue contains events set to be delivered immediately - this.writeQueuedEventNotifications(); + this.writeQueuedEventNotifications() } } } private handleTCPSocketWriteFulfilled(): void { - this.lastSocketOperation = new Date().getTime(); + this.lastSocketOperation = new Date().getTime() } private onTCPSocketError(err: Error): void { - debugCon("[%s] Client connection error: %s", this.remoteAddress, err.message); + debugCon('[%s] Client connection error: %s', this.remoteAddress, err.message) // onTCPSocketClose will be called next } private onTCPSocketClose(): void { - this.state = HAPConnectionState.CLOSED; + this.state = HAPConnectionState.CLOSED - debugCon("[%s] Client connection closed", this.remoteAddress); + debugCon('[%s] Client connection closed', this.remoteAddress) if (this.httpSocket) { - this.httpSocket.destroy(); + this.httpSocket.destroy() } - this.internalHttpServer.close(); + this.internalHttpServer.close() - this.emit(HAPConnectionEvent.CLOSED); // sending final closed event - this.removeAllListeners(); // cleanup listeners, we are officially dead now + this.emit(HAPConnectionEvent.CLOSED) // sending final closed event + this.removeAllListeners() // cleanup listeners, we are officially dead now } private onHttpServerError(err: Error & { code?: string }): void { - debugCon("[%s] HTTP server error: %s", this.remoteAddress, err.message); - if (err.code === "EADDRINUSE") { - this.internalHttpServerPort = undefined; + debugCon('[%s] HTTP server error: %s', this.remoteAddress, err.message) + if (err.code === 'EADDRINUSE') { + this.internalHttpServerPort = undefined - this.internalHttpServer.close(); - this.internalHttpServer.listen(0, this.internalHttpServerAddress = getOSLoopbackAddressIfAvailable()); + this.internalHttpServer.close() + this.internalHttpServer.listen(0, this.internalHttpServerAddress = getOSLoopbackAddressIfAvailable()) } } private onHttpServerClose(): void { - debugCon("[%s] HTTP server was closed", this.remoteAddress); + debugCon('[%s] HTTP server was closed', this.remoteAddress) // make sure the iOS side is closed as well - this.close(); + this.close() } private onHttpSocketError(err: Error): void { - debugCon("[%s] HTTP connection error: ", this.remoteAddress, err.message); + debugCon('[%s] HTTP connection error: ', this.remoteAddress, err.message) // onHttpSocketClose will be called next } private onHttpSocketClose(): void { - debugCon("[%s] HTTP connection was closed", this.remoteAddress); + debugCon('[%s] HTTP connection was closed', this.remoteAddress) // we only support a single long-lived connection to our internal HTTP server. Since it's closed, // we'll need to shut it down entirely. - this.internalHttpServer.close(); + this.internalHttpServer.close() } - public getLocalAddress(ipVersion: "ipv4" | "ipv6"): string { - const interfaceDetails = os.networkInterfaces()[this.networkInterface]; + public getLocalAddress(ipVersion: 'ipv4' | 'ipv6'): string { + const interfaceDetails = networkInterfaces()[this.networkInterface] if (!interfaceDetails) { - throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface); + throw new Error(`Could not find ${ipVersion} address for interface ${this.networkInterface}`) } // Find our first local IPv4 address. - if (ipVersion === "ipv4") { - const ipv4Info = interfaceDetails.find(info => info.family === "IPv4"); + if (ipVersion === 'ipv4') { + const ipv4Info = interfaceDetails.find(info => info.family === 'IPv4') if (ipv4Info) { - return ipv4Info.address; + return ipv4Info.address } - throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface + "."); + throw new Error(`Could not find ${ipVersion} address for interface ${this.networkInterface}.`) } - let localUniqueAddress; + let localUniqueAddress - for (const v6entry of interfaceDetails.filter(entry => entry.family === "IPv6")) { + for (const v6entry of interfaceDetails.filter(entry => entry.family === 'IPv6')) { if (!v6entry.scopeid) { - return v6entry.address; + return v6entry.address } - localUniqueAddress ??= v6entry.address; + localUniqueAddress ??= v6entry.address } - if(localUniqueAddress) { - return localUniqueAddress; + if (localUniqueAddress) { + return localUniqueAddress } - throw new Error("Could not find " + ipVersion + " address for interface " + this.networkInterface); + throw new Error(`Could not find ${ipVersion} address for interface ${this.networkInterface}`) } private static getLocalNetworkInterface(socket: Socket): string { - - let localAddress = socket.localAddress; + let localAddress = socket.localAddress // Grab the list of network interfaces. - const interfaces = os.networkInterfaces(); + const interfaces = networkInterfaces() // Default to the first non-loopback interface we see. - const defaultInterface = () => Object.entries(interfaces).find(([, addresses]) => addresses?.some(address => !address.internal))?.[0] ?? "unknown"; + const defaultInterface = () => Object.entries(interfaces).find(([, addresses]) => addresses?.some(address => !address.internal))?.[0] ?? 'unknown' // No local address return our default. - if(!localAddress) { - return defaultInterface(); + if (!localAddress) { + return defaultInterface() } // Handle IPv4-mapped IPv6 addresses. - localAddress = localAddress.replace(/^::ffff:/i, ""); + localAddress = localAddress.replace(/^::ffff:/i, '') // Handle edge cases where we have an IPv4-mapped IPv6 address without the requisite prefix. - if(/^::(?:\d{1,3}\.){3}\d{1,3}$/.test(localAddress)) { - localAddress = localAddress.replace(/^::/, ""); + if (/^::(?:\d{1,3}\.){3}\d{1,3}$/.test(localAddress)) { + localAddress = localAddress.replace(/^::/, '') } // Handle link-local IPv6 addresses. - localAddress = localAddress.split("%")[0]; + localAddress = localAddress.split('%')[0] // Let's find an exact match using the IP. for (const [name, addresses] of Object.entries(interfaces)) { if (addresses?.some(({ address }) => address === localAddress)) { - return name; + return name } } // We couldn't find an interface to match the address from above, so we attempt to match subnets (see https://github.com/homebridge/HAP-NodeJS/issues/847). - const family = net.isIPv4(localAddress)? "IPv4": "IPv6"; + const family = isIPv4(localAddress) ? 'IPv4' : 'IPv6' // Let's find a match based on the subnet. for (const [name, addresses] of Object.entries(interfaces)) { if (addresses?.some(entry => entry.family === family && getNetAddress(localAddress, entry.netmask) === getNetAddress(entry.address, entry.netmask))) { - return name; + return name } } - console.log("WARNING: unable to determine which interface to use for socket coming from " + socket.remoteAddress + ":" + socket.remotePort + " to " + - socket.localAddress + "."); + // eslint-disable-next-line no-console + console.log(`WARNING: unable to determine which interface to use for socket coming from ${socket.remoteAddress}:${socket.remotePort} to ${ + socket.localAddress}.`) - return defaultInterface(); + return defaultInterface() } } diff --git a/src/lib/util/hapCrypto.spec.ts b/src/lib/util/hapCrypto.spec.ts index 0862b1f0e..66c2a4339 100644 --- a/src/lib/util/hapCrypto.spec.ts +++ b/src/lib/util/hapCrypto.spec.ts @@ -1,9 +1,19 @@ -import { HAPEncryption } from "./eventedhttp"; -import * as hapCrypto from "./hapCrypto"; -import crypto from "crypto"; +import { Buffer } from 'node:buffer' +import { randomBytes } from 'node:crypto' + +import { beforeEach, describe, expect, it } from 'vitest' + +import { HAPEncryption } from './eventedhttp.js' +import { + chacha20_poly1305_decryptAndVerify, + chacha20_poly1305_encryptAndSeal, + HKDF, + layerDecrypt, + layerEncrypt, +} from './hapCrypto.js' function fromHex(hex: string): Buffer { - return Buffer.from(hex.trim().replace(/ /g, ""), "hex"); + return Buffer.from(hex.trim().replace(/ /g, ''), 'hex') } /* @@ -11,148 +21,148 @@ function fromHex(hex: string): Buffer { * but rather to give some easy validation for future modifications somebody might be doing. */ -describe("hapCrypto", () => { - describe("chacha20-poly1305", () => { - const key = fromHex("09b29329 4514f14e e8437501 674e268f 7d85b193 85d38b54 8f5be259 c678fb52"); +describe('hapCrypto', () => { + describe('chacha20-poly1305', () => { + const key = fromHex('09b29329 4514f14e e8437501 674e268f 7d85b193 85d38b54 8f5be259 c678fb52') const input = fromHex( - "48545450 2f312e31 20323030 204f4b0d 0a436f6e 74656e74 2d547970 653a2061 70706c69 636174696f6e2f68 61702b6a" + - "736f6e0d 0a446174 653a2053 756e2c20 31392041 70722032 30323020 31363a32 323a3238 20474d54 0d0a436f 6e6e6563" + - "74696f6e 3a206b65 65702d61 6c697665 0d0a5472 616e7366 65722d45 6e636f64 696e673a 20636875 6e6b6564 0d0a0d0a" + - "33350d0a 7b226368 61726163 74657269 73746963 73223a5b 7b226169 64223a31 2c226969 64223a39 2c227661 6c756522" + - "3a66616c 73657d5d 7d0d0a30 0d0a0d0a", - ); - const nonce = fromHex("08000000 00000000"); - const aad = fromHex("d000"); - const tag = fromHex("8e4c1272 16827838 8a155757 d34a851a"); + '48545450 2f312e31 20323030 204f4b0d 0a436f6e 74656e74 2d547970 653a2061 70706c69 636174696f6e2f68 61702b6a' + + '736f6e0d 0a446174 653a2053 756e2c20 31392041 70722032 30323020 31363a32 323a3238 20474d54 0d0a436f 6e6e6563' + + '74696f6e 3a206b65 65702d61 6c697665 0d0a5472 616e7366 65722d45 6e636f64 696e673a 20636875 6e6b6564 0d0a0d0a' + + '33350d0a 7b226368 61726163 74657269 73746963 73223a5b 7b226169 64223a31 2c226969 64223a39 2c227661 6c756522' + + '3a66616c 73657d5d 7d0d0a30 0d0a0d0a', + ) + const nonce = fromHex('08000000 00000000') + const aad = fromHex('d000') + const tag = fromHex('8e4c1272 16827838 8a155757 d34a851a') const output = fromHex( - "6e62d95a 0811739b f21f4135 00da6a77 a5eb013f d9fe70ce 1c7a4c95 c06eee64 7ffe2dd6 ab58bce8 6d911959 4a11defa" + - "e4162a4e 9d3240ca 36956854 e6150377 b54e4ac9 f98d7607 4331ac06 54e0abaa 76006d43 aecb3638 60952e58 3708ca26" + - "959bccf9 4bb255de 31286989 faf82ec6 99cf7abe dc4c3544 210b1e35 4d323a72 ed197d1b e584a1d6 42931ced e20376d8" + - "31bb3eea 7d3b307c 432d5b10 9b30b1cf 3e0bba23 712695a6 9e6e66b9 7db4ac45 3c49c6de 2c5b3139 ca5c9fef 07bf805d" + - "8cc99047 ad94b0cf 33eac227 b1211074", - ); - - it("should encrypt chacha20-poly1305 correctly", () => { - const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(key, nonce, aad, input); - expect(encrypted.ciphertext.toString("hex")).toBe(output.toString("hex")); - expect(encrypted.authTag.toString("hex")).toBe(tag.toString("hex")); - }); - - it("should decrypt chacha20-poly1305 correctly", () => { - const plaintext = hapCrypto.chacha20_poly1305_decryptAndVerify(key, nonce, aad, output, tag); - expect(plaintext.toString("hex")).toBe(input.toString("hex")); - }); - - it("should fail verification on manipulated data", () => { - const manipulated = Buffer.alloc(output.length); - output.copy(manipulated); - manipulated[3] = 5; - - expect(() => hapCrypto.chacha20_poly1305_decryptAndVerify(key, nonce, aad, manipulated, tag)).toThrow(); - }); - }); - - describe("HKDF", () => { - it("should calculate test vector correctly", () => { - const ikm = fromHex("0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b"); - const salt = fromHex("00010203 04050607 08090a0b 0c"); - const info = Buffer.alloc(0); - - const result = fromHex("f81b8748 1a18b664 936daeb2 22f58cba 0ebc55f5 c85996b9 f1cb396c 327b70bb"); - - const hkdf = hapCrypto.HKDF("sha512", salt, ikm, info, 32); - expect(hkdf.toString("hex")).toBe(result.toString("hex")); - }); - }); - - describe("layer encryption", () => { - let serverEncryption: HAPEncryption; - let clientEncryption: HAPEncryption; - - const message0 = Buffer.from("Hello World"); - const message1 = Buffer.from("Hello Mars"); - const message2 = Buffer.from("Foo"); - const message3 = Buffer.from("Bar"); + '6e62d95a 0811739b f21f4135 00da6a77 a5eb013f d9fe70ce 1c7a4c95 c06eee64 7ffe2dd6 ab58bce8 6d911959 4a11defa' + + 'e4162a4e 9d3240ca 36956854 e6150377 b54e4ac9 f98d7607 4331ac06 54e0abaa 76006d43 aecb3638 60952e58 3708ca26' + + '959bccf9 4bb255de 31286989 faf82ec6 99cf7abe dc4c3544 210b1e35 4d323a72 ed197d1b e584a1d6 42931ced e20376d8' + + '31bb3eea 7d3b307c 432d5b10 9b30b1cf 3e0bba23 712695a6 9e6e66b9 7db4ac45 3c49c6de 2c5b3139 ca5c9fef 07bf805d' + + '8cc99047 ad94b0cf 33eac227 b1211074', + ) + + it('should encrypt chacha20-poly1305 correctly', () => { + const encrypted = chacha20_poly1305_encryptAndSeal(key, nonce, aad, input) + expect(encrypted.ciphertext.toString('hex')).toBe(output.toString('hex')) + expect(encrypted.authTag.toString('hex')).toBe(tag.toString('hex')) + }) + + it('should decrypt chacha20-poly1305 correctly', () => { + const plaintext = chacha20_poly1305_decryptAndVerify(key, nonce, aad, output, tag) + expect(plaintext.toString('hex')).toBe(input.toString('hex')) + }) + + it('should fail verification on manipulated data', () => { + const manipulated = Buffer.alloc(output.length) + output.copy(manipulated) + manipulated[3] = 5 + + expect(() => chacha20_poly1305_decryptAndVerify(key, nonce, aad, manipulated, tag)).toThrow() + }) + }) + + describe('hKDF', () => { + it('should calculate test vector correctly', () => { + const ikm = fromHex('0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b0b0b 0b0b') + const salt = fromHex('00010203 04050607 08090a0b 0c') + const info = Buffer.alloc(0) + + const result = fromHex('f81b8748 1a18b664 936daeb2 22f58cba 0ebc55f5 c85996b9 f1cb396c 327b70bb') + + const hkdf = HKDF('sha512', salt, ikm, info, 32) + expect(hkdf.toString('hex')).toBe(result.toString('hex')) + }) + }) + + describe('layer encryption', () => { + let serverEncryption: HAPEncryption + let clientEncryption: HAPEncryption + + const message0 = Buffer.from('Hello World') + const message1 = Buffer.from('Hello Mars') + const message2 = Buffer.from('Foo') + const message3 = Buffer.from('Bar') beforeEach(() => { - const salt = Buffer.from("Test-Salt"); - const readInfo = Buffer.from("Control-Read-Encryption-Key"); - const writeInfo = Buffer.from("Control-Write-Encryption-Key"); + const salt = Buffer.from('Test-Salt') + const readInfo = Buffer.from('Control-Read-Encryption-Key') + const writeInfo = Buffer.from('Control-Write-Encryption-Key') - const sharedSecret = crypto.randomBytes(32); + const sharedSecret = randomBytes(32) - const accessoryToControllerKey = hapCrypto.HKDF("sha512", salt, sharedSecret, readInfo, 32); - const controllerToAccessoryKey = hapCrypto.HKDF("sha512", salt, sharedSecret, writeInfo, 32); + const accessoryToControllerKey = HKDF('sha512', salt, sharedSecret, readInfo, 32) + const controllerToAccessoryKey = HKDF('sha512', salt, sharedSecret, writeInfo, 32) - const empty = Buffer.alloc(0); - serverEncryption = new HAPEncryption(empty, empty, empty, sharedSecret, empty); - clientEncryption = new HAPEncryption(empty, empty, empty, sharedSecret, empty); + const empty = Buffer.alloc(0) + serverEncryption = new HAPEncryption(empty, empty, empty, sharedSecret, empty) + clientEncryption = new HAPEncryption(empty, empty, empty, sharedSecret, empty) - serverEncryption.accessoryToControllerKey = accessoryToControllerKey; - serverEncryption.controllerToAccessoryKey = controllerToAccessoryKey; + serverEncryption.accessoryToControllerKey = accessoryToControllerKey + serverEncryption.controllerToAccessoryKey = controllerToAccessoryKey - clientEncryption.accessoryToControllerKey = controllerToAccessoryKey; - clientEncryption.controllerToAccessoryKey = accessoryToControllerKey; - }); + clientEncryption.accessoryToControllerKey = controllerToAccessoryKey + clientEncryption.controllerToAccessoryKey = accessoryToControllerKey + }) - test("simple encryption and decryption", () => { - const encrypted = hapCrypto.layerEncrypt(message0, serverEncryption); + it('simple encryption and decryption', () => { + const encrypted = layerEncrypt(message0, serverEncryption) - const decrypted = hapCrypto.layerDecrypt(encrypted, clientEncryption); - expect(decrypted).toEqual(message0); - }); + const decrypted = layerDecrypt(encrypted, clientEncryption) + expect(decrypted).toEqual(message0) + }) - test("multiple frames encryption and decryption", () => { - const encrypted0 = hapCrypto.layerEncrypt(message0, serverEncryption); - const encrypted1 = hapCrypto.layerEncrypt(message1, serverEncryption); - const encrypted2 = hapCrypto.layerEncrypt(message2, serverEncryption); + it('multiple frames encryption and decryption', () => { + const encrypted0 = layerEncrypt(message0, serverEncryption) + const encrypted1 = layerEncrypt(message1, serverEncryption) + const encrypted2 = layerEncrypt(message2, serverEncryption) - const decrypted = hapCrypto.layerDecrypt(Buffer.concat([encrypted0, encrypted1, encrypted2]), clientEncryption); - expect(decrypted).toEqual(Buffer.concat([message0, message1, message2])); - }); + const decrypted = layerDecrypt(Buffer.concat([encrypted0, encrypted1, encrypted2]), clientEncryption) + expect(decrypted).toEqual(Buffer.concat([message0, message1, message2])) + }) - test("multiple frames encryption and decryption intertwined", () => { - const S_encrypted0 = hapCrypto.layerEncrypt(message0, serverEncryption); - const S_encrypted1 = hapCrypto.layerEncrypt(message1, serverEncryption); - const S_encrypted2 = hapCrypto.layerEncrypt(message2, serverEncryption); + it('multiple frames encryption and decryption intertwined', () => { + const S_encrypted0 = layerEncrypt(message0, serverEncryption) + const S_encrypted1 = layerEncrypt(message1, serverEncryption) + const S_encrypted2 = layerEncrypt(message2, serverEncryption) - const C_encrypted0 = hapCrypto.layerEncrypt(message1, clientEncryption); - const C_encrypted1 = hapCrypto.layerEncrypt(message3, clientEncryption); + const C_encrypted0 = layerEncrypt(message1, clientEncryption) + const C_encrypted1 = layerEncrypt(message3, clientEncryption) - const S_decrypted = hapCrypto.layerDecrypt(Buffer.concat([S_encrypted0, S_encrypted1, S_encrypted2]), clientEncryption); - expect(S_decrypted).toEqual(Buffer.concat([message0, message1, message2])); + const S_decrypted = layerDecrypt(Buffer.concat([S_encrypted0, S_encrypted1, S_encrypted2]), clientEncryption) + expect(S_decrypted).toEqual(Buffer.concat([message0, message1, message2])) - const C_decrypted = hapCrypto.layerDecrypt(Buffer.concat([C_encrypted0, C_encrypted1]), serverEncryption); - expect(C_decrypted).toEqual(Buffer.concat([message1, message3])); - }); + const C_decrypted = layerDecrypt(Buffer.concat([C_encrypted0, C_encrypted1]), serverEncryption) + expect(C_decrypted).toEqual(Buffer.concat([message1, message3])) + }) - test("incomplete frames decryption; first", () => { - const S_encrypted0 = hapCrypto.layerEncrypt(message0, serverEncryption); - const S_encrypted1 = hapCrypto.layerEncrypt(message1, serverEncryption); - const S_encrypted2 = hapCrypto.layerEncrypt(message2, serverEncryption); + it('incomplete frames decryption; first', () => { + const S_encrypted0 = layerEncrypt(message0, serverEncryption) + const S_encrypted1 = layerEncrypt(message1, serverEncryption) + const S_encrypted2 = layerEncrypt(message2, serverEncryption) - const C_encrypted0 = hapCrypto.layerEncrypt(message2, clientEncryption); - const C_encrypted1 = hapCrypto.layerEncrypt(message3, clientEncryption); + const C_encrypted0 = layerEncrypt(message2, clientEncryption) + const C_encrypted1 = layerEncrypt(message3, clientEncryption) - expect(hapCrypto.layerDecrypt(C_encrypted0.slice(0, 5), serverEncryption)).toEqual(Buffer.alloc(0)); + expect(layerDecrypt(C_encrypted0.subarray(0, 5), serverEncryption)).toEqual(Buffer.alloc(0)) - const S_decrypted0 = hapCrypto.layerDecrypt( - Buffer.concat([S_encrypted0, S_encrypted1.slice(0, 5)]), + const S_decrypted0 = layerDecrypt( + Buffer.concat([S_encrypted0, S_encrypted1.subarray(0, 5)]), clientEncryption, - ); - expect(S_decrypted0).toEqual(message0); + ) + expect(S_decrypted0).toEqual(message0) - const C_decrypted = hapCrypto.layerDecrypt( - Buffer.concat([C_encrypted0.slice(5), C_encrypted1]), + const C_decrypted = layerDecrypt( + Buffer.concat([C_encrypted0.subarray(5), C_encrypted1]), serverEncryption, - ); - expect(C_decrypted).toEqual(Buffer.concat([message2, message3])); + ) + expect(C_decrypted).toEqual(Buffer.concat([message2, message3])) - const S_decrypted1 = hapCrypto.layerDecrypt( - Buffer.concat([S_encrypted1.slice(5), S_encrypted2]), + const S_decrypted1 = layerDecrypt( + Buffer.concat([S_encrypted1.subarray(5), S_encrypted2]), clientEncryption, - ); - expect(S_decrypted1).toEqual(Buffer.concat([message1, message2])); - }); - }); -}); + ) + expect(S_decrypted1).toEqual(Buffer.concat([message1, message2])) + }) + }) +}) diff --git a/src/lib/util/hapCrypto.ts b/src/lib/util/hapCrypto.ts index e3b517a2b..566143795 100644 --- a/src/lib/util/hapCrypto.ts +++ b/src/lib/util/hapCrypto.ts @@ -1,61 +1,66 @@ -import assert from "assert"; -import crypto from "crypto"; -import hkdf from "futoin-hkdf"; -import tweetnacl, { BoxKeyPair } from "tweetnacl"; -import { HAPEncryption } from "./eventedhttp"; - -if (!crypto.getCiphers().includes("chacha20-poly1305")) { - assert.fail("The cipher 'chacha20-poly1305' is not supported with your current running nodejs version v" + process.version + ". " + - "At least a nodejs version of v10.17.0 (excluding v11.0 and v11.1) is required!"); +import type { BoxKeyPair } from 'tweetnacl' + +import type { HAPEncryption } from './eventedhttp' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import { createCipheriv, createDecipheriv, getCiphers } from 'node:crypto' +import process from 'node:process' + +import hkdf from 'futoin-hkdf' +import tweetnacl from 'tweetnacl' + +if (!getCiphers().includes('chacha20-poly1305')) { + assert.fail(`The cipher 'chacha20-poly1305' is not supported with your current running nodejs version v${process.version}. ` + + `At least a nodejs version of v10.17.0 (excluding v11.0 and v11.1) is required!`) } /** * @group Cryptography */ export function generateCurve25519KeyPair(): BoxKeyPair { - return tweetnacl.box.keyPair(); + return tweetnacl.box.keyPair() } /** * @group Cryptography */ export function generateCurve25519SharedSecKey(priKey: Uint8Array, pubKey: Uint8Array): Uint8Array { - return tweetnacl.scalarMult(priKey, pubKey); + return tweetnacl.scalarMult(priKey, pubKey) } /** * @group Cryptography */ export function HKDF(hashAlg: string, salt: Buffer, ikm: Buffer, info: Buffer, size: number): Buffer { - return hkdf(ikm, size, { hash: hashAlg, salt: salt, info: info }); + return hkdf(ikm, size, { hash: hashAlg, salt, info }) } - -const MAX_UINT32 = 0x00000000FFFFFFFF; -const MAX_INT53 = 0x001FFFFFFFFFFFFF; +const MAX_UINT32 = 0x00000000FFFFFFFF +const MAX_INT53 = 0x001FFFFFFFFFFFFF function uintHighLow(number: number) { - assert(number > -1 && number <= MAX_INT53, "number out of range"); - assert(Math.floor(number) === number, "number must be an integer"); - let high = 0; - const signbit = number & 0xFFFFFFFF; - const low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit; + assert(number > -1 && number <= MAX_INT53, 'number out of range') + assert(Math.floor(number) === number, 'number must be an integer') + let high = 0 + const signbit = number & 0xFFFFFFFF + const low = signbit < 0 ? (number & 0x7FFFFFFF) + 0x80000000 : signbit if (number > MAX_UINT32) { - high = (number - low) / (MAX_UINT32 + 1); + high = (number - low) / (MAX_UINT32 + 1) } - return [high, low]; + return [high, low] } /** * @group Utils */ -export function writeUInt64LE (number: number, buffer: Buffer, offset = 0): void { - const hl = uintHighLow(number); - buffer.writeUInt32LE(hl[1], offset); - buffer.writeUInt32LE(hl[0], offset + 4); +export function writeUInt64LE(number: number, buffer: Buffer, offset = 0): void { + const hl = uintHighLow(number) + buffer.writeUInt32LE(hl[1], offset) + buffer.writeUInt32LE(hl[0], offset + 4) } -//Security Layer Enc/Dec +// Security Layer Enc/Dec /** * @group Cryptography @@ -65,26 +70,26 @@ export function chacha20_poly1305_decryptAndVerify(key: Buffer, nonce: Buffer, a nonce = Buffer.concat([ Buffer.alloc(12 - nonce.length, 0), nonce, - ]); + ]) } - const decipher = crypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 }); + const decipher = createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 }) if (aad) { - decipher.setAAD(aad, { plaintextLength: ciphertext.length }); + decipher.setAAD(aad, { plaintextLength: ciphertext.length }) } - decipher.setAuthTag(authTag); - const plaintext = decipher.update(ciphertext); - decipher.final(); // final call verifies integrity using the auth tag. Throws error if something was manipulated! + decipher.setAuthTag(authTag) + const plaintext = decipher.update(ciphertext) + decipher.final() // final call verifies integrity using the auth tag. Throws error if something was manipulated! - return plaintext; + return plaintext } /** * @group Cryptography */ export interface EncryptedData { - ciphertext: Buffer; - authTag: Buffer; + ciphertext: Buffer + authTag: Buffer } /** @@ -95,45 +100,45 @@ export function chacha20_poly1305_encryptAndSeal(key: Buffer, nonce: Buffer, aad nonce = Buffer.concat([ Buffer.alloc(12 - nonce.length, 0), nonce, - ]); + ]) } - const cipher = crypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 }); + const cipher = createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 }) if (aad) { - cipher.setAAD(aad, { plaintextLength: plaintext.length }); + cipher.setAAD(aad, { plaintextLength: plaintext.length }) } - const ciphertext = cipher.update(plaintext); - cipher.final(); // final call creates the auth tag - const authTag = cipher.getAuthTag(); + const ciphertext = cipher.update(plaintext) + cipher.final() // final call creates the auth tag + const authTag = cipher.getAuthTag() return { // return type is a bit weird, but we are going to change that on a later code cleanup - ciphertext: ciphertext, - authTag: authTag, - }; + ciphertext, + authTag, + } } /** * @group Cryptography */ export function layerEncrypt(data: Buffer, encryption: HAPEncryption): Buffer { - let result = Buffer.alloc(0); - const total = data.length; - for (let offset = 0; offset < total; ) { - const length = Math.min(total - offset, 0x400); - const leLength = Buffer.alloc(2); - leLength.writeUInt16LE(length,0); + let result = Buffer.alloc(0) + const total = data.length + for (let offset = 0; offset < total;) { + const length = Math.min(total - offset, 0x400) + const leLength = Buffer.alloc(2) + leLength.writeUInt16LE(length, 0) - const nonce = Buffer.alloc(8); - writeUInt64LE(encryption.accessoryToControllerCount++, nonce, 0); + const nonce = Buffer.alloc(8) + writeUInt64LE(encryption.accessoryToControllerCount++, nonce, 0) - const encrypted = chacha20_poly1305_encryptAndSeal(encryption.accessoryToControllerKey, nonce, leLength, data.slice(offset, offset + length)); - offset += length; + const encrypted = chacha20_poly1305_encryptAndSeal(encryption.accessoryToControllerKey, nonce, leLength, data.subarray(offset, offset + length)) + offset += length - result = Buffer.concat([result,leLength,encrypted.ciphertext,encrypted.authTag]); + result = Buffer.concat([result, leLength, encrypted.ciphertext, encrypted.authTag]) } - return result; + return result } /** @@ -141,35 +146,35 @@ export function layerEncrypt(data: Buffer, encryption: HAPEncryption): Buffer { */ export function layerDecrypt(packet: Buffer, encryption: HAPEncryption): Buffer { if (encryption.incompleteFrame) { - packet = Buffer.concat([encryption.incompleteFrame, packet]); - encryption.incompleteFrame = undefined; + packet = Buffer.concat([encryption.incompleteFrame, packet]) + encryption.incompleteFrame = undefined } - let result = Buffer.alloc(0); - const total = packet.length; + let result = Buffer.alloc(0) + const total = packet.length for (let offset = 0; offset < total;) { - const realDataLength = packet.slice(offset, offset + 2).readUInt16LE(0); + const realDataLength = packet.subarray(offset, offset + 2).readUInt16LE(0) - const availableDataLength = total - offset - 2 - 16; + const availableDataLength = total - offset - 2 - 16 if (realDataLength > availableDataLength) { // Fragmented packet - encryption.incompleteFrame = packet.slice(offset); - break; + encryption.incompleteFrame = packet.subarray(offset) + break } - const nonce = Buffer.alloc(8); - writeUInt64LE(encryption.controllerToAccessoryCount++, nonce, 0); + const nonce = Buffer.alloc(8) + writeUInt64LE(encryption.controllerToAccessoryCount++, nonce, 0) const plaintext = chacha20_poly1305_decryptAndVerify( encryption.controllerToAccessoryKey, nonce, - packet.slice(offset,offset+2), - packet.slice(offset + 2, offset + 2 + realDataLength), - packet.slice(offset + 2 + realDataLength, offset + 2 + realDataLength + 16), - ); - result = Buffer.concat([result, plaintext]); - offset += (18 + realDataLength); + packet.subarray(offset, offset + 2), + packet.subarray(offset + 2, offset + 2 + realDataLength), + packet.subarray(offset + 2 + realDataLength, offset + 2 + realDataLength + 16), + ) + result = Buffer.concat([result, plaintext]) + offset += (18 + realDataLength) } - return result; + return result } diff --git a/src/lib/util/hapStatusError.spec.ts b/src/lib/util/hapStatusError.spec.ts index de3a16d0d..ffe73f438 100644 --- a/src/lib/util/hapStatusError.spec.ts +++ b/src/lib/util/hapStatusError.spec.ts @@ -1,18 +1,20 @@ -import { HAPStatus } from "../HAPServer"; -import { HapStatusError } from "./hapStatusError"; +import { beforeEach, describe, expect, it, vi } from 'vitest' -describe("HapStatusError", () => { +import { HAPStatus } from '../HAPServer.js' +import { HapStatusError } from './hapStatusError.js' + +describe('hapStatusError', () => { beforeEach(() => { - jest.resetAllMocks(); - }); + vi.resetAllMocks() + }) - it("sets the hap status code correctly", async () => { - const error = new HapStatusError(HAPStatus.RESOURCE_BUSY); - expect(error.hapStatus).toEqual(HAPStatus.RESOURCE_BUSY); - }); + it('sets the hap status code correctly', async () => { + const error = new HapStatusError(HAPStatus.RESOURCE_BUSY) + expect(error.hapStatus).toEqual(HAPStatus.RESOURCE_BUSY) + }) - it("reverts to SERVICE_COMMUNICATION_FAILURE if an invalid code is passed in", async () => { - const error = new HapStatusError(234523323423423 as HAPStatus); - expect(error.hapStatus).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - }); -}); + it('reverts to SERVICE_COMMUNICATION_FAILURE if an invalid code is passed in', async () => { + const error = new HapStatusError(234523323423423 as HAPStatus) + expect(error.hapStatus).toEqual(HAPStatus.SERVICE_COMMUNICATION_FAILURE) + }) +}) diff --git a/src/lib/util/hapStatusError.ts b/src/lib/util/hapStatusError.ts index 918457d14..4afc55381 100644 --- a/src/lib/util/hapStatusError.ts +++ b/src/lib/util/hapStatusError.ts @@ -1,4 +1,4 @@ -import { HAPStatus, IsKnownHAPStatusError } from "../HAPServer"; +import { HAPStatus, isKnownHAPStatusError } from '../HAPServer.js' /** * Throws a HAP status error that is sent back to HomeKit. @@ -11,17 +11,17 @@ import { HAPStatus, IsKnownHAPStatusError } from "../HAPServer"; * @group Utils */ export class HapStatusError extends Error { - public hapStatus: HAPStatus; + public hapStatus: HAPStatus constructor(status: HAPStatus) { - super("HAP Status Error: " + status); + super(`HAP Status Error: ${status}`) - Object.setPrototypeOf(this, HapStatusError.prototype); + Object.setPrototypeOf(this, HapStatusError.prototype) - if (IsKnownHAPStatusError(status)) { - this.hapStatus = status; + if (isKnownHAPStatusError(status)) { + this.hapStatus = status } else { - this.hapStatus = HAPStatus.SERVICE_COMMUNICATION_FAILURE; + this.hapStatus = HAPStatus.SERVICE_COMMUNICATION_FAILURE } } } diff --git a/src/lib/util/net-utils.spec.ts b/src/lib/util/net-utils.spec.ts index 910687731..5069e0c2a 100644 --- a/src/lib/util/net-utils.spec.ts +++ b/src/lib/util/net-utils.spec.ts @@ -1,126 +1,134 @@ -import { findLoopbackAddress } from "./net-utils"; -import os from "os"; +import type { Mock } from 'vitest' -const mock = jest.spyOn(os, "networkInterfaces"); +import * as os from 'node:os' -describe("net-utils", () => { +import { describe, expect, it, vi } from 'vitest' + +import { findLoopbackAddress } from './net-utils.js' + +// Mock the os module +vi.mock('node:os', () => ({ + networkInterfaces: vi.fn(), +})) + +describe('net-utils', () => { describe(findLoopbackAddress, () => { - it("should find ipv4 only loopback address", () => { - mock.mockImplementationOnce(() => ({ - "lo": [{ - address: "127.0.0.1", - netmask: "255.0.0.0", - family: "IPv4", - mac: "00:00:00:00:00:00", + it('should find ipv4 only loopback address', () => { + (os.networkInterfaces as Mock).mockImplementationOnce(() => ({ + lo: [{ + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', internal: true, - cidr: "127.0.0.1/8", + cidr: '127.0.0.1/8', }], - "eth0": [{ - address: "192.168.0.3", - netmask: "255.255.255.0", - family: "IPv4", - mac: "00:00:03:04:00:02", + eth0: [{ + address: '192.168.0.3', + netmask: '255.255.255.0', + family: 'IPv4', + mac: '00:00:03:04:00:02', internal: false, - cidr: "192.168.0.3/24", + cidr: '192.168.0.3/24', }], - })); + })) - expect(findLoopbackAddress()).toBe("127.0.0.1"); - }); + expect(findLoopbackAddress()).toBe('127.0.0.1') + }) - it("should properly format ipv6 link local loopback address", () => { - mock.mockImplementationOnce(() => ({ - "lo": [ + it('should properly format ipv6 link local loopback address', () => { + (os.networkInterfaces as Mock).mockImplementationOnce(() => ({ + lo: [ { - address: "fe80::1", - netmask: "ffff:ffff:ffff:ffff::", - family: "IPv6", - mac: "00:00:00:00:00:00", + address: 'fe80::1', + netmask: 'ffff:ffff:ffff:ffff::', + family: 'IPv6', + mac: '00:00:00:00:00:00', internal: true, - cidr: "fe80::1/64", + cidr: 'fe80::1/64', scopeid: 1, }, ], - })); + })) - expect(findLoopbackAddress()).toBe("fe80::1%lo"); - }); + expect(findLoopbackAddress()).toBe('fe80::1%lo') + }) - it("should prioritize ipv6 loopback address", () => { - mock.mockImplementationOnce(() => ({ - "lo": [ + it('should prioritize ipv6 loopback address', () => { + (os.networkInterfaces as Mock).mockImplementationOnce(() => ({ + lo: [ { - address: "::1", - netmask: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - family: "IPv6", - mac: "00:00:00:00:00:00", + address: '::1', + netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + family: 'IPv6', + mac: '00:00:00:00:00:00', internal: true, - cidr: "::1/128", + cidr: '::1/128', scopeid: 0, }, { - address: "fe80::1", - netmask: "ffff:ffff:ffff:ffff::", - family: "IPv6", - mac: "00:00:00:00:00:00", + address: 'fe80::1', + netmask: 'ffff:ffff:ffff:ffff::', + family: 'IPv6', + mac: '00:00:00:00:00:00', internal: true, - cidr: "fe80::1/64", + cidr: 'fe80::1/64', scopeid: 1, }, ], - })); + })) - expect(findLoopbackAddress()).toBe("::1"); - }); + expect(findLoopbackAddress()).toBe('::1') + }) - it("should prioritize ipv4 loopback address", () => { - mock.mockImplementationOnce(() => ({ - "lo": [ + it('should prioritize ipv4 loopback address', () => { + (os.networkInterfaces as Mock).mockImplementationOnce(() => ({ + lo: [ { - address: "127.0.0.1", - netmask: "255.0.0.0", - family: "IPv4", - mac: "00:00:00:00:00:00", + address: '127.0.0.1', + netmask: '255.0.0.0', + family: 'IPv4', + mac: '00:00:00:00:00:00', internal: true, - cidr: "127.0.0.1/8", + cidr: '127.0.0.1/8', }, { - address: "fe80::1", - netmask: "ffff:ffff:ffff:ffff::", - family: "IPv6", - mac: "00:00:00:00:00:00", + address: 'fe80::1', + netmask: 'ffff:ffff:ffff:ffff::', + family: 'IPv6', + mac: '00:00:00:00:00:00', internal: true, - cidr: "fe80::1/64", + cidr: 'fe80::1/64', scopeid: 1, }, { - address: "::1", - netmask: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - family: "IPv6", - mac: "00:00:00:00:00:00", + address: '::1', + netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + family: 'IPv6', + mac: '00:00:00:00:00:00', internal: true, - cidr: "::1/128", + cidr: '::1/128', scopeid: 0, }, ], - })); + })) - expect(findLoopbackAddress()).toBe("127.0.0.1"); - }); + expect(findLoopbackAddress()).toBe('127.0.0.1') + }) - it("should throw an error if it can't find one", () => { - mock.mockImplementationOnce(() => ({ - "eth0": [{ - address: "192.168.0.3", - netmask: "255.255.255.0", - family: "IPv4", - mac: "00:00:03:04:00:02", + it('should throw an error if it can\'t find one', () => { + (os.networkInterfaces as Mock).mockImplementationOnce(() => ({ + eth0: [{ + address: '192.168.0.3', + netmask: '255.255.255.0', + family: 'IPv4', + mac: '00:00:03:04:00:02', internal: false, - cidr: "192.168.0.3/24", + cidr: '192.168.0.3/24', }], - })); + })) - expect(() => findLoopbackAddress()).toThrowError(); - }); - }); -}); + expect(() => findLoopbackAddress()).toThrowError() + }) + }) +}) diff --git a/src/lib/util/net-utils.ts b/src/lib/util/net-utils.ts index 81ba2eb89..0e3e708b3 100644 --- a/src/lib/util/net-utils.ts +++ b/src/lib/util/net-utils.ts @@ -1,63 +1,63 @@ -import os from "os"; +import { networkInterfaces } from 'node:os' /** * @group Utils */ export function findLoopbackAddress(): string { - let ipv6: string | undefined = undefined; // ::1/128 - let ipv6LinkLocal: string | undefined = undefined; // fe80::/10 - let ipv4: string | undefined = undefined; // 127.0.0.1/8 + let ipv6: string | undefined // ::1/128 + let ipv6LinkLocal: string | undefined // fe80::/10 + let ipv4: string | undefined // 127.0.0.1/8 - for (const [name, infos] of Object.entries(os.networkInterfaces())) { - let internal = false; + for (const [name, infos] of Object.entries(networkInterfaces())) { + let internal = false if (infos) { for (const info of infos) { if (!info.internal) { - continue; + continue } - internal = true; - // @ts-expect-error Nodejs 18+ uses the number 4 the string "IPv4" - if (info.family === "IPv4" || info.family === 4) { + internal = true + // @ts-expect-error Node.js 18+ uses the number 4 the string "IPv4" + if (info.family === 'IPv4' || info.family === 4) { if (!ipv4) { - ipv4 = info.address; + ipv4 = info.address } - // @ts-expect-error Nodejs 18+ uses the number 6 the string "IPv6" - } else if (info.family === "IPv6" || info.family === 6) { + // @ts-expect-error Node.js 18+ uses the number 6 the string "IPv6" + } else if (info.family === 'IPv6' || info.family === 6) { if (info.scopeid) { if (!ipv6LinkLocal) { - ipv6LinkLocal = info.address + "%" + name; // ipv6 link local addresses are only valid with a scope + ipv6LinkLocal = `${info.address}%${name}` // ipv6 link local addresses are only valid with a scope } } else if (!ipv6) { - ipv6 = info.address; + ipv6 = info.address } } } } if (internal) { - break; + break } } - const address = ipv4 || ipv6 || ipv6LinkLocal; + const address = ipv4 || ipv6 || ipv6LinkLocal if (!address) { - throw new Error("Could not find a valid loopback address on the platform!"); + throw new Error('Could not find a valid loopback address on the platform!') } - return address; + return address } -let loopbackAddress: string | undefined = undefined; // loopback addressed used for the internal http server (::1 or 127.0.0.1) +let loopbackAddress: string | undefined // loopback addressed used for the internal http server (::1 or 127.0.0.1) /** * Returns the loopback address for the machine. * Uses IPV4 loopback address by default and falls back to global unique IPv6 loopback and then * link local IPv6 loopback address. - * If no loopback interface could be found a error is thrown. + * If no loopback interface could be found an error is thrown. * * @group Utils */ export function getOSLoopbackAddress(): string { - return loopbackAddress ?? (loopbackAddress = findLoopbackAddress()); + return loopbackAddress ?? (loopbackAddress = findLoopbackAddress()) } /** @@ -68,9 +68,9 @@ export function getOSLoopbackAddress(): string { */ export function getOSLoopbackAddressIfAvailable(): string | undefined { try { - return loopbackAddress ?? (loopbackAddress = findLoopbackAddress()); + return loopbackAddress ?? (loopbackAddress = findLoopbackAddress()) } catch (error) { - console.log(error.stack); - return undefined; + console.log(error.stack) // eslint-disable-line no-console + return undefined } } diff --git a/src/lib/util/once.spec.ts b/src/lib/util/once.spec.ts index 22801572f..7579bd729 100644 --- a/src/lib/util/once.spec.ts +++ b/src/lib/util/once.spec.ts @@ -1,22 +1,24 @@ -import { once } from "./once"; +import { describe, expect, it, vi } from 'vitest' -describe("#once()", () => { - it("should call a function once", () => { - const spy = jest.fn(); - const callback = once(spy); - callback(); +import { once } from './once.js' - expect(spy).toHaveBeenCalledTimes(1); - }); +describe('#once()', () => { + it('should call a function once', () => { + const spy = vi.fn() + const callback = once(spy) + callback() - it("should throw an error if a function is called more than once", () => { - const spy = jest.fn(); - const callback = once(spy); - callback(); + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should throw an error if a function is called more than once', () => { + const spy = vi.fn() + const callback = once(spy) + callback() expect(() => { - callback(); - }).toThrow(" already been called"); - expect(spy).toHaveBeenCalledTimes(1); - }); -}); + callback() + }).toThrow(' already been called') + expect(spy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/lib/util/once.ts b/src/lib/util/once.ts index bc264587d..78c187bae 100644 --- a/src/lib/util/once.ts +++ b/src/lib/util/once.ts @@ -3,16 +3,15 @@ * * @group Utils */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export function once(func: T): T { - let called = false; +export function once(func: T): T { // eslint-disable-line ts/no-unsafe-function-type + let called = false return ((...args: unknown[]) => { if (called) { - throw new Error("This callback function has already been called by someone else; it can only be called one time."); + throw new Error('This callback function has already been called by someone else; it can only be called one time.') } else { - called = true; - return func(...args); + called = true + return func(...args) } - }) as unknown as T; + }) as unknown as T } diff --git a/src/lib/util/promise-utils.ts b/src/lib/util/promise-utils.ts index cc2c6a6aa..e0c268d1a 100644 --- a/src/lib/util/promise-utils.ts +++ b/src/lib/util/promise-utils.ts @@ -1,40 +1,38 @@ -import { EventEmitter } from "events"; +/* global NodeJS */ +import type { EventEmitter } from 'node:events' /** * @group Utils */ export function PromiseTimeout(timeout: number): Promise { - return new Promise(resolve => { - setTimeout(() => resolve(), timeout); - }); + return new Promise((resolve) => { + setTimeout(() => resolve(), timeout) + }) } /** * @group Utils */ -export function awaitEventOnce(element: Obj, event: Event, timeout?: number): Promise; +export function awaitEventOnce(element: Obj, event: Event, timeout?: number): Promise /** * @group Utils */ -export function awaitEventOnce(element: Obj, event: Event, timeout?: number): Promise; +export function awaitEventOnce(element: Obj, event: Event, timeout?: number): Promise export function awaitEventOnce(element: Object, event: Event, timeout = 5000): Promise { return new Promise((resolve, reject) => { - // eslint-disable-next-line prefer-const - let timeoutId: NodeJS.Timeout; + let timeoutId: NodeJS.Timeout - // eslint-disable-next-line @typescript-eslint/no-explicit-any const resolveListener = (...args: any) => { - clearTimeout(timeoutId); + clearTimeout(timeoutId) - resolve(args.length ? (args.length === 1 ? args[0] : args) : undefined); - }; + resolve(args.length ? (args.length === 1 ? args[0] : args) : undefined) + } timeoutId = setTimeout(() => { - element.removeListener(event, resolveListener); - reject(new Error(`awaitEvent for event ${event} timed out!`)); - }, timeout); + element.removeListener(event, resolveListener) + reject(new Error(`awaitEvent for event ${event} timed out!`)) + }, timeout) - element.once(event, resolveListener); - }); + element.once(event, resolveListener) + }) } - diff --git a/src/lib/util/request-util.spec.ts b/src/lib/util/request-util.spec.ts index e51b87b28..c27ea1891 100644 --- a/src/lib/util/request-util.spec.ts +++ b/src/lib/util/request-util.spec.ts @@ -1,75 +1,79 @@ -import { CharacteristicProps, Formats, Perms } from "../Characteristic"; -import { formatOutgoingCharacteristicValue } from "./request-util"; +import type { CharacteristicProps } from '../Characteristic' + +import { describe, expect, it } from 'vitest' + +import { Formats, Perms } from '../Characteristic.js' +import { formatOutgoingCharacteristicValue } from './request-util.js' function createProps(format: Formats, props?: Partial): CharacteristicProps { return { - format: format, + format, perms: [Perms.PAIRED_READ], ...props, - }; + } } -describe("request-util", () => { - it("should reduce bandwidth of boolean true", () => { - expect(formatOutgoingCharacteristicValue(true, createProps(Formats.BOOL))).toEqual(1); - }); +describe('request-util', () => { + it('should reduce bandwidth of boolean true', () => { + expect(formatOutgoingCharacteristicValue(true, createProps(Formats.BOOL))).toEqual(1) + }) - it("should reduce bandwidth of boolean false", () => { - expect(formatOutgoingCharacteristicValue(false, createProps(Formats.BOOL))).toEqual(0); - }); + it('should reduce bandwidth of boolean false', () => { + expect(formatOutgoingCharacteristicValue(false, createProps(Formats.BOOL))).toEqual(0) + }) - it("should not round valid value", () => { + it('should not round valid value', () => { const props = createProps(Formats.INT, { minStep: 1, - }); - expect(formatOutgoingCharacteristicValue(4, props)).toBe(4); - }); + }) + expect(formatOutgoingCharacteristicValue(4, props)).toBe(4) + }) - it("should round invalid value", () => { + it('should round invalid value', () => { const props = createProps(Formats.INT, { minStep: 0.15, minValue: 6, - }); - expect(formatOutgoingCharacteristicValue(6.1500001, props)).toBe(6.15); - }); + }) + expect(formatOutgoingCharacteristicValue(6.1500001, props)).toBe(6.15) + }) - it("should round up invalid value", () => { + it('should round up invalid value', () => { const props = createProps(Formats.INT, { minStep: 0.1, minValue: 2, - }); - expect(formatOutgoingCharacteristicValue(2.1542, props)).toBe(2.2); - }); + }) + expect(formatOutgoingCharacteristicValue(2.1542, props)).toBe(2.2) + }) - it("should round invalid huge value", () => { + it('should round invalid huge value', () => { const props = createProps(Formats.INT, { minStep: 0.1, minValue: 10, maxValue: 38, - }); - expect(formatOutgoingCharacteristicValue(36.135795, props)).toBe(36.1); - }); + }) + expect(formatOutgoingCharacteristicValue(36.135795, props)).toBe(36.1) + }) - it("should handle negative minimum values", () => { + it('should handle negative minimum values', () => { const props = createProps(Formats.INT, { minStep: 0.1, minValue: -100, maxValue: 100, - }); - expect(formatOutgoingCharacteristicValue(25.1, props)).toBe(25.1); - }); + }) + expect(formatOutgoingCharacteristicValue(25.1, props)).toBe(25.1) + }) - it("should handle small minimum values", () => { + it('should handle small minimum values', () => { const props = createProps(Formats.INT, { minStep: 0.1, minValue: 0.1, maxValue: 10000, - }); - expect(formatOutgoingCharacteristicValue(2.3, props)).toBe(2.3); - }); + }) + expect(formatOutgoingCharacteristicValue(2.3, props)).toBe(2.3) + }) - it("should leave string as is", () => { - const props = createProps(Formats.STRING, {}); - expect(formatOutgoingCharacteristicValue("Hello World!", props)).toBe("Hello World!"); - }); -}); + it('should leave string as is', () => { + const props = createProps(Formats.STRING, {}) + expect(formatOutgoingCharacteristicValue('Hello World!', props)).toBe('Hello World!') + }) +}) diff --git a/src/lib/util/request-util.ts b/src/lib/util/request-util.ts index dc29d286b..11a638031 100644 --- a/src/lib/util/request-util.ts +++ b/src/lib/util/request-util.ts @@ -1,5 +1,7 @@ -import { CharacteristicValue, Nullable } from "../../types"; -import { CharacteristicProps, Formats } from "../Characteristic"; +import type { CharacteristicValue, Nullable } from '../../types' +import type { CharacteristicProps } from '../Characteristic' + +import { Formats } from '../Characteristic.js' /** * Prepares the characteristic value to be sent to the HomeKit controller. @@ -12,23 +14,23 @@ import { CharacteristicProps, Formats } from "../Characteristic"; * @private * @group Utils */ -export function formatOutgoingCharacteristicValue(value: Nullable, props: CharacteristicProps): Nullable; +export function formatOutgoingCharacteristicValue(value: Nullable, props: CharacteristicProps): Nullable export function formatOutgoingCharacteristicValue(value: CharacteristicValue, props: CharacteristicProps): CharacteristicValue export function formatOutgoingCharacteristicValue(value: Nullable, props: CharacteristicProps): Nullable { - if (typeof value === "boolean") { - return value? 1: 0; - } else if (typeof value === "number") { + if (typeof value === 'boolean') { + return value ? 1 : 0 + } else if (typeof value === 'number') { if (!props.minStep || props.minStep >= 1) { - return value; + return value } - const base = props.minValue ?? 0; - const inverse = 1 / props.minStep; + const base = props.minValue ?? 0 + const inverse = 1 / props.minStep - return Math.round(((Math.round((value - base) * inverse) / inverse) + base) * 10000) / 10000; + return Math.round(((Math.round((value - base) * inverse) / inverse) + base) * 10000) / 10000 } - return value; + return value } /** @@ -36,15 +38,15 @@ export function formatOutgoingCharacteristicValue(value: Nullable { - describe("encode", () => { - test("encode simple integer", () => { - const encoded = encode(1, 42); - expect(encoded.toString("hex")) - .toEqual("01012a"); - }); - - test("encode simple string", () => { - const encoded = encode(2, "Hello World!"); - expect(encoded.toString("hex")) - .toEqual("020c" + HELLO_WORLD_BUFFER.toString("hex")); - }); - - test("encode simple string as Buffer", () => { - const buffer = Buffer.from("Hello World!"); - const encoded = encode(3, buffer); - expect(encoded.toString("hex")) - .toEqual("030c" + HELLO_WORLD_BUFFER.toString("hex")); - }); - - test("encode tlv list", () => { - const encoded = encode(4, [1, 2, 3, 4, 5]); - expect(encoded.toString("hex")) +import { Buffer } from 'node:buffer' + +import { describe, expect, it } from 'vitest' + +import { decode, decodeList, decodeWithLists, encode, readVariableUIntLE, writeVariableUIntLE } from './tlv.js' + +const HELLO_WORLD_BUFFER = Buffer.from('Hello World!') + +describe('tlv', () => { + describe('encode', () => { + it('encode simple integer', () => { + const encoded = encode(1, 42) + expect(encoded.toString('hex')) + .toEqual('01012a') + }) + + it('encode simple string', () => { + const encoded = encode(2, 'Hello World!') + expect(encoded.toString('hex')) + .toEqual(`020c${HELLO_WORLD_BUFFER.toString('hex')}`) + }) + + it('encode simple string as Buffer', () => { + const buffer = Buffer.from('Hello World!') + const encoded = encode(3, buffer) + expect(encoded.toString('hex')) + .toEqual(`030c${HELLO_WORLD_BUFFER.toString('hex')}`) + }) + + it('encode tlv list', () => { + const encoded = encode(4, [1, 2, 3, 4, 5]) + expect(encoded.toString('hex')) .toEqual( - "040101" + - "0000" + - "040102" + - "0000" + - "040103" + - "0000" + - "040104" + - "0000" + - "040105", - ); - }); - - test("encode single value tlv list", () => { - const encoded = encode(5, [1]); - expect(encoded.toString("hex")) - .toEqual("050101"); - }); - - test("encode empty tlv list", () => { - const encoded = encode(6, []); - expect(encoded.toString("hex")) - .toEqual("0600"); - }); - - test("ensure too long buffers are split!", () => { - const fill = Buffer.from([1, 2, 3]); - - const buffer = Buffer.alloc(615, fill); - const encoded = encode(0x01, buffer); - - expect(encoded.toString("hex")) + '040101' + + '0000' + + '040102' + + '0000' + + '040103' + + '0000' + + '040104' + + '0000' + + '040105', + ) + }) + + it('encode single value tlv list', () => { + const encoded = encode(5, [1]) + expect(encoded.toString('hex')) + .toEqual('050101') + }) + + it('encode empty tlv list', () => { + const encoded = encode(6, []) + expect(encoded.toString('hex')) + .toEqual('0600') + }) + + it('ensure too long buffers are split!', () => { + const fill = Buffer.from([1, 2, 3]) + + const buffer = Buffer.alloc(615, fill) + const encoded = encode(0x01, buffer) + + expect(encoded.toString('hex')) .toEqual( - "01ff" + Buffer.alloc(255, fill).toString("hex") + - "01ff" + Buffer.alloc(255, fill).toString("hex") + - "0169" + Buffer.alloc(105, fill).toString("hex"), - ); - }); - - test("encode via varargs", () => { - const encoded = encode(0x01, 1, 0x02, 2, 0x03, 3); - expect(encoded.toString("hex")) - .toEqual("010101020102030103"); - }); - }); - - describe("decode", () => { - test("decode single element", () => { - const decoded = decode(Buffer.from("010101", "hex")); + `01ff${Buffer.alloc(255, fill).toString('hex') + }01ff${Buffer.alloc(255, fill).toString('hex') + }0169${Buffer.alloc(105, fill).toString('hex')}`, + ) + }) + + it('encode via varargs', () => { + const encoded = encode(0x01, 1, 0x02, 2, 0x03, 3) + expect(encoded.toString('hex')) + .toEqual('010101020102030103') + }) + }) + + describe('decode', () => { + it('decode single element', () => { + const decoded = decode(Buffer.from('010101', 'hex')) expect(decoded).toEqual({ - 1: Buffer.from("01", "hex"), - }); - }); + 1: Buffer.from('01', 'hex'), + }) + }) - test("decode multiple elements", () => { - const decoded = decode(Buffer.from("010101020102030103", "hex")); + it('decode multiple elements', () => { + const decoded = decode(Buffer.from('010101020102030103', 'hex')) expect(decoded).toEqual({ - 1: Buffer.from("01", "hex"), - 2: Buffer.from("02", "hex"), - 3: Buffer.from("03", "hex"), - }); - }); - - test("decode concatenation of multiple elements", () => { - const decoded = decode(Buffer.from("010101010102010103", "hex")); + 1: Buffer.from('01', 'hex'), + 2: Buffer.from('02', 'hex'), + 3: Buffer.from('03', 'hex'), + }) + }) + + it('decode concatenation of multiple elements', () => { + const decoded = decode(Buffer.from('010101010102010103', 'hex')) expect(decoded).toEqual({ - 1: Buffer.from("010203", "hex"), - }); - }); - }); - - describe("decodeWithLists", () => { - test("decode single element", () => { - const decoded = decodeWithLists(Buffer.from("010101", "hex")); + 1: Buffer.from('010203', 'hex'), + }) + }) + }) + + describe('decodeWithLists', () => { + it('decode single element', () => { + const decoded = decodeWithLists(Buffer.from('010101', 'hex')) expect(decoded).toEqual({ - 1: Buffer.from("01", "hex"), - }); - }); + 1: Buffer.from('01', 'hex'), + }) + }) - test("decode multiple elements", () => { - const decoded = decodeWithLists(Buffer.from("010101020102030103", "hex")); + it('decode multiple elements', () => { + const decoded = decodeWithLists(Buffer.from('010101020102030103', 'hex')) expect(decoded).toEqual({ - 1: Buffer.from("01", "hex"), - 2: Buffer.from("02", "hex"), - 3: Buffer.from("03", "hex"), - }); - }); + 1: Buffer.from('01', 'hex'), + 2: Buffer.from('02', 'hex'), + 3: Buffer.from('03', 'hex'), + }) + }) - test("decode concatenation of multiple elements", () => { + it('decode concatenation of multiple elements', () => { // tlv entries longer than 255 are split over multiple entries // decodeWithList should reassemble them to a single entry! const content = Buffer.concat([ - Buffer.from("01ff", "hex"), - Buffer.alloc(255, "A", "ascii"), - Buffer.from("0102", "hex"), - Buffer.alloc(2, "A", "ascii"), - ]); + Buffer.from('01ff', 'hex'), + Buffer.alloc(255, 'A', 'ascii'), + Buffer.from('0102', 'hex'), + Buffer.alloc(2, 'A', 'ascii'), + ]) - const decoded = decodeWithLists(content); + const decoded = decodeWithLists(content) expect(decoded).toEqual({ - 1: Buffer.alloc(257, "A", "ascii"), - }); - }); + 1: Buffer.alloc(257, 'A', 'ascii'), + }) + }) - test("decode list of entries", () => { + it('decode list of entries', () => { const content = Buffer.from( - "010101" + - "0000" + - "010102" + - "0000" + - "010103" + - "0000" + - "010104", - "hex", - ); - - const decoded = decodeWithLists(content); + '010101' + + '0000' + + '010102' + + '0000' + + '010103' + + '0000' + + '010104', + 'hex', + ) + + const decoded = decodeWithLists(content) expect(decoded).toEqual({ 1: [ - Buffer.from("01", "hex"), - Buffer.from("02", "hex"), - Buffer.from("03", "hex"), - Buffer.from("04", "hex"), + Buffer.from('01', 'hex'), + Buffer.from('02', 'hex'), + Buffer.from('03', 'hex'), + Buffer.from('04', 'hex'), ], - }); - }); + }) + }) - test("decode list of entries where some are split due to size limits", () => { + it('decode list of entries where some are split due to size limits', () => { const content = Buffer.concat([ - Buffer.from("01010b", "hex"), - Buffer.from("0000", "hex"), - Buffer.from("01010c", "hex"), - Buffer.from("0000", "hex"), - Buffer.from("01ff", "hex"), - Buffer.alloc(255, "A", "ascii"), - Buffer.from("0102", "hex"), - Buffer.alloc(2, "A", "ascii"), - ]); - - const decoded = decodeWithLists(content); + Buffer.from('01010b', 'hex'), + Buffer.from('0000', 'hex'), + Buffer.from('01010c', 'hex'), + Buffer.from('0000', 'hex'), + Buffer.from('01ff', 'hex'), + Buffer.alloc(255, 'A', 'ascii'), + Buffer.from('0102', 'hex'), + Buffer.alloc(2, 'A', 'ascii'), + ]) + + const decoded = decodeWithLists(content) expect(decoded).toEqual({ 1: [ - Buffer.from("0b", "hex"), - Buffer.from("0c", "hex"), - Buffer.alloc(257, "A", "ascii"), + Buffer.from('0b', 'hex'), + Buffer.from('0c', 'hex'), + Buffer.alloc(257, 'A', 'ascii'), ], - }); - }); + }) + }) - test("reject lists which are not properly separated with zero tlv type", () => { - expect(() => decodeWithLists(Buffer.from("01010a01010b", "hex"))) - .toThrowError(); - }); - }); + it('reject lists which are not properly separated with zero tlv type', () => { + expect(() => decodeWithLists(Buffer.from('01010a01010b', 'hex'))) + .toThrowError() + }) + }) - describe("decodeList", () => { - test("simple concatenated list buffer", () => { - const decoded = decodeList(Buffer.from("010101010102010103010104", "hex"), 0x01); + describe('decodeList', () => { + it('simple concatenated list buffer', () => { + const decoded = decodeList(Buffer.from('010101010102010103010104', 'hex'), 0x01) expect(decoded).toEqual([ - { 1: Buffer.from("01", "hex") }, - { 1: Buffer.from("02", "hex") }, - { 1: Buffer.from("03", "hex") }, - { 1: Buffer.from("04", "hex") }, - ]); - }); - - test("list buffer with multiple tlv entries", () => { + { 1: Buffer.from('01', 'hex') }, + { 1: Buffer.from('02', 'hex') }, + { 1: Buffer.from('03', 'hex') }, + { 1: Buffer.from('04', 'hex') }, + ]) + }) + + it('list buffer with multiple tlv entries', () => { const decoded = decodeList(Buffer.from( - "01010102010a" + - "01010202010b", - "hex", - ), 0x01); + '01010102010a' + + '01010202010b', + 'hex', + ), 0x01) expect(decoded).toEqual([ { - 1: Buffer.from("01", "hex"), - 2: Buffer.from("0a", "hex"), + 1: Buffer.from('01', 'hex'), + 2: Buffer.from('0a', 'hex'), }, { - 1: Buffer.from("02", "hex"), - 2: Buffer.from("0b", "hex"), + 1: Buffer.from('02', 'hex'), + 2: Buffer.from('0b', 'hex'), }, - ]); - }); + ]) + }) - test("concatenate multiple entries of same value", () => { - const decoded = decodeList(Buffer.from("01010102010a02010b", "hex"), 0x01); + it('concatenate multiple entries of same value', () => { + const decoded = decodeList(Buffer.from('01010102010a02010b', 'hex'), 0x01) expect(decoded).toEqual([ { - 1: Buffer.from("01", "hex"), - 2: Buffer.from("0a0b", "hex"), + 1: Buffer.from('01', 'hex'), + 2: Buffer.from('0a0b', 'hex'), }, - ]); - }); + ]) + }) - test("reject ill-formatted tlv list", () => { - expect(() => decodeList(Buffer.from("020101", "hex"), 0x01)) - .toThrowError(); - }); - }); + it('reject ill-formatted tlv list', () => { + expect(() => decodeList(Buffer.from('020101', 'hex'), 0x01)) + .toThrowError() + }) + }) - describe("buffer", () => { - test("writeVariableUIntLE", () => { + describe('buffer', () => { + it('writeVariableUIntLE', () => { // negative numbers are not allowed expect(() => writeVariableUIntLE(-1)) - .toThrowError(); - - const input8 = 128; - const buffer8 = writeVariableUIntLE(input8); - expect(buffer8.length).toBe(1); - expect(buffer8.readUInt8(0)).toBe(input8); - expect(readVariableUIntLE(buffer8)).toBe(input8); - - const input16 = 512; - const buffer16 = writeVariableUIntLE(input16); - expect(buffer16.length).toBe(2); - expect(buffer16.readUInt16LE(0)).toBe(input16); - expect(readVariableUIntLE(buffer16)).toBe(input16); - - const input32 = 70_000; - const buffer32 = writeVariableUIntLE(input32); - expect(buffer32.length).toBe(4); - expect(buffer32.readUInt32LE(0)).toBe(input32); - expect(readVariableUIntLE(buffer32)).toBe(input32); - - const input64 = 2*0xFFFFFFFF; - const buffer64 = writeVariableUIntLE(input64); // shifted by one - expect(buffer64.length).toBe(8); + .toThrowError() + + const input8 = 128 + const buffer8 = writeVariableUIntLE(input8) + expect(buffer8.length).toBe(1) + expect(buffer8.readUInt8(0)).toBe(input8) + expect(readVariableUIntLE(buffer8)).toBe(input8) + + const input16 = 512 + const buffer16 = writeVariableUIntLE(input16) + expect(buffer16.length).toBe(2) + expect(buffer16.readUInt16LE(0)).toBe(input16) + expect(readVariableUIntLE(buffer16)).toBe(input16) + + const input32 = 70_000 + const buffer32 = writeVariableUIntLE(input32) + expect(buffer32.length).toBe(4) + expect(buffer32.readUInt32LE(0)).toBe(input32) + expect(readVariableUIntLE(buffer32)).toBe(input32) + + const input64 = 2 * 0xFFFFFFFF + const buffer64 = writeVariableUIntLE(input64) // shifted by one + expect(buffer64.length).toBe(8) // hex is just 32 bit 0xFF shifter by one - expect(buffer64.toString("hex")).toBe("feffffff01000000"); - expect(readVariableUIntLE(buffer64)).toBe(input64); - }); - }); -}); + expect(buffer64.toString('hex')).toBe('feffffff01000000') + expect(readVariableUIntLE(buffer64)).toBe(input64) + }) + }) +}) diff --git a/src/lib/util/tlv.ts b/src/lib/util/tlv.ts index f13469069..bf1971a5c 100644 --- a/src/lib/util/tlv.ts +++ b/src/lib/util/tlv.ts @@ -1,76 +1,76 @@ -import assert from "assert"; -import * as hapCrypto from "../util/hapCrypto"; +import assert from 'node:assert' +import { Buffer } from 'node:buffer' + +import { writeUInt64LE } from '../util/hapCrypto.js' /** * Type Length Value encoding/decoding, used by HAP as a wire format. * https://en.wikipedia.org/wiki/Type-length-value */ -const EMPTY_TLV_TYPE = 0x00; // and empty tlv with id 0 is usually used as delimiter for tlv lists +const EMPTY_TLV_TYPE = 0x00 // and empty tlv with id 0 is usually used as delimiter for tlv lists /** * @group TLV8 */ -export type TLVEncodable = Buffer | number | string; +export type TLVEncodable = Buffer | number | string /** * @group TLV8 */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function encode(type: number, data: TLVEncodable | TLVEncodable[], ...args: any[]): Buffer { - const encodedTLVBuffers: Buffer[] = []; + const encodedTLVBuffers: Buffer[] = [] // coerce data to Buffer if needed - if (typeof data === "number") { - data = Buffer.from([data]); - } else if (typeof data === "string") { - data = Buffer.from(data); + if (typeof data === 'number') { + data = Buffer.from([data]) + } else if (typeof data === 'string') { + data = Buffer.from(data) } if (Array.isArray(data)) { - let first = true; + let first = true for (const entry of data) { if (!first) { - encodedTLVBuffers.push(Buffer.from([EMPTY_TLV_TYPE, 0])); // push delimiter + encodedTLVBuffers.push(Buffer.from([EMPTY_TLV_TYPE, 0])) // push delimiter } - first = false; - encodedTLVBuffers.push(encode(type, entry)); + first = false + encodedTLVBuffers.push(encode(type, entry)) } if (first) { // we have a zero length array! - encodedTLVBuffers.push(Buffer.from([type, 0])); + encodedTLVBuffers.push(Buffer.from([type, 0])) } } else if (data.length <= 255) { - encodedTLVBuffers.push(Buffer.concat([Buffer.from([type,data.length]),data])); + encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, data.length]), data])) } else { // otherwise it doesn't fit into one tlv entry, thus we push multiple - let leftBytes = data.length; - let currentIndex = 0; + let leftBytes = data.length + let currentIndex = 0 for (; leftBytes > 0;) { if (leftBytes >= 255) { - encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, 0xFF]), data.slice(currentIndex, currentIndex + 255)])); - leftBytes -= 255; - currentIndex += 255; + encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, 0xFF]), data.subarray(currentIndex, currentIndex + 255)])) + leftBytes -= 255 + currentIndex += 255 } else { - encodedTLVBuffers.push(Buffer.concat([Buffer.from([type,leftBytes]), data.slice(currentIndex)])); - leftBytes -= leftBytes; + encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, leftBytes]), data.subarray(currentIndex)])) + leftBytes -= leftBytes } } } // do we have more arguments to encode? if (args.length >= 2) { - // chop off the first two arguments which we already processed, and process the rest recursively - const [ nextType, nextData, ...nextArgs ] = args; - const remainingTLVBuffer = encode(nextType, nextData, ...nextArgs); + const [nextType, nextData, ...nextArgs] = args + const remainingTLVBuffer = encode(nextType, nextData, ...nextArgs) // append the remaining encoded arguments directly to the buffer - encodedTLVBuffers.push(remainingTLVBuffer); + encodedTLVBuffers.push(remainingTLVBuffer) } - return Buffer.concat(encodedTLVBuffers); + return Buffer.concat(encodedTLVBuffers) } /** @@ -85,31 +85,31 @@ export function encode(type: number, data: TLVEncodable | TLVEncodable[], ...arg * @group TLV8 */ export function decode(buffer: Buffer): Record { - assert(buffer instanceof Buffer, "Illegal argument. tlv.decode() expects Buffer type!"); - const objects: Record = {}; + assert(buffer instanceof Buffer, 'Illegal argument. tlv.decode() expects Buffer type!') + const objects: Record = {} - let leftLength = buffer.length; - let currentIndex = 0; + let leftLength = buffer.length + let currentIndex = 0 for (; leftLength > 0;) { - const type = buffer[currentIndex]; - const length = buffer[currentIndex + 1]; - currentIndex += 2; - leftLength -= 2; + const type = buffer[currentIndex] + const length = buffer[currentIndex + 1] + currentIndex += 2 + leftLength -= 2 - const data = buffer.slice(currentIndex, currentIndex + length); + const data = buffer.subarray(currentIndex, currentIndex + length) if (objects[type]) { - objects[type] = Buffer.concat([objects[type],data]); + objects[type] = Buffer.concat([objects[type], data]) } else { - objects[type] = data; + objects[type] = data } - currentIndex += length; - leftLength -= length; + currentIndex += length + leftLength -= length } - return objects; + return objects } /** @@ -122,59 +122,59 @@ export function decode(buffer: Buffer): Record { * @group TLV8 */ export function decodeWithLists(buffer: Buffer): Record { - const result: Record = {}; + const result: Record = {} - let leftBytes = buffer.length; - let readIndex = 0; + let leftBytes = buffer.length + let readIndex = 0 - let lastType = -1; - let lastLength = -1; - let lastItemWasDelimiter = false; + let lastType = -1 + let lastLength = -1 + let lastItemWasDelimiter = false for (; leftBytes > 0;) { - const type = buffer.readUInt8(readIndex++); - const length = buffer.readUInt8(readIndex++); - leftBytes -= 2; + const type = buffer.readUInt8(readIndex++) + const length = buffer.readUInt8(readIndex++) + leftBytes -= 2 - const data = buffer.slice(readIndex, readIndex + length); - readIndex += length; - leftBytes -= length; + const data = buffer.subarray(readIndex, readIndex + length) + readIndex += length + leftBytes -= length if (type === 0 && length === 0) { - lastItemWasDelimiter = true; - continue; + lastItemWasDelimiter = true + continue } - const existing = result[type]; + const existing = result[type] if (existing) { // there is already an item with the same type if (lastItemWasDelimiter && lastType === type) { // list of tlv types if (Array.isArray(existing)) { - existing.push(data); + existing.push(data) } else { - result[type] = [existing, data]; + result[type] = [existing, data] } } else if (lastType === type && lastLength === 255) { // tlv data got split into multiple entries as length exceeded 255 if (Array.isArray(existing)) { // append to the last data blob in the array - const last = existing[existing.length - 1]; - existing[existing.length - 1] = Buffer.concat([last, data]); + const last = existing[existing.length - 1] + existing[existing.length - 1] = Buffer.concat([last, data]) } else { - result[type] = Buffer.concat([existing, data]); + result[type] = Buffer.concat([existing, data]) } } else { throw new Error(`Found duplicated tlv entry with type ${type} and length ${length} ` - + `(lastItemWasDelimiter: ${lastItemWasDelimiter}, lastType: ${lastType}, lastLength: ${lastLength})`); + + `(lastItemWasDelimiter: ${lastItemWasDelimiter}, lastType: ${lastType}, lastLength: ${lastLength})`) } } else { - result[type] = data; + result[type] = data } - lastType = type; - lastLength = length; - lastItemWasDelimiter = false; + lastType = type + lastLength = length + lastItemWasDelimiter = false } - return result; + return result } /** @@ -192,54 +192,54 @@ export function decodeWithLists(buffer: Buffer): Record[] { - const objectsList: Record[] = []; + const objectsList: Record[] = [] - let leftLength = data.length; - let currentIndex = 0; + let leftLength = data.length + let currentIndex = 0 - let objects: Record | undefined = undefined; + let objects: Record | undefined for (; leftLength > 0;) { - const type = data[currentIndex]; // T - const length = data[currentIndex + 1]; // L - const value = data.slice(currentIndex + 2, currentIndex + 2 + length); // V + const type = data[currentIndex] // T + const length = data[currentIndex + 1] // L + const value = data.subarray(currentIndex + 2, currentIndex + 2 + length) // V if (type === entryStartId) { // we got the start of a new entry if (objects !== undefined) { // save the previous entry - objectsList.push(objects); + objectsList.push(objects) } - objects = {}; + objects = {} } if (objects === undefined) { - throw new Error("Error parsing tlv list: Encountered uninitialized storage object"); + throw new Error('Error parsing tlv list: Encountered uninitialized storage object') } if (objects[type]) { // append to buffer if we have already data for this type - objects[type] = Buffer.concat([objects[type], value]); + objects[type] = Buffer.concat([objects[type], value]) } else { - objects[type] = value; + objects[type] = value } - currentIndex += 2 + length; - leftLength -= 2 + length; + currentIndex += 2 + length + leftLength -= 2 + length } if (objects !== undefined) { - objectsList.push(objects); + objectsList.push(objects) } // push last entry - return objectsList; + return objectsList } /** * @group TLV8 */ export function readUInt64LE(buffer: Buffer, offset = 0): number { - const low = buffer.readUInt32LE(offset); + const low = buffer.readUInt32LE(offset) // javascript doesn't allow to shift by 32(?), therefore we multiply here - return buffer.readUInt32LE(offset + 4) * 0x100000000 + low; + return buffer.readUInt32LE(offset + 4) * 0x100000000 + low } /** @@ -247,11 +247,11 @@ export function readUInt64LE(buffer: Buffer, offset = 0): number { * @group TLV8 */ export function writeUInt32(value: number): Buffer { - const buffer = Buffer.alloc(4); + const buffer = Buffer.alloc(4) - buffer.writeUInt32LE(value, 0); + buffer.writeUInt32LE(value, 0) - return buffer; + return buffer } /** @@ -259,16 +259,16 @@ export function writeUInt32(value: number): Buffer { * @group TLV8 */ export function readUInt32(buffer: Buffer): number { - return buffer.readUInt32LE(0); + return buffer.readUInt32LE(0) } /** * @group TLV8 */ export function writeFloat32LE(value: number): Buffer { - const buffer = Buffer.alloc(4); - buffer.writeFloatLE(value, 0); - return buffer; + const buffer = Buffer.alloc(4) + buffer.writeFloatLE(value, 0) + return buffer } /** @@ -276,11 +276,11 @@ export function writeFloat32LE(value: number): Buffer { * @group TLV8 */ export function writeUInt16(value: number): Buffer { - const buffer = Buffer.alloc(2); + const buffer = Buffer.alloc(2) - buffer.writeUInt16LE(value, 0); + buffer.writeUInt16LE(value, 0) - return buffer; + return buffer } /** @@ -288,28 +288,26 @@ export function writeUInt16(value: number): Buffer { * @group TLV8 */ export function readUInt16(buffer: Buffer): number { - return buffer.readUInt16LE(0); + return buffer.readUInt16LE(0) } - /** * Reads variable size unsigned integer {@link writeVariableUIntLE}. * @param buffer - The buffer to read from. It must have exactly the size of the given integer. * @group TLV8 */ export function readVariableUIntLE(buffer: Buffer): number { - switch (buffer.length) { - case 1: - return buffer.readUInt8(0); - case 2: - return buffer.readUInt16LE(0); - case 4: - return buffer.readUInt32LE(0); - case 8: - return readUInt64LE(buffer, 0); - default: - throw new Error("Can't read uint LE with length " + buffer.length); + case 1: + return buffer.readUInt8(0) + case 2: + return buffer.readUInt16LE(0) + case 4: + return buffer.readUInt32LE(0) + case 8: + return readUInt64LE(buffer, 0) + default: + throw new Error(`Can't read uint LE with length ${buffer.length}`) } } @@ -323,19 +321,19 @@ export function readVariableUIntLE(buffer: Buffer): number { * @group TLV8 */ export function writeVariableUIntLE(number: number): Buffer { - assert(number >= 0, "Can't encode a negative integer as unsigned integer"); + assert(number >= 0, 'Can\'t encode a negative integer as unsigned integer') if (number <= 255) { - const buffer = Buffer.alloc(1); - buffer.writeUInt8(number, 0); - return buffer; + const buffer = Buffer.alloc(1) + buffer.writeUInt8(number, 0) + return buffer } else if (number <= 65535) { - return writeUInt16(number); + return writeUInt16(number) } else if (number <= 4294967295) { - return writeUInt32(number); + return writeUInt32(number) } else { - const buffer = Buffer.alloc(8); - hapCrypto.writeUInt64LE(number, buffer, 0); - return buffer; + const buffer = Buffer.alloc(8) + writeUInt64LE(number, buffer, 0) + return buffer } } diff --git a/src/lib/util/uuid.spec.ts b/src/lib/util/uuid.spec.ts index 129874c12..93514f113 100644 --- a/src/lib/util/uuid.spec.ts +++ b/src/lib/util/uuid.spec.ts @@ -1,59 +1,63 @@ -import { toShortForm, toLongForm, unparse, write } from "./uuid"; - -const uuidString = "cf47a128-769a-4563-8203-11f307b1926d"; -const uuidBuffer = Buffer.from(uuidString.replace(/-/g, ""), "hex"); - -describe("uuid", () => { - describe("toShortForm", () => { - it("should return short form UUIDs without providing a base UUID", () => { - const VALUE = "0000003E-0000-1000-8000-0026BB765291"; - expect(toShortForm(VALUE)).toBe("3E"); - }); - - it("should return short form UUIDs when provided with a matching base UUID", () => { - const VALUE = "0000003E-0000-1000-8000-0026BB765291"; - const BASE = "-0000-1000-8000-0026BB765291"; - expect(toShortForm(VALUE, BASE)).toBe("3E"); - }); - - it("should return standard UUIDs when provided with a non-matching base UUID", () => { - const VALUE = "0000003E-0000-1000-8000-0026BB765292"; - const BASE = "-0000-1000-8000-0026BB765291"; - expect(toShortForm(VALUE, BASE)).toBe(VALUE); - }); - - it("should not be case-sensitive when checking if the UUID matches the base UUID", () => { - const VALUE = "0000003e-0000-1000-8000-0026bb765291"; - const BASE = "-0000-1000-8000-0026BB765291"; - const EXPECTED = "0000003E-0000-1000-8000-0026BB765291"; - expect(toShortForm(VALUE, BASE)).toEqual(EXPECTED); - }); - }); - - describe("toLongForm", () => { - it("should return standard UUIDs", () => { - const VALUE = "3E"; - const BASE = "-0000-1000-8000-0026BB765291"; - const EXPECTED = "0000003E-0000-1000-8000-0026BB765291"; - expect(toLongForm(VALUE, BASE)).toBe(EXPECTED); - }); - }); - - it("should read/write uuids from Buffer", () => { - const uuid = unparse(uuidBuffer); - expect(uuid).toBe(uuidString); - - const buffer = write(uuid); - expect(buffer.toString("hex")).toBe(uuidBuffer.toString("hex")); - expect(unparse(buffer)).toBe(uuid); - }); - - it("should read/write uuids from Buffer with offset", () => { - const buffer = Buffer.concat([Buffer.alloc(5, "A"), uuidBuffer]); - const uuid = unparse(buffer, 5); - - const resultBuffer = Buffer.alloc(21, "FF", "hex"); - write(uuid, resultBuffer, 5); - expect(resultBuffer.toString("hex")).toBe("ffffffffff" + uuidBuffer.toString("hex")); - }); -}); +import { Buffer } from 'node:buffer' + +import { describe, expect, it } from 'vitest' + +import { toLongForm, toShortForm, unparse, write } from './uuid.js' + +const uuidString = 'cf47a128-769a-4563-8203-11f307b1926d' +const uuidBuffer = Buffer.from(uuidString.replace(/-/g, ''), 'hex') + +describe('uuid', () => { + describe('toShortForm', () => { + it('should return short form UUIDs without providing a base UUID', () => { + const VALUE = '0000003E-0000-1000-8000-0026BB765291' + expect(toShortForm(VALUE)).toBe('3E') + }) + + it('should return short form UUIDs when provided with a matching base UUID', () => { + const VALUE = '0000003E-0000-1000-8000-0026BB765291' + const BASE = '-0000-1000-8000-0026BB765291' + expect(toShortForm(VALUE, BASE)).toBe('3E') + }) + + it('should return standard UUIDs when provided with a non-matching base UUID', () => { + const VALUE = '0000003E-0000-1000-8000-0026BB765292' + const BASE = '-0000-1000-8000-0026BB765291' + expect(toShortForm(VALUE, BASE)).toBe(VALUE) + }) + + it('should not be case-sensitive when checking if the UUID matches the base UUID', () => { + const VALUE = '0000003e-0000-1000-8000-0026bb765291' + const BASE = '-0000-1000-8000-0026BB765291' + const EXPECTED = '0000003E-0000-1000-8000-0026BB765291' + expect(toShortForm(VALUE, BASE)).toEqual(EXPECTED) + }) + }) + + describe('toLongForm', () => { + it('should return standard UUIDs', () => { + const VALUE = '3E' + const BASE = '-0000-1000-8000-0026BB765291' + const EXPECTED = '0000003E-0000-1000-8000-0026BB765291' + expect(toLongForm(VALUE, BASE)).toBe(EXPECTED) + }) + }) + + it('should read/write uuids from Buffer', () => { + const uuid = unparse(uuidBuffer) + expect(uuid).toBe(uuidString) + + const buffer = write(uuid) + expect(buffer.toString('hex')).toBe(uuidBuffer.toString('hex')) + expect(unparse(buffer)).toBe(uuid) + }) + + it('should read/write uuids from Buffer with offset', () => { + const buffer = Buffer.concat([Buffer.alloc(5, 'A'), uuidBuffer]) + const uuid = unparse(buffer, 5) + + const resultBuffer = Buffer.alloc(21, 'FF', 'hex') + write(uuid, resultBuffer, 5) + expect(resultBuffer.toString('hex')).toBe(`ffffffffff${uuidBuffer.toString('hex')}`) + }) +}) diff --git a/src/lib/util/uuid.ts b/src/lib/util/uuid.ts index c1603f006..28f966e9d 100644 --- a/src/lib/util/uuid.ts +++ b/src/lib/util/uuid.ts @@ -1,35 +1,36 @@ -import crypto from "crypto"; +/* global NodeJS */ +import { Buffer } from 'node:buffer' +import { createHash } from 'node:crypto' -export type Binary = Buffer | NodeJS.TypedArray | DataView; -export type BinaryLike = string | Binary; +export type Binary = Buffer | NodeJS.TypedArray | DataView +export type BinaryLike = string | Binary -export const BASE_UUID = "-0000-1000-8000-0026BB765291"; +export const BASE_UUID = '-0000-1000-8000-0026BB765291' // http://stackoverflow.com/a/25951500/66673 export function generate(data: BinaryLike): string { - const sha1sum = crypto.createHash("sha1"); - sha1sum.update(data); - const s = sha1sum.digest("hex"); - let i = -1; - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c: string) => { - i += 1; + const sha1sum = createHash('sha1') + sha1sum.update(data) + const s = sha1sum.digest('hex') + let i = -1 + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => { + i += 1 switch (c) { - case "y": - return ((parseInt("0x" + s[i], 16) & 0x3) | 0x8).toString(16); - case "x": - default: - return s[i]; + case 'y': + return ((Number.parseInt(`0x${s[i]}`, 16) & 0x3) | 0x8).toString(16) + case 'x': + default: + return s[i] } - }); + }) } -const VALID_UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const VALID_UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i export function isValid(UUID: string): boolean { - return VALID_UUID_REGEX.test(UUID); + return VALID_UUID_REGEX.test(UUID) } - /** * Parses the uuid as a string from the given Buffer. * The parser will use the first 8 bytes. @@ -44,56 +45,56 @@ export function unparse(buf: Buffer): string */ export function unparse(buf: Buffer, offset: number): string export function unparse(buf: Buffer | string, offset = 0): string { - let i = offset; + let i = offset - return buf.toString("hex", i, (i += 4)) + "-" + - buf.toString("hex", i, (i += 2)) + "-" + - buf.toString("hex", i, (i += 2)) + "-" + - buf.toString("hex", i, (i += 2)) + "-" + - buf.toString("hex", i, i + 6); + return `${buf.toString('hex', i, (i += 4))}-${ + buf.toString('hex', i, (i += 2))}-${ + buf.toString('hex', i, (i += 2))}-${ + buf.toString('hex', i, (i += 2))}-${ + buf.toString('hex', i, i + 6)}` } export function write(uuid: string): Buffer export function write(uuid: string, buf: Buffer, offset: number): void export function write(uuid: string, buf?: Buffer, offset = 0): Buffer { - const buffer = Buffer.from(uuid.replace(/-/g, ""), "hex"); + const buffer = Buffer.from(uuid.replace(/-/g, ''), 'hex') if (buf) { - buffer.copy(buf, offset); - return buf; + buffer.copy(buf, offset) + return buf } else { - return buffer; + return buffer } } -const SHORT_FORM_REGEX = /^0*([0-9a-f]{1,8})-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i; +const SHORT_FORM_REGEX = /^0*([0-9a-f]{1,8})-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i export function toShortForm(uuid: string, base: string = BASE_UUID): string { if (!isValid(uuid)) { - throw new TypeError("uuid was not a valid UUID or short form UUID"); + throw new TypeError('uuid was not a valid UUID or short form UUID') } - if (base && !isValid("00000000" + base)) { - throw new TypeError("base was not a valid base UUID"); + if (base && !isValid(`00000000${base}`)) { + throw new TypeError('base was not a valid base UUID') } if (base && !uuid.endsWith(base)) { - return uuid.toUpperCase(); + return uuid.toUpperCase() } - return uuid.replace(SHORT_FORM_REGEX, "$1").toUpperCase(); + return uuid.replace(SHORT_FORM_REGEX, '$1').toUpperCase() } -const VALID_SHORT_REGEX = /^[0-9a-f]{1,8}$/i; +const VALID_SHORT_REGEX = /^[0-9a-f]{1,8}$/i export function toLongForm(uuid: string, base: string = BASE_UUID): string { if (isValid(uuid)) { - return uuid.toUpperCase(); + return uuid.toUpperCase() } if (!VALID_SHORT_REGEX.test(uuid)) { - throw new TypeError("uuid was not a valid UUID or short form UUID"); + throw new TypeError('uuid was not a valid UUID or short form UUID') } - if (!isValid("00000000" + base)) { - throw new TypeError("base was not a valid base UUID"); + if (!isValid(`00000000${base}`)) { + throw new TypeError('base was not a valid base UUID') } - return (("00000000" + uuid).substr(-8) + base).toUpperCase(); + return ((`00000000${uuid}`).slice(-8) + base).toUpperCase() } diff --git a/src/test-utils/HAPHTTPClient.ts b/src/test-utils/HAPHTTPClient.ts index fae72d233..a86954477 100644 --- a/src/test-utils/HAPHTTPClient.ts +++ b/src/test-utils/HAPHTTPClient.ts @@ -1,15 +1,11 @@ -import assert from "assert"; -import { Agent } from "http"; -import { HeaderObject, HTTPParser } from "http-parser-js"; -import { Socket } from "net"; -import { HAPMimeTypes, PairingStates, PairMethods, TLVValues } from "../internal-types"; -import { HAPHTTPCode, HAPPairingHTTPCode } from "../lib/HAPServer"; -import { PairingInformation, PermissionTypes } from "../lib/model/AccessoryInfo"; -import { HAPEncryption, HAPUsername } from "../lib/util/eventedhttp"; -import * as hapCrypto from "../lib/util/hapCrypto"; -import { PromiseTimeout } from "../lib/util/promise-utils"; -import * as tlv from "../lib/util/tlv"; -import { +import type { Agent } from 'node:http' +import type { Socket } from 'node:net' + +import type { HeaderObject } from 'http-parser-js' + +import type { PairingInformation, PermissionTypes } from '../lib/model/AccessoryInfo' +import type { HAPEncryption, HAPUsername } from '../lib/util/eventedhttp' +import type { AccessoriesResponse, CharacteristicId, CharacteristicsReadResponse, @@ -17,208 +13,228 @@ import { CharacteristicsWriteResponse, PrepareWriteRequest, ResourceRequest, -} from "../types"; -import { HAPHTTPError } from "./HAPHTTPError"; -import { TLVError } from "./tlvError"; +} from '../types' + +import assert from 'node:assert' +import { Buffer } from 'node:buffer' + +import { HTTPParser } from 'http-parser-js' +import { expect } from 'vitest' + +import { HAPMimeTypes, PairingStates, PairMethods, TLVValues } from '../internal-types.js' +import { HAPHTTPCode, HAPPairingHTTPCode } from '../lib/HAPServer.js' +import { layerDecrypt, layerEncrypt } from '../lib/util/hapCrypto.js' +import { PromiseTimeout } from '../lib/util/promise-utils.js' +import { decode, decodeList, encode } from '../lib/util/tlv.js' +import { HAPHTTPError } from './HAPHTTPError.js' +import { TLVError } from './tlvError.js' export interface HTTPResponse { - shouldKeepAlive: boolean; - upgrade: boolean; - statusCode: number; - statusMessage: string; - versionMajor: number; - versionMinor: number; - headers: Record; - body: T; - trailers: string[]; + shouldKeepAlive: boolean + upgrade: boolean + statusCode: number + statusMessage: string + versionMajor: number + versionMinor: number + headers: Record + body: T + trailers: string[] } /** * A http client that wraps around a http agent. */ export class HAPHTTPClient { - private readonly agent: Agent; - private readonly address: string; - private readonly port: number; + private readonly agent: Agent + private readonly address: string + private readonly port: number - private currentSocket?: Socket; - private encryption?: HAPEncryption; + private currentSocket?: Socket + private encryption?: HAPEncryption - private currentDataListener?: (data: Buffer) => void; - private dataQueue: Buffer[] = []; + private currentDataListener?: (data: Buffer) => void + private dataQueue: Buffer[] = [] constructor(agent: Agent, address: string, port: number) { - this.agent = agent; - this.address = address; - this.port = port; + this.agent = agent + this.address = address + this.port = port } attachSocket(): void { - expect(this.currentSocket).toBeUndefined(); - expect(this.currentDataListener).toBeUndefined(); + expect(this.currentSocket).toBeUndefined() + expect(this.currentDataListener).toBeUndefined() // we extract the underlying TCP socket! - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const freeSockets = ((this.agent as any).freeSockets as Record); - expect(Object.values(freeSockets).length).toBe(1); - this.currentSocket = Object.values(freeSockets)[0][0]; + const freeSockets = (this.agent as any).freeSockets as Record + expect(Object.values(freeSockets).length).toBe(1) + this.currentSocket = Object.values(freeSockets)[0][0] - this.currentDataListener = data => { + this.currentDataListener = (data) => { // packets shall fit into a single TCP segment, no need to write a parser! - this.dataQueue.push(data); - }; - this.currentSocket.on("data", this.currentDataListener); + this.dataQueue.push(data) + } + this.currentSocket.on('data', this.currentDataListener) } get receiveBufferCount(): number { - return this.dataQueue.length; + return this.dataQueue.length } enableEncryption(encryption: HAPEncryption): void { - this.encryption = encryption; + this.encryption = encryption } disableEncryption(): void { - this.encryption = undefined; + this.encryption = undefined } popReceiveBuffer(): Buffer { - expect(this.currentSocket).toBeDefined(); - expect(this.dataQueue.length > 0).toBeTruthy(); - const buffer = this.dataQueue.splice(0, 1)[0]; + expect(this.currentSocket).toBeDefined() + expect(this.dataQueue.length > 0).toBeTruthy() + const buffer = this.dataQueue.splice(0, 1)[0] if (this.encryption) { - return hapCrypto.layerDecrypt(buffer, this.encryption); + return layerDecrypt(buffer, this.encryption) } - return buffer; + return buffer } formatHTTPRequest( - method: "GET" | "POST" | "PUT" | "DELETE", + method: 'GET' | 'POST' | 'PUT' | 'DELETE', route: string, data?: Buffer, - contentType = "application/json", + contentType = 'application/json', ): Buffer { - expect(!!data || method === "GET").toBeTruthy(); - const buffer = Buffer.from(`${method} ${route} HTTP/1.1\r\n` + - "Accept: application/json, text/plain, */*\r\n" + - "User-Agent: test-util\r\n" + - "Host: " + this.address + ":" + this.port + "\r\n" + - "Connection: keep-alive\r\n" + - (data - ? "Content-Type: " + contentType + "\r\n" + - "Content-Length: " + data.length + "\r\n" - : "" - ) + - "\r\n"); + expect(!!data || method === 'GET').toBeTruthy() + const buffer = Buffer.from(`${method} ${route} HTTP/1.1\r\n` + + `Accept: application/json, text/plain, */*\r\n` + + `User-Agent: test-util\r\n` + + `Host: ${this.address}:${this.port}\r\n` + + `Connection: keep-alive\r\n${ + data + ? `Content-Type: ${contentType}\r\n` + + `Content-Length: ${data.length}\r\n` + : '' + }\r\n`) if (data) { - return Buffer.concat([buffer, data]); + return Buffer.concat([buffer, data]) } - return buffer; + return buffer } - async writeHTTPRequest(method: "GET" | "POST" | "PUT" | "DELETE", route: string, data?: Buffer, contentType?: string): Promise { - const httpRequest = this.formatHTTPRequest(method, route, data, contentType); - this.write(httpRequest); + async writeHTTPRequest(method: 'GET' | 'POST' | 'PUT' | 'DELETE', route: string, data?: Buffer, contentType?: string): Promise { + const httpRequest = this.formatHTTPRequest(method, route, data, contentType) + this.write(httpRequest) - await PromiseTimeout(20); + await PromiseTimeout(20) - const responseBuffer = this.popReceiveBuffer(); - return this.parseHTTPResponse(responseBuffer); + const responseBuffer = this.popReceiveBuffer() + return this.parseHTTPResponse(responseBuffer) } write(data: Buffer): void { if (this.encryption) { - data = hapCrypto.layerEncrypt(data, this.encryption); + data = layerEncrypt(data, this.encryption) } - expect(this.currentSocket).toBeDefined(); - this.currentSocket!.write(data); + expect(this.currentSocket).toBeDefined() + this.currentSocket!.write(data) } releaseSocket(): void { if (this.currentSocket && this.currentDataListener) { - this.currentSocket.removeListener("data", this.currentDataListener); - this.currentDataListener = undefined; + this.currentSocket.removeListener('data', this.currentDataListener) + this.currentDataListener = undefined } - expect(this.currentDataListener).toBeUndefined(); + expect(this.currentDataListener).toBeUndefined() - this.currentSocket = undefined; + this.currentSocket = undefined } async sendAddPairingRequest(identifier: HAPUsername, publicKey: Buffer, permission: PermissionTypes): Promise { - const requestTLV = tlv.encode( - TLVValues.METHOD, PairMethods.ADD_PAIRING, - TLVValues.STATE, PairingStates.M1, - TLVValues.IDENTIFIER, identifier, - TLVValues.PUBLIC_KEY, publicKey, - TLVValues.PERMISSIONS, permission, - ); - - await this.sendPairingsRequest(requestTLV); + const requestTLV = encode( + TLVValues.METHOD, + PairMethods.ADD_PAIRING, + TLVValues.STATE, + PairingStates.M1, + TLVValues.IDENTIFIER, + identifier, + TLVValues.PUBLIC_KEY, + publicKey, + TLVValues.PERMISSIONS, + permission, + ) + + await this.sendPairingsRequest(requestTLV) } async sendRemovePairingRequest(identifier: HAPUsername): Promise { - const requestTLV = tlv.encode( - TLVValues.METHOD, PairMethods.REMOVE_PAIRING, - TLVValues.STATE, PairingStates.M1, - TLVValues.IDENTIFIER, identifier, - ); - - await this.sendPairingsRequest(requestTLV); + const requestTLV = encode( + TLVValues.METHOD, + PairMethods.REMOVE_PAIRING, + TLVValues.STATE, + PairingStates.M1, + TLVValues.IDENTIFIER, + identifier, + ) + + await this.sendPairingsRequest(requestTLV) } async sendListPairingsRequest(): Promise { - const requestTLV = tlv.encode( - TLVValues.METHOD, PairMethods.LIST_PAIRINGS, - TLVValues.STATE, PairingStates.M1, - ); + const requestTLV = encode( + TLVValues.METHOD, + PairMethods.LIST_PAIRINGS, + TLVValues.STATE, + PairingStates.M1, + ) - const responseBody = await this.sendPairingsRequest(requestTLV); - const tlvDataList = tlv.decodeList(responseBody.slice(3), TLVValues.IDENTIFIER); + const responseBody = await this.sendPairingsRequest(requestTLV) + const tlvDataList = decodeList(responseBody.subarray(3), TLVValues.IDENTIFIER) - const result: PairingInformation[] = []; + const result: PairingInformation[] = [] for (const element of tlvDataList) { result.push({ username: element[TLVValues.IDENTIFIER].toString(), publicKey: element[TLVValues.PUBLIC_KEY], permission: element[TLVValues.PERMISSIONS].readUInt8(0), - }); + }) } - return result; + return result } private async sendPairingsRequest(requestTLV: Buffer): Promise { - const httpResponse = await this.writeHTTPRequest("POST", "/pairings", requestTLV, HAPMimeTypes.PAIRING_TLV8); + const httpResponse = await this.writeHTTPRequest('POST', '/pairings', requestTLV, HAPMimeTypes.PAIRING_TLV8) // `/pairings` errors are transported via the tlv8 record - expect(httpResponse.statusCode).toEqual(HAPPairingHTTPCode.OK); - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.PAIRING_TLV8); + expect(httpResponse.statusCode).toEqual(HAPPairingHTTPCode.OK) + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.PAIRING_TLV8) - const tlvData = tlv.decode(httpResponse.body); - expect(tlvData[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2); + const tlvData = decode(httpResponse.body) + expect(tlvData[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2) if (tlvData[TLVValues.ERROR_CODE]) { - throw new TLVError(tlvData[TLVValues.ERROR_CODE].readUInt8(0)); + throw new TLVError(tlvData[TLVValues.ERROR_CODE].readUInt8(0)) } // we return the raw buffer because LIST_PAIRINGS has some custom decoding strategies! - return httpResponse.body; + return httpResponse.body } public async sendAccessoriesRequest(): Promise { - const httpResponse = await this.writeHTTPRequest("GET", "/accessories"); - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON); - const jsonBody = JSON.parse(httpResponse.body.toString()); + const httpResponse = await this.writeHTTPRequest('GET', '/accessories') + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.HAP_JSON) + const jsonBody = JSON.parse(httpResponse.body.toString()) if (httpResponse.statusCode !== HAPPairingHTTPCode.OK) { - throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status); + throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status) } - return jsonBody; + return jsonBody } public async sendCharacteristicRead( @@ -228,126 +244,126 @@ export class HAPHTTPClient { includeType?: boolean, includeEvent?: boolean, ): Promise> { - assert(ids.length > 0); - let query = "?id=" + ids.map(id => id.aid + "." + id.iid).join(","); + assert(ids.length > 0) + let query = `?id=${ids.map(id => `${id.aid}.${id.iid}`).join(',')}` if (includeMeta) { - query += "&meta=" + (includeMeta ? "true" : "false"); + query += `&meta=${includeMeta ? 'true' : 'false'}` } if (includePerms) { - query += "&perms=" + (includePerms ? "1" : "0"); + query += `&perms=${includePerms ? '1' : '0'}` } if (includeType) { - query += "&type=" + (includeType ? "true" : "false"); + query += `&type=${includeType ? 'true' : 'false'}` } if (includeEvent) { - query += "&ev=" + (includeEvent ? "1" : "0"); + query += `&ev=${includeEvent ? '1' : '0'}` } - const httpResponse = await this.writeHTTPRequest("GET", "/characteristics" + query); - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON); + const httpResponse = await this.writeHTTPRequest('GET', `/characteristics${query}`) + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.HAP_JSON) - const body = JSON.parse(httpResponse.body.toString()); - if (!httpResponse.statusCode.toString().startsWith("2")) { - throw new HAPHTTPError(httpResponse.statusCode, body.status); + const body = JSON.parse(httpResponse.body.toString()) + if (!httpResponse.statusCode.toString().startsWith('2')) { + throw new HAPHTTPError(httpResponse.statusCode, body.status) } return { ...httpResponse, - body: body, - }; + body, + } } public async sendCharacteristicWrite(writeRequest: CharacteristicsWriteRequest): Promise> { - const httpResponse = await this.writeHTTPRequest("PUT", "/characteristics", Buffer.from(JSON.stringify(writeRequest)), HAPMimeTypes.HAP_JSON); + const httpResponse = await this.writeHTTPRequest('PUT', '/characteristics', Buffer.from(JSON.stringify(writeRequest)), HAPMimeTypes.HAP_JSON) if (httpResponse.statusCode !== HAPHTTPCode.NO_CONTENT) { - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON); + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.HAP_JSON) } - if (!httpResponse.statusCode.toString().startsWith("2")) { - const jsonBody = JSON.parse(httpResponse.body.toString()); - throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status); + if (!httpResponse.statusCode.toString().startsWith('2')) { + const jsonBody = JSON.parse(httpResponse.body.toString()) + throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status) } return { ...httpResponse, body: httpResponse.body.length > 0 ? JSON.parse(httpResponse.body.toString()) : undefined, - }; + } } public async sendPrepareWrite(prepareWrite: PrepareWriteRequest): Promise { - const httpResponse = await this.writeHTTPRequest("PUT", "/prepare", Buffer.from(JSON.stringify(prepareWrite)), HAPMimeTypes.HAP_JSON); - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON); + const httpResponse = await this.writeHTTPRequest('PUT', '/prepare', Buffer.from(JSON.stringify(prepareWrite)), HAPMimeTypes.HAP_JSON) + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.HAP_JSON) if (httpResponse.statusCode !== HAPHTTPCode.OK) { - const jsonBody = JSON.parse(httpResponse.body.toString()); - throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status); + const jsonBody = JSON.parse(httpResponse.body.toString()) + throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status) } } public async sendResourceRequest(resourceRequest: ResourceRequest): Promise { - const httpResponse = await this.writeHTTPRequest("POST", "/resource", Buffer.from(JSON.stringify(resourceRequest)), HAPMimeTypes.HAP_JSON); + const httpResponse = await this.writeHTTPRequest('POST', '/resource', Buffer.from(JSON.stringify(resourceRequest)), HAPMimeTypes.HAP_JSON) if (httpResponse.statusCode !== HAPHTTPCode.OK) { - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON); - const jsonBody = JSON.parse(httpResponse.body.toString()); - throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status); + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.HAP_JSON) + const jsonBody = JSON.parse(httpResponse.body.toString()) + throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status) } - expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.IMAGE_JPEG); - return httpResponse.body; + expect(httpResponse.headers['Content-Type']).toEqual(HAPMimeTypes.IMAGE_JPEG) + return httpResponse.body } parseHTTPResponse(input: Buffer): HTTPResponse { // taken and adapted from https://github.com/creationix/http-parser-js/blob/88c665381470e27cd428a728447a13dce198f782/standalone-example.js#L73 - const parser = new HTTPParser(HTTPParser.RESPONSE); - - let complete = false; - let shouldKeepAlive = false; - let upgrade = false; - let statusCode = 0; - let statusMessage = ""; - let versionMajor = 0; - let versionMinor = 0; - let headers: HeaderObject = []; - let trailers: string[] = []; - const bodyChunks: Buffer[] = []; - - parser[HTTPParser.kOnHeadersComplete] = info => { - shouldKeepAlive = info.shouldKeepAlive; - upgrade = info.upgrade; - statusCode = info.statusCode; - statusMessage = info.statusMessage; - versionMajor = info.versionMajor; - versionMinor = info.versionMinor; - headers = info.headers; - }; + const parser = new HTTPParser(HTTPParser.RESPONSE) + + let complete = false + let shouldKeepAlive = false + let upgrade = false + let statusCode = 0 + let statusMessage = '' + let versionMajor = 0 + let versionMinor = 0 + let headers: HeaderObject = [] + let trailers: string[] = [] + const bodyChunks: Buffer[] = [] + + parser[HTTPParser.kOnHeadersComplete] = (info) => { + shouldKeepAlive = info.shouldKeepAlive + upgrade = info.upgrade + statusCode = info.statusCode + statusMessage = info.statusMessage + versionMajor = info.versionMajor + versionMinor = info.versionMinor + headers = info.headers + } parser[HTTPParser.kOnBody] = (chunk, offset, length) => { - bodyChunks.push(chunk.slice(offset, offset + length)); - }; + bodyChunks.push(chunk.subarray(offset, offset + length)) + } // that's the event for trailers! - parser[HTTPParser.kOnHeaders] = t => { - trailers = t; - }; + parser[HTTPParser.kOnHeaders] = (t) => { + trailers = t + } parser[HTTPParser.kOnMessageComplete] = () => { - complete = true; - }; + complete = true + } // Since we are sending the entire Buffer at once here all callbacks above happen synchronously. // The parser does not do _anything_ asynchronous. // However, you can of course call execute() multiple times with multiple chunks, e.g. from a stream. // But then you have to refactor the entire logic to be async (e.g. resolve a Promise in kOnMessageComplete and add timeout logic). - parser.execute(input); - parser.finish(); + parser.execute(input) + parser.finish() if (!complete) { - throw new Error("Failed to parse input: " + input.toString()); + throw new Error(`Failed to parse input: ${input.toString()}`) } - const body = Buffer.concat(bodyChunks); + const body = Buffer.concat(bodyChunks) return { shouldKeepAlive, @@ -359,18 +375,18 @@ export class HAPHTTPClient { headers: this.headersArrayToObject(headers), body, trailers, - }; + } } private headersArrayToObject(headers: HeaderObject): Record { - expect(headers.length % 2).toBe(0); + expect(headers.length % 2).toBe(0) - const result: Record = {}; + const result: Record = {} for (let i = 0; i < headers.length; i += 2) { - result[headers[i]] = headers[i+1]; + result[headers[i]] = headers[i + 1] } - return result; + return result } } diff --git a/src/test-utils/HAPHTTPError.ts b/src/test-utils/HAPHTTPError.ts index ec09db309..b04f784bc 100644 --- a/src/test-utils/HAPHTTPError.ts +++ b/src/test-utils/HAPHTTPError.ts @@ -1,4 +1,4 @@ -import { HAPHTTPCode, HAPStatus } from "../lib/HAPServer"; +import type { HAPHTTPCode, HAPStatus } from '../lib/HAPServer' /** * Encapsulates a {@link HAPHTTPCode} and a {@link HAPStatus} @@ -10,15 +10,15 @@ import { HAPHTTPCode, HAPStatus } from "../lib/HAPServer"; * ``` */ export class HAPHTTPError extends Error { - public httpStatusCode: HAPHTTPCode; - public hapStatusCode: HAPStatus; + public httpStatusCode: HAPHTTPCode + public hapStatusCode: HAPStatus constructor(http: HAPHTTPCode, hap: HAPStatus) { - super("HAP HTTP Error: code: " + http + " status: " + hap); + super(`HAP HTTP Error: code: ${http} status: ${hap}`) - Object.setPrototypeOf(this, HAPHTTPError.prototype); + Object.setPrototypeOf(this, HAPHTTPError.prototype) - this.httpStatusCode = http; - this.hapStatusCode = hap; + this.httpStatusCode = http + this.hapStatusCode = hap } } diff --git a/src/test-utils/PairSetupClient.ts b/src/test-utils/PairSetupClient.ts index 8111a2760..c8958a775 100644 --- a/src/test-utils/PairSetupClient.ts +++ b/src/test-utils/PairSetupClient.ts @@ -1,222 +1,242 @@ -import axios, { AxiosResponse } from "axios"; -import { SRP, SrpClient } from "fast-srp-hap"; -import { Agent } from "http"; -import tweetnacl from "tweetnacl"; -import { PairingStates, PairMethods, TLVValues } from "../internal-types"; -import * as hapCrypto from "../lib/util/hapCrypto"; -import { EncryptedData } from "../lib/util/hapCrypto"; -import * as tlv from "../lib/util/tlv"; +import type { Agent } from 'node:http' + +import type { AxiosResponse } from 'axios' + +import type { + EncryptedData, +} from '../lib/util/hapCrypto' + +import { Buffer } from 'node:buffer' + +import axios from 'axios' +import { SRP, SrpClient } from 'fast-srp-hap' +import tweetnacl from 'tweetnacl' +import { expect } from 'vitest' + +import { PairingStates, PairMethods, TLVValues } from '../internal-types.js' +import { chacha20_poly1305_decryptAndVerify, chacha20_poly1305_encryptAndSeal, HKDF } from '../lib/util/hapCrypto.js' +import { decode, encode } from '../lib/util/tlv.js' export interface PairSetupM2 { - serverPublicKey: Buffer; - salt: Buffer; + serverPublicKey: Buffer + salt: Buffer } export interface PairSetupM3 { - srpClient: SrpClient; + srpClient: SrpClient } export interface PairSetupM4 { - sharedSecret: Buffer; + sharedSecret: Buffer } export interface PairSetupM5 { - sessionKey: Buffer; - encryptedData: EncryptedData; + sessionKey: Buffer + encryptedData: EncryptedData } export interface PairSetupM6 { - accessoryLTPK: Buffer, - accessoryIdentifier: string, + accessoryLTPK: Buffer + accessoryIdentifier: string } export interface PairSetupClientInfo { - username: string; - publicKey: Buffer; + username: string + publicKey: Buffer privateKey: Buffer } export class PairSetupClient { - private readonly port: number; - private readonly httpAgent: Agent; + private readonly port: number + private readonly httpAgent: Agent constructor(port: number, httpAgent: Agent) { - this.port = port; - this.httpAgent = httpAgent; + this.port = port + this.httpAgent = httpAgent } async sendPairSetup(pincode: string, clientInfo: PairSetupClientInfo): Promise { - const responseM1 = await this.sendM1(); + const responseM1 = await this.sendM1() - const M2 = this.parseM2(responseM1.data); + const M2 = this.parseM2(responseM1.data) - const M3 = await this.prepareM3(M2, pincode); - const responseM3 = await this.sendM3(M3); + const M3 = await this.prepareM3(M2, pincode) + const responseM3 = await this.sendM3(M3) - const M4 = this.parseM4(responseM3.data, M3); + const M4 = this.parseM4(responseM3.data, M3) - const M5 = this.prepareM5(M4, clientInfo); - const responseM5 = await this.sendM5(M5); + const M5 = this.prepareM5(M4, clientInfo) + const responseM5 = await this.sendM5(M5) - return this.parseM6(responseM5.data, M4, M5); + return this.parseM6(responseM5.data, M4, M5) } sendM1(): Promise> { return axios.post( `http://localhost:${this.port}/pair-setup`, - tlv.encode( - TLVValues.STATE, PairingStates.M1, - TLVValues.METHOD, PairMethods.PAIR_SETUP, + encode( + TLVValues.STATE, + PairingStates.M1, + TLVValues.METHOD, + PairMethods.PAIR_SETUP, ), - { httpAgent: this.httpAgent, responseType: "arraybuffer" }, - ); + { httpAgent: this.httpAgent, responseType: 'arraybuffer' }, + ) } parseM2(m1Response: Buffer): PairSetupM2 { - const objectsM2 = tlv.decode(m1Response); - expect(objectsM2[TLVValues.ERROR_CODE]).toBeUndefined(); - expect(objectsM2[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2); + const objectsM2 = decode(m1Response) + expect(objectsM2[TLVValues.ERROR_CODE]).toBeUndefined() + expect(objectsM2[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2) return { serverPublicKey: objectsM2[TLVValues.PUBLIC_KEY], salt: objectsM2[TLVValues.SALT], - }; + } } async prepareM3(m2: PairSetupM2, pincode: string): Promise { - const srpKey = await SRP.genKey(32); - const srpClient = new SrpClient(SRP.params.hap, m2.salt, Buffer.from("Pair-Setup"), Buffer.from(pincode), srpKey); - srpClient.setB(m2.serverPublicKey); + const srpKey = await SRP.genKey(32) + const srpClient = new SrpClient(SRP.params.hap, m2.salt, Buffer.from('Pair-Setup'), Buffer.from(pincode), srpKey) + srpClient.setB(m2.serverPublicKey) return { srpClient, - }; + } } sendM3(m3: PairSetupM3): Promise> { return axios.post( `http://localhost:${this.port}/pair-setup`, - tlv.encode( - TLVValues.STATE, PairingStates.M3, - TLVValues.PUBLIC_KEY, m3.srpClient.computeA(), - TLVValues.PASSWORD_PROOF, m3.srpClient.computeM1(), + encode( + TLVValues.STATE, + PairingStates.M3, + TLVValues.PUBLIC_KEY, + m3.srpClient.computeA(), + TLVValues.PASSWORD_PROOF, + m3.srpClient.computeM1(), ), - { httpAgent: this.httpAgent, responseType: "arraybuffer" }, - ); + { httpAgent: this.httpAgent, responseType: 'arraybuffer' }, + ) } parseM4(m3Response: Buffer, m3: PairSetupM3): PairSetupM4 { - const objectsM4 = tlv.decode(m3Response); - expect(objectsM4[TLVValues.ERROR_CODE]).toBeUndefined(); - expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M4); + const objectsM4 = decode(m3Response) + expect(objectsM4[TLVValues.ERROR_CODE]).toBeUndefined() + expect(objectsM4[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M4) - const serverProof = objectsM4[TLVValues.PASSWORD_PROOF]; - const encryptedDataM4 = objectsM4[TLVValues.ENCRYPTED_DATA]; + const serverProof = objectsM4[TLVValues.PASSWORD_PROOF] + const encryptedDataM4 = objectsM4[TLVValues.ENCRYPTED_DATA] - expect(() => m3.srpClient.checkM2(serverProof)).not.toThrow(); - expect(encryptedDataM4).toBeUndefined(); // we don't do MFI challenges + expect(() => m3.srpClient.checkM2(serverProof)).not.toThrow() + expect(encryptedDataM4).toBeUndefined() // we don't do MFI challenges return { sharedSecret: m3.srpClient.computeK(), - }; + } } prepareM5(m4: PairSetupM4, clientInfo: PairSetupClientInfo): PairSetupM5 { - const iOSDeviceX = hapCrypto.HKDF( - "sha512", - Buffer.from("Pair-Setup-Controller-Sign-Salt"), + const iOSDeviceX = HKDF( + 'sha512', + Buffer.from('Pair-Setup-Controller-Sign-Salt'), m4.sharedSecret, - Buffer.from("Pair-Setup-Controller-Sign-Info"), + Buffer.from('Pair-Setup-Controller-Sign-Info'), 32, - ); + ) const iOSDeviceInfo = Buffer.concat([ iOSDeviceX, Buffer.from(clientInfo.username), clientInfo.publicKey, - ]); + ]) - const iOSDeviceSignature = tweetnacl.sign.detached(iOSDeviceInfo, clientInfo.privateKey); + const iOSDeviceSignature = tweetnacl.sign.detached(iOSDeviceInfo, clientInfo.privateKey) - const subTLV_M5 = tlv.encode( - TLVValues.IDENTIFIER, clientInfo.username, - TLVValues.PUBLIC_KEY, clientInfo.publicKey, - TLVValues.SIGNATURE, iOSDeviceSignature, - ); + const subTLV_M5 = encode( + TLVValues.IDENTIFIER, + clientInfo.username, + TLVValues.PUBLIC_KEY, + clientInfo.publicKey, + TLVValues.SIGNATURE, + iOSDeviceSignature, + ) - const sessionKey = hapCrypto.HKDF( - "sha512", - Buffer.from("Pair-Setup-Encrypt-Salt"), + const sessionKey = HKDF( + 'sha512', + Buffer.from('Pair-Setup-Encrypt-Salt'), m4.sharedSecret, - Buffer.from("Pair-Setup-Encrypt-Info"), + Buffer.from('Pair-Setup-Encrypt-Info'), 32, - ); + ) - const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(sessionKey, Buffer.from("PS-Msg05"), null, subTLV_M5); + const encrypted = chacha20_poly1305_encryptAndSeal(sessionKey, Buffer.from('PS-Msg05'), null, subTLV_M5) return { sessionKey, encryptedData: encrypted, - }; + } } sendM5(m5: PairSetupM5): Promise> { return axios.post( `http://localhost:${this.port}/pair-setup`, - tlv.encode( - TLVValues.STATE, PairingStates.M5, - TLVValues.ENCRYPTED_DATA, Buffer.concat([m5.encryptedData.ciphertext, m5.encryptedData.authTag]), + encode( + TLVValues.STATE, + PairingStates.M5, + TLVValues.ENCRYPTED_DATA, + Buffer.concat([m5.encryptedData.ciphertext, m5.encryptedData.authTag]), ), - { httpAgent: this.httpAgent, responseType: "arraybuffer" }, - ); + { httpAgent: this.httpAgent, responseType: 'arraybuffer' }, + ) } parseM6(responseM5: Buffer, m4: PairSetupM4, m5: PairSetupM5): PairSetupM6 { - const objectsM6 = tlv.decode(responseM5); - expect(objectsM6[TLVValues.ERROR_CODE]).toBeUndefined(); - expect(objectsM6[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M6); + const objectsM6 = decode(responseM5) + expect(objectsM6[TLVValues.ERROR_CODE]).toBeUndefined() + expect(objectsM6[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M6) // step 1 & 2 - const encryptedDataTagM6 = objectsM6[TLVValues.ENCRYPTED_DATA]; - const encryptedDataM6 = encryptedDataTagM6.slice(0, -16); - const authTagM6 = encryptedDataTagM6.slice(-16); + const encryptedDataTagM6 = objectsM6[TLVValues.ENCRYPTED_DATA] + const encryptedDataM6 = encryptedDataTagM6.subarray(0, -16) + const authTagM6 = encryptedDataTagM6.subarray(-16) - let plaintextM6 = Buffer.alloc(0); - expect(() => plaintextM6 = hapCrypto.chacha20_poly1305_decryptAndVerify( + let plaintextM6 = Buffer.alloc(0) + expect(() => plaintextM6 = chacha20_poly1305_decryptAndVerify( m5.sessionKey, - Buffer.from("PS-Msg06"), + Buffer.from('PS-Msg06'), null, encryptedDataM6, authTagM6, - )).not.toThrow(); + )).not.toThrow() - const subTLV_M6 = tlv.decode(plaintextM6); - const accessoryIdentifier = subTLV_M6[TLVValues.IDENTIFIER]; - const accessoryLTPK = subTLV_M6[TLVValues.PUBLIC_KEY]; - const accessorySignature = subTLV_M6[TLVValues.SIGNATURE]; + const subTLV_M6 = decode(plaintextM6) + const accessoryIdentifier = subTLV_M6[TLVValues.IDENTIFIER] + const accessoryLTPK = subTLV_M6[TLVValues.PUBLIC_KEY] + const accessorySignature = subTLV_M6[TLVValues.SIGNATURE] // step 3 - const accessoryX = hapCrypto.HKDF( - "sha512", - Buffer.from("Pair-Setup-Accessory-Sign-Salt"), + const accessoryX = HKDF( + 'sha512', + Buffer.from('Pair-Setup-Accessory-Sign-Salt'), m4.sharedSecret, - Buffer.from("Pair-Setup-Accessory-Sign-Info"), + Buffer.from('Pair-Setup-Accessory-Sign-Info'), 32, - ); + ) const accessoryInfo = Buffer.concat([ accessoryX, accessoryIdentifier, accessoryLTPK, - ]); + ]) expect( tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature, accessoryLTPK), - ).toEqual(true); + ).toEqual(true) return { accessoryLTPK, accessoryIdentifier: accessoryIdentifier.toString(), - }; + } } } diff --git a/src/test-utils/PairVerifyClient.ts b/src/test-utils/PairVerifyClient.ts index 1238a2be2..d55687eb5 100644 --- a/src/test-utils/PairVerifyClient.ts +++ b/src/test-utils/PairVerifyClient.ts @@ -1,65 +1,82 @@ -import axios, { AxiosResponse } from "axios"; -import { Agent } from "http"; -import tweetnacl, { BoxKeyPair } from "tweetnacl"; -import { PairingStates, TLVValues } from "../internal-types"; -import { HAPEncryption } from "../lib/util/eventedhttp"; -import * as hapCrypto from "../lib/util/hapCrypto"; -import { EncryptedData } from "../lib/util/hapCrypto"; -import * as tlv from "../lib/util/tlv"; +import type { Agent } from 'node:http' + +import type { AxiosResponse } from 'axios' +import type { BoxKeyPair } from 'tweetnacl' + +import type { + EncryptedData, +} from '../lib/util/hapCrypto' + +import { Buffer } from 'node:buffer' + +import axios from 'axios' +import tweetnacl from 'tweetnacl' +import { expect } from 'vitest' + +import { PairingStates, TLVValues } from '../internal-types.js' +import { HAPEncryption } from '../lib/util/eventedhttp.js' +import { + chacha20_poly1305_decryptAndVerify, + chacha20_poly1305_encryptAndSeal, + generateCurve25519KeyPair, + generateCurve25519SharedSecKey, + HKDF, +} from '../lib/util/hapCrypto.js' +import { decode, encode } from '../lib/util/tlv.js' export interface PairVerifyM2 { - sharedSecret: Buffer, - sessionKey: Buffer, - serverEphemeralPublicKey: Buffer, + sharedSecret: Buffer + sessionKey: Buffer + serverEphemeralPublicKey: Buffer } export interface PairVerifyM3 { - encryptedData: EncryptedData; + encryptedData: EncryptedData } export interface PairVerifyM4 { - accessoryToControllerKey: Buffer, - controllerToAccessoryKey: Buffer, + accessoryToControllerKey: Buffer + controllerToAccessoryKey: Buffer } export interface PairVerifyServerInfo { - username: string; - publicKey: Buffer; + username: string + publicKey: Buffer } export interface PairVerifyClientInfo { - username: string; - privateKey: Buffer; + username: string + privateKey: Buffer } export class PairVerifyClient { - private readonly port: number; - private readonly httpAgent: Agent; + private readonly port: number + private readonly httpAgent: Agent - readonly ephemeralKeyPair: BoxKeyPair; + readonly ephemeralKeyPair: BoxKeyPair constructor(port: number, httpAgent: Agent, ephemeralKeyPair?: BoxKeyPair) { - this.port = port; - this.httpAgent = httpAgent; + this.port = port + this.httpAgent = httpAgent - this.ephemeralKeyPair = ephemeralKeyPair ?? hapCrypto.generateCurve25519KeyPair(); + this.ephemeralKeyPair = ephemeralKeyPair ?? generateCurve25519KeyPair() } async sendPairVerify(serverInfo: PairVerifyServerInfo, clientInfo: PairVerifyClientInfo & { publicKey: Buffer }): Promise { // M1 - const responseM1 = await this.sendM1(); + const responseM1 = await this.sendM1() // M2 - const M2 = this.parseM2(responseM1.data, serverInfo); + const M2 = this.parseM2(responseM1.data, serverInfo) // M3 - const M3 = this.prepareM3(M2, clientInfo); + const M3 = this.prepareM3(M2, clientInfo) // step 11 & 12 - const responseM3 = await this.sendM3(M3); + const responseM3 = await this.sendM3(M3) // M4 - const M4 = this.parseM4(responseM3.data, M2); + const M4 = this.parseM4(responseM3.data, M2) // verify that encryption works! const encryption = new HAPEncryption( @@ -68,84 +85,86 @@ export class PairVerifyClient { clientInfo.publicKey, M2.sharedSecret, M2.sessionKey, - ); + ) // our HAPCrypto is engineered for the server side, so we have to switch the keys here (deliberately wrongfully) // such that hapCrypto uses the controllerToAccessoryKey for encryption! - encryption.accessoryToControllerKey = M4.controllerToAccessoryKey; - encryption.controllerToAccessoryKey = M4.accessoryToControllerKey; + encryption.accessoryToControllerKey = M4.controllerToAccessoryKey + encryption.controllerToAccessoryKey = M4.accessoryToControllerKey - return encryption; + return encryption } sendM1(): Promise> { return axios.post( `http://localhost:${this.port}/pair-verify`, - tlv.encode( - TLVValues.STATE, PairingStates.M1, - TLVValues.PUBLIC_KEY, this.ephemeralKeyPair.publicKey, + encode( + TLVValues.STATE, + PairingStates.M1, + TLVValues.PUBLIC_KEY, + this.ephemeralKeyPair.publicKey, ), - { httpAgent: this.httpAgent, responseType: "arraybuffer" }, - ); + { httpAgent: this.httpAgent, responseType: 'arraybuffer' }, + ) } parseM2(responseM1: Buffer, serverInfo: PairVerifyServerInfo): PairVerifyM2 { - const objectsM2 = tlv.decode(responseM1); - expect(objectsM2[TLVValues.ERROR_CODE]).toBeUndefined(); - expect(objectsM2[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M2); + const objectsM2 = decode(responseM1) + expect(objectsM2[TLVValues.ERROR_CODE]).toBeUndefined() + expect(objectsM2[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M2) - const serverPublicKey_M2 = objectsM2[TLVValues.PUBLIC_KEY]; - const encryptedDataM2 = objectsM2[TLVValues.ENCRYPTED_DATA]; + const serverPublicKey_M2 = objectsM2[TLVValues.PUBLIC_KEY] + const encryptedDataM2 = objectsM2[TLVValues.ENCRYPTED_DATA] // step 1 - const sharedSecret = Buffer.from(hapCrypto.generateCurve25519SharedSecKey( + const sharedSecret = Buffer.from(generateCurve25519SharedSecKey( this.ephemeralKeyPair.secretKey, serverPublicKey_M2, - )); + )) // step 2 - const sessionKey = hapCrypto.HKDF( - "sha512", - Buffer.from("Pair-Verify-Encrypt-Salt"), + const sessionKey = HKDF( + 'sha512', + Buffer.from('Pair-Verify-Encrypt-Salt'), sharedSecret, - Buffer.from("Pair-Verify-Encrypt-Info"), + Buffer.from('Pair-Verify-Encrypt-Info'), 32, - ); + ) // step 3 & 4 - const cipherTextM2 = encryptedDataM2.slice(0, -16); - const authTagM2 = encryptedDataM2.slice(-16); + const cipherTextM2 = encryptedDataM2.subarray(0, -16) + const authTagM2 = encryptedDataM2.subarray(-16) - let plaintextM2 = Buffer.alloc(0); - expect(() => plaintextM2 = hapCrypto.chacha20_poly1305_decryptAndVerify( + let plaintextM2 = Buffer.alloc(0) + expect(() => plaintextM2 = chacha20_poly1305_decryptAndVerify( sessionKey, - Buffer.from("PV-Msg02"), + Buffer.from('PV-Msg02'), null, cipherTextM2, authTagM2, - )).not.toThrow(); + )).not.toThrow() // step 5 - const dataM2 = tlv.decode(plaintextM2); - const accessoryIdentifier = dataM2[TLVValues.IDENTIFIER]; - const accessorySignature = dataM2[TLVValues.SIGNATURE]; - expect(accessoryIdentifier.toString()).toEqual(serverInfo.username); + const dataM2 = decode(plaintextM2) + const accessoryIdentifier = dataM2[TLVValues.IDENTIFIER] + const accessorySignature = dataM2[TLVValues.SIGNATURE] + expect(accessoryIdentifier.toString()).toEqual(serverInfo.username) // step 6 const accessoryInfo = Buffer.concat([ serverPublicKey_M2, accessoryIdentifier, this.ephemeralKeyPair.publicKey, - ]); + ]) expect( tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature, serverInfo.publicKey), - ).toBeTruthy(); + ).toBeTruthy() return { sharedSecret, sessionKey, serverEphemeralPublicKey: serverPublicKey_M2, - }; + } } prepareM3(m2: PairVerifyM2, clientInfo: PairVerifyClientInfo): PairVerifyM3 { @@ -154,67 +173,71 @@ export class PairVerifyClient { this.ephemeralKeyPair.publicKey, Buffer.from(clientInfo.username), m2.serverEphemeralPublicKey, - ]); + ]) // step 8 const iOSDeviceSignature = Buffer.from( tweetnacl.sign.detached(iOSDeviceInfo, clientInfo.privateKey), - ); + ) // step 9 - const plainTextTLV_M3 = tlv.encode( - TLVValues.IDENTIFIER, clientInfo.username, - TLVValues.SIGNATURE, iOSDeviceSignature, - ); + const plainTextTLV_M3 = encode( + TLVValues.IDENTIFIER, + clientInfo.username, + TLVValues.SIGNATURE, + iOSDeviceSignature, + ) // step 10 - const encrypted_M3 = hapCrypto.chacha20_poly1305_encryptAndSeal( + const encrypted_M3 = chacha20_poly1305_encryptAndSeal( m2.sessionKey, - Buffer.from("PV-Msg03"), + Buffer.from('PV-Msg03'), null, plainTextTLV_M3, - ); + ) return { encryptedData: encrypted_M3, - }; + } } sendM3(m3: PairVerifyM3): Promise> { return axios.post( `http://localhost:${this.port}/pair-verify`, - tlv.encode( - TLVValues.STATE, PairingStates.M3, - TLVValues.ENCRYPTED_DATA, Buffer.concat([m3.encryptedData.ciphertext, m3.encryptedData.authTag]), + encode( + TLVValues.STATE, + PairingStates.M3, + TLVValues.ENCRYPTED_DATA, + Buffer.concat([m3.encryptedData.ciphertext, m3.encryptedData.authTag]), ), - { httpAgent: this.httpAgent, responseType: "arraybuffer" }, - ); + { httpAgent: this.httpAgent, responseType: 'arraybuffer' }, + ) } parseM4(responseM3: Buffer, m2: PairVerifyM2): PairVerifyM4 { - const objectsM4 = tlv.decode(responseM3); - expect(objectsM4[TLVValues.ERROR_CODE]).toBeUndefined(); - expect(objectsM4[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M4); + const objectsM4 = decode(responseM3) + expect(objectsM4[TLVValues.ERROR_CODE]).toBeUndefined() + expect(objectsM4[TLVValues.STATE].readUInt8(0)).toBe(PairingStates.M4) - const salt = Buffer.from("Control-Salt"); - const accessoryToControllerKey = hapCrypto.HKDF( - "sha512", + const salt = Buffer.from('Control-Salt') + const accessoryToControllerKey = HKDF( + 'sha512', salt, m2.sharedSecret, - Buffer.from("Control-Read-Encryption-Key"), + Buffer.from('Control-Read-Encryption-Key'), 32, - ); - const controllerToAccessoryKey = hapCrypto.HKDF( - "sha512", + ) + const controllerToAccessoryKey = HKDF( + 'sha512', salt, m2.sharedSecret, - Buffer.from("Control-Write-Encryption-Key"), + Buffer.from('Control-Write-Encryption-Key'), 32, - ); + ) return { accessoryToControllerKey, controllerToAccessoryKey, - }; + } } } diff --git a/src/test-utils/tlvError.ts b/src/test-utils/tlvError.ts index 31cce4094..0cd9f02dd 100644 --- a/src/test-utils/tlvError.ts +++ b/src/test-utils/tlvError.ts @@ -1,4 +1,4 @@ -import { TLVErrorCode } from "../lib/HAPServer"; +import type { TLVErrorCode } from '../lib/HAPServer' /** * Encapsulates a {@link TLVErrorCode} in an error object. @@ -9,13 +9,13 @@ import { TLVErrorCode } from "../lib/HAPServer"; * ``` */ export class TLVError extends Error { - public errorCode: TLVErrorCode; + public errorCode: TLVErrorCode constructor(errorCode: TLVErrorCode) { - super("TLV Error Code: " + errorCode); + super(`TLV Error Code: ${errorCode}`) - Object.setPrototypeOf(this, TLVError.prototype); + Object.setPrototypeOf(this, TLVError.prototype) - this.errorCode = errorCode; + this.errorCode = errorCode } } diff --git a/src/types.ts b/src/types.ts index 179a1cfd0..6af05dbf8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,16 @@ -import { Formats, Perms, Units } from "./lib/Characteristic"; -import { ResourceRequestReason } from "./lib/controller"; -import { HAPStatus } from "./lib/HAPServer"; +import type { Formats, Perms, Units } from './lib/Characteristic' +import type { ResourceRequestReason } from './lib/controller' +import type { HAPStatus } from './lib/HAPServer' /** * @group Utils */ -export type Nullable = T | null; +export type Nullable = T | null /** * @group Utils */ -export type WithUUID = T & { UUID: string }; +export type WithUUID = T & { UUID: string } /** * Like TypeScripts `Partial` but allowing null as a value. @@ -26,217 +26,216 @@ export type PartialAllowingNull = { * * @group Utils */ -export type ConstructorArgs = C extends new (...args: infer A) => any ? A : never; // eslint-disable-line @typescript-eslint/no-explicit-any +export type ConstructorArgs = C extends new (...args: infer A) => any ? A : never /** * UUID string uniquely identifying every HAP connection. * * @group HAP Accessory Server */ -export type SessionIdentifier = string; +export type SessionIdentifier = string /** * Defines a mac address. * Must have a format like 'XX:XX:XX:XX:XX:XX' with XX being a valid hexadecimal string * * @group Utils */ -export type MacAddress = string; +export type MacAddress = string /** * Defines a pincode for the HAP accessory. * Must have a format like "XXX-XX-XXX". * * @group Accessory */ -export type HAPPincode = string; +export type HAPPincode = string /** * @group Utils */ -export type InterfaceName = string; +export type InterfaceName = string /** * @group Utils */ -export type IPv4Address = string; +export type IPv4Address = string /** * @group Utils */ -export type IPv6Address = string; +export type IPv6Address = string /** * @group Utils */ -export type IPAddress = IPv4Address | IPv6Address; +export type IPAddress = IPv4Address | IPv6Address /** * @group Utils */ -export type NodeCallback = (err: Nullable | undefined, data?: T) => void; +export type NodeCallback = (err: Nullable | undefined, data?: T) => void /** * @group Utils */ -export type VoidCallback = (err?: Nullable) => void; +export type VoidCallback = (err?: Nullable) => void /** * @group Utils */ -export type PrimitiveTypes = string | number | boolean; +export type PrimitiveTypes = string | number | boolean /** * @group Characteristic */ -export type CharacteristicValue = PrimitiveTypes | PrimitiveTypes[] | { [key: string]: PrimitiveTypes }; - +export type CharacteristicValue = PrimitiveTypes | PrimitiveTypes[] | { [key: string]: PrimitiveTypes } /** * @group HAP Accessory Server */ export interface CharacteristicJsonObject { - type: string, // uuid or short uuid - iid: number, - value?: Nullable, // undefined for non-readable characteristics - - perms: Perms[], - format: Formats | string, - - description?: string, - - unit?: Units | string, - minValue?: number, - maxValue?: number, - minStep?: number, - maxLen?: number, - maxDataLen?: number, - "valid-values"?: number[], - "valid-values-range"?: [min: number, max: number], + 'type': string // uuid or short uuid + 'iid': number + 'value'?: Nullable // undefined for non-readable characteristics + + 'perms': Perms[] + 'format': Formats | string + + 'description'?: string + + 'unit'?: Units | string + 'minValue'?: number + 'maxValue'?: number + 'minStep'?: number + 'maxLen'?: number + 'maxDataLen'?: number + 'valid-values'?: number[] + 'valid-values-range'?: [min: number, max: number] } /** * @group HAP Accessory Server */ export interface ServiceJsonObject { - type: string, - iid: number, - characteristics: CharacteristicJsonObject[], // must not be empty, max 100 characteristics - hidden?: boolean, - primary?: boolean, - linked?: number[], // iid array + type: string + iid: number + characteristics: CharacteristicJsonObject[] // must not be empty, max 100 characteristics + hidden?: boolean + primary?: boolean + linked?: number[] // iid array } /** * @group HAP Accessory Server */ export interface AccessoryJsonObject { - aid: number, - services: ServiceJsonObject[], // must not be empty, max 100 services + aid: number + services: ServiceJsonObject[] // must not be empty, max 100 services } /** * @group HAP Accessory Server */ export interface AccessoriesResponse { - accessories: AccessoryJsonObject[], + accessories: AccessoryJsonObject[] } /** * @group HAP Accessory Server */ export interface CharacteristicId { - aid: number, - iid: number, + aid: number + iid: number } /** * @group HAP Accessory Server */ export interface CharacteristicsReadRequest { - ids: CharacteristicId[], - includeMeta: boolean; - includePerms: boolean, - includeType: boolean, - includeEvent: boolean, + ids: CharacteristicId[] + includeMeta: boolean + includePerms: boolean + includeType: boolean + includeEvent: boolean } /** * @group HAP Accessory Server */ export interface PartialCharacteristicReadDataValue { - value: CharacteristicValue | null, + value: CharacteristicValue | null - status?: HAPStatus.SUCCESS, + status?: HAPStatus.SUCCESS // type - type?: string, // characteristics uuid + type?: string // characteristics uuid // metadata - format?: string, - unit?: string, - minValue?: number, - maxValue?: number, - minStep?: number, - maxLen?: number, + format?: string + unit?: string + minValue?: number + maxValue?: number + minStep?: number + maxLen?: number // perms - perms?: Perms[], + perms?: Perms[] // event - ev?: boolean, + ev?: boolean } /** * @group HAP Accessory Server */ export interface PartialCharacteristicReadError { - status: HAPStatus, + status: HAPStatus } /** * @group HAP Accessory Server */ export interface CharacteristicReadDataValue extends PartialCharacteristicReadDataValue { - aid: number, - iid: number, + aid: number + iid: number } /** * @group HAP Accessory Server */ export interface CharacteristicReadError extends PartialCharacteristicReadError { - aid: number, - iid: number, + aid: number + iid: number } /** * @group HAP Accessory Server */ -export type PartialCharacteristicReadData = PartialCharacteristicReadDataValue | PartialCharacteristicReadError; +export type PartialCharacteristicReadData = PartialCharacteristicReadDataValue | PartialCharacteristicReadError /** * @group HAP Accessory Server */ -export type CharacteristicReadData = CharacteristicReadDataValue | CharacteristicReadError; +export type CharacteristicReadData = CharacteristicReadDataValue | CharacteristicReadError /** * @group HAP Accessory Server */ export interface CharacteristicsReadResponse { - characteristics: CharacteristicReadData[], + characteristics: CharacteristicReadData[] } /** * @group HAP Accessory Server */ export interface CharacteristicWrite { - aid: number, - iid: number, + aid: number + iid: number - value?: CharacteristicValue, - ev?: boolean, // enable/disable event notifications for the accessory + value?: CharacteristicValue + ev?: boolean // enable/disable event notifications for the accessory - authData?: string, // base64 encoded string used for custom authorisation - remote?: boolean, // remote access used - r?: boolean, // write response + authData?: string // base64 encoded string used for custom authorisation + remote?: boolean // remote access used + r?: boolean // write response } /** * @group HAP Accessory Server */ export interface CharacteristicsWriteRequest { - characteristics: CharacteristicWrite[], + characteristics: CharacteristicWrite[] pid?: number } @@ -244,74 +243,75 @@ export interface CharacteristicsWriteRequest { * @group HAP Accessory Server */ export interface PartialCharacteristicWriteDataValue { - value?: CharacteristicValue | null, + value?: CharacteristicValue | null - status: HAPStatus.SUCCESS, + status: HAPStatus.SUCCESS } /** * @group HAP Accessory Server */ export interface PartialCharacteristicWriteError { - status: HAPStatus, + status: HAPStatus - value?: undefined, // defined to make things easier + value?: undefined // defined to make things easier } /** * @group HAP Accessory Server */ export interface CharacteristicWriteDataValue extends PartialCharacteristicWriteDataValue { - aid: number, - iid: number, + aid: number + iid: number } /** * @group HAP Accessory Server */ export interface CharacteristicWriteError extends PartialCharacteristicWriteError { - aid: number, - iid: number, + aid: number + iid: number } /** * @group HAP Accessory Server */ -export type PartialCharacteristicWriteData = PartialCharacteristicWriteDataValue | PartialCharacteristicWriteError; +export type PartialCharacteristicWriteData = PartialCharacteristicWriteDataValue | PartialCharacteristicWriteError /** * @group HAP Accessory Server */ -export type CharacteristicWriteData = CharacteristicWriteDataValue | CharacteristicWriteError; +export type CharacteristicWriteData = CharacteristicWriteDataValue | CharacteristicWriteError /** * @group HAP Accessory Server */ export interface CharacteristicsWriteResponse { - characteristics: CharacteristicWriteData[], + characteristics: CharacteristicWriteData[] } /** * @group HAP Accessory Server */ -export type PrepareWriteRequest = { - ttl: number, +export interface PrepareWriteRequest { + ttl: number pid: number } /** * @group HAP Accessory Server */ +// eslint-disable-next-line no-restricted-syntax export const enum ResourceRequestType { - IMAGE = "image", + IMAGE = 'image', } /** * @group HAP Accessory Server */ export interface ResourceRequest { - aid?: number; - "image-height": number; - "image-width": number; - "reason"?: ResourceRequestReason; - "resource-type": ResourceRequestType; + 'aid'?: number + 'image-height': number + 'image-width': number + 'reason'?: ResourceRequestReason + 'resource-type': ResourceRequestType } diff --git a/src/types/dbus-native.d.ts b/src/types/dbus-native.d.ts index e44b8f033..7b5c664f6 100644 --- a/src/types/dbus-native.d.ts +++ b/src/types/dbus-native.d.ts @@ -1,42 +1,40 @@ -declare module "@homebridge/dbus-native" { - import { EventEmitter } from "events"; - import { Socket } from "net"; +declare module '@homebridge/dbus-native' { + import type { Socket } from 'node:net' - function systemBus(): MessageBus; + import { EventEmitter } from 'node:events' + + function systemBus(): MessageBus export class MessageBus { - connection: BusConnection; + connection: BusConnection - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any - public invoke(message: any, callback: (error: { name: string, message: any } | undefined, value: any) => void): void; + public invoke(message: any, callback: (error: { name: string, message: any } | undefined, value: any) => void): void - public getService(name: string): DBusService; + public getService(name: string): DBusService } export class BusConnection extends EventEmitter { - public stream: Socket; + public stream: Socket } export class DBusService { - public name: string; - public bus: MessageBus; + public name: string + public bus: MessageBus // the dbus object has additional properties `proxy` and `nodes´ added to it! - public getObject(name: string, callback: (error: null | Error, obj?: DBusObject) => void): DBusObject; - public getInterface(objName: string, ifaceName: string, callback: (error: null | Error, iface?: DBusInterface) => void): void; + public getObject(name: string, callback: (error: null | Error, obj?: DBusObject) => void): DBusObject + public getInterface(objName: string, ifaceName: string, callback: (error: null | Error, iface?: DBusInterface) => void): void } export class DBusObject { - public name: string; - public service: DBusService; + public name: string + public service: DBusService - public as(name: string): DBusInterface; + public as(name: string): DBusInterface } - // eslint-disable-next-line @typescript-eslint/no-explicit-any export class DBusInterface extends EventEmitter implements Record { - public $parent: DBusObject; - public $name: string; // string interface name - + public $parent: DBusObject + public $name: string // string interface name } } diff --git a/src/types/node-persist.d.ts b/src/types/node-persist.d.ts index 406accc32..a000dd210 100644 --- a/src/types/node-persist.d.ts +++ b/src/types/node-persist.d.ts @@ -1,36 +1,30 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -declare module "node-persist" { - +declare module 'node-persist' { export interface InitOptions { - dir?: string; // default 'persist' - stringify?: typeof JSON.stringify, // default JSON.stringify - parse?: typeof JSON.parse, // default JSON.parse - encoding?: string, // default 'utf8' - logging?: boolean, - continuous?: boolean, // default true (instantly persists to disk) - interval?: false | number, // milliseconds - ttl?: false | true | number, // can be true for 24h default or a number in MILLISECONDS + dir?: string // default 'persist' + stringify?: typeof JSON.stringify // default JSON.stringify + parse?: typeof JSON.parse // default JSON.parse + encoding?: string // default 'utf8' + logging?: boolean + continuous?: boolean // default true (instantly persists to disk) + interval?: false | number // milliseconds + ttl?: false | true | number // can be true for 24h default or a number in MILLISECONDS } export class LocalStorage { + constructor(options?: InitOptions) - constructor(options?: InitOptions); - - initSync(options?: InitOptions): void; - getItem(key: string): any; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - setItemSync(key: string, value: any): void; + initSync(options?: InitOptions): void + getItem(key: string): any + setItemSync(key: string, value: any): void removeItemSync(key: string): void - persistSync(): void; - + persistSync(): void } - export function initSync(options?: InitOptions): void; - export function create(options?: InitOptions): LocalStorage; - export function getItem(key: string): any; - export function setItemSync(key: string, data: any): void; - export function persistSync(): void; - export function removeItemSync(key: string): void; + export function initSync(options?: InitOptions): void + export function create(options?: InitOptions): LocalStorage + export function getItem(key: string): any + export function setItemSync(key: string, data: any): void + export function persistSync(): void + export function removeItemSync(key: string): void } diff --git a/tsconfig.json b/tsconfig.json index e6efcbb73..f4972b78e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,23 @@ { "compilerOptions": { - "allowSyntheticDefaultImports": true, + "target": "ESNext", + "lib": [ + "ESNext" + ], + "rootDir": "./src", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "useUnknownInCatchVariables": false, "declaration": true, "declarationMap": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, "importHelpers": true, - "lib": ["ES2022"], - "module": "CommonJS", - "moduleResolution": "node", - "preserveConstEnums": true, // do not remove this option! "outDir": "./dist", - "rootDir": "./src", - "sourceMap": true, - "strict": true, - "target": "ES2022", - "useUnknownInCatchVariables": false + "preserveConstEnums": true, + "sourceMap": true, // do not remove this option! + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true }, "include": [ "src/" diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000..1ce24f9ca --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + include: ['src/**'], + }, + pool: 'threads', + testTimeout: 10000, + }, +})