From 757b69897063aa50c9fdd54149970c6a6686e6db Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:49:41 -0500 Subject: [PATCH] Updates to support homebridge-automation (#14) --- CHANGELOG.md | 13 +++ README.md | 36 ++++++ package-lock.json | 291 +++++++++++++++++++++++++--------------------- package.json | 19 ++- src/index.ts | 197 ++++++++++++++++++++++--------- src/interfaces.ts | 10 ++ src/monitor.ts | 52 ++++++++- 7 files changed, 414 insertions(+), 204 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0199631..2340ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `@homebridge/hap-client` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/). +## v2.0.5 (2024-12-04) + +### Changed + +- updated dependencies +- Added refresh of 'values' field when refreshServiceCharacteristics or getCharacteristic is used +- Added new setCharacteristic option, setCharacteristicByType which finds characteristic by type for setting. +- Added new getResource request to retrieve snapshot images from camera's +- Minor tweak to serviceName, and if the name is blank, use the name value from Accessory Information +- Fixed issue of error handler triggering an error when attempting to connect to a homebridge instance that is down +- Added restart of monitor when client connections close +- Added console logging if a logger is not provided + ## v2.0.4 (2024-11-07) ### Changed diff --git a/README.md b/README.md index 678aa38..76db8d4 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,46 @@ A client for an insecure HAP-NodeJS instance. Provides a Typescript based interface based on the homekit accessory protocol, allowing the creation of clients able to connect to and control Homebridge devices. +# API + +``` +const { HapClient } = require('@homebridge/hap-client'); + +this.hapClient = new HapClient({ + config: { debug: true }, + pin: config.username, + logger: this.log, +}); + +this.monitor = await this.hapClient.monitorCharacteristics(services?: ServiceType[]); // Creates event monitors for all event capabable Homebridge services. If a list of services is, this list is used rather than all +``` + +## hap-client Events + +``` +this.hapClient.on('instance-discovered', this.instanceDiscovered(instance: HapInstance)); // Emitted during discovery for each HB instance discovered + +this.hapClient.on('instance-configuration-changed', this.instanceChanged(instance: HapInstance)); // Emitted during discovery for each HB instance change + +this.hapClient.on('discovery-terminated', this.discoveryTerminated()); // Instance discovery was terminated + +this.hapClient.on('discovery-ended', this.discoveryEnded()); // Emitted when discovery has ended ( 60 Seconds ) + +this.monitor.on('service-update', this.serviceUpdate(services)); // Emitted when a characteristic change is received from a homebridge service + +this.monitor.on('monitor-close', this.monitorClose(instance, hadError)); // Emitted when the connection to a homebridge service is closed ( likely a restart ) + +this.monitor.on('monitor-error', this.monitorError(instance, error)); // Emitted when the connection to a homebridge service has an error ( likely a restart ) + +this.monitor.on('monitor-refresh', this.monitorRefresh(instance, error)); // Emitted when the connection to a homebridge instance has been refreshed ( Triggered when an instance is discovered and its port, configuration number or name has changed) +``` + + # Dependant Applications - homebridge-config-ui-x - homebridge-gsh +- node-red-contrib-homebridge-automation - [NPM Dependants](https://www.npmjs.com/package/@homebridge/hap-client?activeTab=dependents) diff --git a/package-lock.json b/package-lock.json index 9712c67..5fc587c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,30 @@ { "name": "@homebridge/hap-client", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@homebridge/hap-client", - "version": "2.0.4", + "version": "2.0.5", "license": "MIT", "dependencies": { - "axios": "1.7.6", - "bonjour-service": "1.2.1", + "axios": "1.7.9", + "bonjour-service": "1.3.0", "decamelize": "5.0.1", "inflection": "3.0.0", "source-map-support": "0.5.21" }, "devDependencies": { - "@types/node": "^22.5.1", + "@types/node": "^22.10.1", "@types/source-map-support": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.8.1", + "eslint-plugin-jest": "^28.9.0", "hap-nodejs": "^1.1.0", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.3" } }, "node_modules/@cspotcode/source-map-support": { @@ -41,25 +41,28 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -115,9 +118,9 @@ } }, "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==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -180,14 +183,14 @@ } }, "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==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -342,15 +345,22 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", - "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/source-map-support": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", @@ -362,17 +372,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz", + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/type-utils": "8.3.0", - "@typescript-eslint/utils": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/type-utils": "8.15.0", + "@typescript-eslint/utils": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -396,16 +406,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", - "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4" }, "engines": { @@ -425,14 +435,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "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==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -443,14 +453,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "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==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz", + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/typescript-estree": "8.15.0", + "@typescript-eslint/utils": "8.15.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -461,6 +471,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -468,9 +481,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", "dev": true, "license": "MIT", "engines": { @@ -482,14 +495,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/visitor-keys": "8.15.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -511,16 +524,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "8.15.0", + "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/typescript-estree": "8.15.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -531,17 +544,22 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.15.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -551,6 +569,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -559,9 +590,9 @@ "license": "ISC" }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -582,9 +613,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", "dependencies": { @@ -698,9 +729,9 @@ } }, "node_modules/axios": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.6.tgz", - "integrity": "sha512-Ekur6XDwhnJ5RgOCaxFnXyqlPALI3rVeukZMwOdfghW7/wGz784BYKiQq+QD8NPcr91KRo30KfHOchyijwWw7g==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -729,9 +760,9 @@ } }, "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -861,9 +892,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -876,13 +907,13 @@ } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1090,17 +1121,18 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "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/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1146,9 +1178,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.1.tgz", - "integrity": "sha512-G46XMyYu6PtSNJUkQ0hsPjzXYpzq/O4vpCciMizTKRJG8kNsRreGoMRDG6H9FIB/xVgfFuclVnuX4XRvFUzrZQ==", + "version": "28.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz", + "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1434,16 +1466,16 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -1471,9 +1503,9 @@ } }, "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==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2313,9 +2345,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -2358,9 +2390,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", "engines": { @@ -2615,16 +2647,16 @@ "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2941,9 +2973,9 @@ } }, "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==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", "dev": true, "license": "MIT", "engines": { @@ -2998,9 +3030,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, @@ -3038,9 +3070,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3051,13 +3083,6 @@ "node": ">=14.17" } }, - "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/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 952cd71..4d9390d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@homebridge/hap-client", - "version": "2.0.4", + "version": "2.0.5", "description": "A client for HAP-NodeJS.", "main": "./dist/index.js", "scripts": { @@ -23,8 +23,7 @@ "api" ], "author": { - "name": "oznu", - "email": "dev@oz.nu" + "name": "Homebridge" }, "contributors": [ { @@ -38,21 +37,21 @@ }, "homepage": "https://github.com/homebridge/hap-client/blob/latest#readme", "dependencies": { - "axios": "1.7.6", - "bonjour-service": "1.2.1", + "axios": "1.7.9", + "bonjour-service": "1.3.0", "decamelize": "5.0.1", "inflection": "3.0.0", "source-map-support": "0.5.21" }, "devDependencies": { - "@types/node": "^22.5.1", + "@types/node": "^22.10.1", "@types/source-map-support": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.8.1", + "eslint-plugin-jest": "^28.9.0", "hap-nodejs": "^1.1.0", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.3" } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7c81742..2b71e34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,15 +2,15 @@ import { createHash } from 'node:crypto'; import { EventEmitter } from 'node:events'; import axios from 'axios'; +import Bonjour, { Browser, Service } from 'bonjour-service'; import * as decamelize from 'decamelize'; import { titleize } from 'inflection'; -import Bonjour, { Browser, Service } from 'bonjour-service' -import { Services, Characteristics } from './hap-types'; -import { toLongFormUUID } from './uuid'; -import { HapMonitor } from './monitor'; -import { HapAccessoriesRespType, ServiceType, CharacteristicType, HapInstance, HapCharacteristicRespType, AccessoryInformationProperties } from './interfaces'; import 'source-map-support/register'; +import { Characteristics, Services } from './hap-types'; +import { AccessoryInformationProperties, CharacteristicType, HapAccessoriesRespType, HapCharacteristicRespType, HapInstance, ResourceRequestType, ServiceType } from './interfaces'; +import { HapMonitor } from './monitor'; +import { toLongFormUUID } from './uuid'; export * from './interfaces'; @@ -19,9 +19,9 @@ export class HapClient extends EventEmitter { private browser: Browser; private discoveryInProgress = false; - private logger; + private logger: any; private pin: string; - private debugEnabled: boolean; + private debugEnabled: boolean = false; private config: { debug?: boolean; instanceBlacklist?: string[]; @@ -29,13 +29,8 @@ export class HapClient extends EventEmitter { private instances: HapInstance[] = []; - private hiddenServices = [ - Services.AccessoryInformation, - ]; - - private hiddenCharacteristics = [ - Characteristics.Name, - ]; + private hiddenServices = [Services.AccessoryInformation]; + private hiddenCharacteristics = [Characteristics.Name]; private resetInstancePoolTimeout: NodeJS.Timeout | undefined = undefined; private startDiscoveryTimeout: NodeJS.Timeout | undefined = undefined; @@ -47,32 +42,55 @@ export class HapClient extends EventEmitter { config: any; }) { super(); - this.pin = opts.pin; - this.logger = opts.logger; - this.debugEnabled = opts.config.debug; + this.logger = opts.logger || console; // Fallback to console if no logger is provided + this.debugEnabled = !!opts.config.debug; this.config = opts.config; this.startDiscovery(); } - debug(msg) { - if (this.debugEnabled) { - this.logger.log(msg); + /** + * Unified logging method. + */ + private logMessage(level: 'debug' | 'info' | 'warn' | 'error', msg: string, includeStack = false) { + if (!this.logger || typeof this.logger[level] !== 'function') { + return; } + + const message = includeStack + ? `${msg} @ ${new Error().stack?.split('\n')[3]?.trim()}` + : msg; + + if (level === 'debug' && !this.debugEnabled) return; + this.logger[level](message); } - /** - * resetInstancePool - Reset the instance pool, useful for when a Homebridge instance is restarted - */ + debug(msg: string) { + this.logMessage('debug', msg, true); + } + + info(msg: string) { + this.logMessage('info', msg); + } + + warn(msg: string) { + this.logMessage('warn', msg); + } + + error(msg: string) { + this.logMessage('error', msg); + } + + // Example usage in methods public resetInstancePool() { if (this.discoveryInProgress) { this.browser.stop(); this.debug(`[HapClient] Discovery :: Terminated`); this.discoveryInProgress = false; + this.emit('discovery-terminated'); } this.instances = []; - this.resetInstancePoolTimeout = setTimeout(() => { this.refreshInstances(); }, 6000); @@ -103,11 +121,12 @@ export class HapClient extends EventEmitter { this.browser.start(); this.debug(`[HapClient] Discovery :: Started`); - // stop discovery after 20 seconds + // stop discovery after 60 seconds this.startDiscoveryTimeout = setTimeout(() => { this.browser.stop(); this.debug(`[HapClient] Discovery :: Ended`); this.discoveryInProgress = false; + this.emit('discovery-ended'); }, 60000); // service found @@ -124,6 +143,7 @@ export class HapClient extends EventEmitter { port: device.port, services: [], connectionFailedCount: 0, + configurationNumber: device.txt['c#'], }; this.debug(`[HapClient] Discovery :: Found HAP device with username ${instance.username}`); @@ -131,16 +151,23 @@ export class HapClient extends EventEmitter { // update an existing instance const existingInstanceIndex = this.instances.findIndex(x => x.username === instance.username); if (existingInstanceIndex > -1) { - + // ipAddresses change use case is not handled + const configurationChanged = this.instances[existingInstanceIndex].configurationNumber !== instance.configurationNumber; if ( this.instances[existingInstanceIndex].port !== instance.port || - this.instances[existingInstanceIndex].name !== instance.name + this.instances[existingInstanceIndex].name !== instance.name || + configurationChanged ) { this.instances[existingInstanceIndex].port = instance.port; this.instances[existingInstanceIndex].name = instance.name; + this.instances[existingInstanceIndex].configurationNumber = instance.configurationNumber; this.debug(`[HapClient] Discovery :: [${this.instances[existingInstanceIndex].ipAddress}:${instance.port} ` + `(${instance.username})] Instance Updated`); - this.emit('instance-discovered', instance); + this.emit('instance-discovered', this.instances[existingInstanceIndex]); + if (configurationChanged) { + this.emit('instance-configuration-changed', this.instances[existingInstanceIndex]); + } + this.hapMonitor?.refreshMonitorConnection(this.instances[existingInstanceIndex]); } return; @@ -176,6 +203,7 @@ export class HapClient extends EventEmitter { this.instances.push(instance); this.debug(`[HapClient] Discovery :: [${instance.ipAddress}:${instance.port} (${instance.username})] Instance Registered`); this.emit('instance-discovered', instance); + this.hapMonitor?.refreshMonitorConnection(instance); } else { this.debug(`[HapClient] Discovery :: Could not register to device with username ${instance.username}`); } @@ -220,15 +248,13 @@ export class HapClient extends EventEmitter { accessories.push(accessory); } } catch (e) { - if (this.logger) { - instance.connectionFailedCount++; - this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Failed to connect`); - - if (instance.connectionFailedCount > 5) { - const instanceIndex = this.instances.findIndex(x => x.username === instance.username && x.ipAddress === instance.ipAddress); - this.instances.splice(instanceIndex, 1); - this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Removed From Instance Pool`); - } + instance.connectionFailedCount++; + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Failed to connect`); + + if (instance.connectionFailedCount > 5) { + const instanceIndex = this.instances.findIndex(x => x.username === instance.username && x.ipAddress === instance.ipAddress); + this.instances.splice(instanceIndex, 1); + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Removed From Instance Pool`); } } } @@ -328,7 +354,7 @@ export class HapClient extends EventEmitter { uuid: s.type, type: Services[s.type], humanType: this.humanizeString(Services[s.type]), - serviceName: serviceName.value.toString(), + serviceName: (serviceName.value.toString().length ? serviceName.value.toString() : accessoryInformation.Name), serviceCharacteristics, accessoryInformation, values: {}, @@ -351,11 +377,20 @@ export class HapClient extends EventEmitter { return this.setCharacteristic.bind(this)(service, iid, value); }; + service.setCharacteristicByType = (type: string, value: number | string | boolean) => { + return this.setCharacteristicByType.bind(this)(service, type, value); + }; + /* Helper function to returns a characteristic by it's type name */ service.getCharacteristic = (type: string) => { return service.serviceCharacteristics.find(c => c.type === type); }; + if (service.type === 'CameraRTPStreamManagement') { + service.getResource = (body: ResourceRequestType) => { + return this.getResource.bind(this)(service, body); + }; + } service.serviceCharacteristics.forEach((c) => { /* Helper function to set the value of a characteristic */ c.setValue = async (value: number | string | boolean) => { @@ -401,13 +436,15 @@ export class HapClient extends EventEmitter { resp.characteristics.forEach((c) => { const characteristic = service.serviceCharacteristics.find(x => x.iid === c.iid && x.aid === service.aid); characteristic.value = c.value; + service.values[characteristic.type] = c.value; }); - + return service; } catch (e) { - this.debug(e); - this.logger.log(`Failed to refresh characteristics for ${service.serviceName}: ${e.message}`); + this.debug(`[HapClient] +${e}`); + + this.error(`[HapClient] Failed to refresh characteristics for ${service.serviceName}: ${e.message}`); + } - return service; } async getCharacteristic(service: ServiceType, iid: number): Promise { @@ -420,14 +457,25 @@ export class HapClient extends EventEmitter { const characteristic = service.serviceCharacteristics.find(x => x.iid === resp.characteristics[0].iid && x.aid === service.aid); characteristic.value = resp.characteristics[0].value; + service.values[characteristic.type] = resp.characteristics[0].value; return characteristic; } catch (e) { - this.debug(e); - this.logger.log(`Failed to get characteristics for ${service.serviceName} with iid ${iid}: ${e.message}`); + this.debug(`[HapClient] +${e}`); + + this.error(`[HapClient] Failed to get characteristic for ${service.serviceName} with iid ${iid}: ${e.message}`); + } } + async setCharacteristicByType(service: ServiceType, type: string, value: number | string | boolean) { + const characteristic = service.serviceCharacteristics.find(x => x.type === type); + if (!characteristic) { + throw new Error(`Characteristic ${type} not found in service ${service.serviceName}`); + } + return this.setCharacteristic(service, characteristic.iid, value); + } + async setCharacteristic(service: ServiceType, iid: number, value: number | string | boolean) { try { await axios.put(`http://${service.instance.ipAddress}:${service.instance.port}/characteristics`, @@ -448,21 +496,58 @@ export class HapClient extends EventEmitter { ); return this.getCharacteristic(service, iid); } catch (e) { - if (this.logger) { - this.logger.error(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + - `Failed to set value for ${service.serviceName}.`); - if (e.response && e.response.status === 470 || e.response.status === 401) { - this.logger.warn(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + - `Make sure Homebridge pin for this instance is set to ${this.pin}.`); - throw new Error(`Failed to control accessory. Make sure the Homebridge pin for ${service.instance.ipAddress}:${service.instance.port} ` + - `is set to ${this.pin}.`); - } else { - this.logger.error(e.message); - throw new Error(`Failed to control accessory: ${e.message}`); + + this.error(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + + `Failed to set value for ${service.serviceName}.`); + if (e.response && e.response?.status === 470 || e.response?.status === 401) { + this.warn(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + + `Make sure Homebridge pin for this instance is set to ${this.pin}.`); + throw new Error(`Failed to control accessory. Make sure the Homebridge pin for ${service.instance.ipAddress}:${service.instance.port} ` + + `is set to ${this.pin}.`); + } else { + this.error(e.message); + throw new Error(`Failed to control accessory: ${e.message}`); + } + + } + } + + async getResource(service: ServiceType, body: ResourceRequestType) { + try { + const resp: any = await axios.post(`http://${service.instance.ipAddress}:${service.instance.port}/resource`, + { + ...body, aid: service.aid + }, + { + responseType: 'arraybuffer', + headers: { + Authorization: this.pin, + }, } + ); + if (resp.status === 200) { + return resp.data; } else { - console.log(e); + + this.warn(`[HapClient] getResource [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + + `Failed to request resource from accessory ${service.serviceName}. Response status Code ${resp.status}`); + } + return; + } catch (e) { + + this.error(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + + `Failed to request resource from accessory ${service.serviceName}.`); + if (e.response && e.response?.status === 470 || e.response?.status === 401) { + this.warn(`[HapClient] [${service.instance.ipAddress}:${service.instance.port} (${service.instance.username})] ` + + `Make sure Homebridge pin for this instance is set to ${this.pin}.`); + throw new Error(`Failed to request resource from accessory. Make sure the Homebridge pin for ${service.instance.ipAddress}:${service.instance.port} ` + + `is set to ${this.pin}.`); + } else { + this.error(e.message); + throw new Error(`Failed to request resource: ${e.message}`); + } + } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 30d69f2..60ea6ce 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -7,6 +7,7 @@ export interface HapInstance { username: string; connectionFailedCount: number; services: ServiceType[]; + configurationNumber: number; } export interface HapEvInstance { @@ -68,7 +69,9 @@ export interface ServiceType { accessoryInformation: any; refreshCharacteristics?: () => Promise; setCharacteristic?: (iid: number, value: number | string | boolean) => Promise; + setCharacteristicByType?: (type: string, value: number | string | boolean) => Promise; getCharacteristic?: (type: string) => CharacteristicType; + getResource?: (body: ResourceRequestType) => Promise; values: any; instance: HapInstance; uniqueId?: string; @@ -103,3 +106,10 @@ export interface AccessoryInformationProperties { 'Serial Number': string; 'Firmware Revision': string; } + +export interface ResourceRequestType { + aid?: number, + "resource-type": string, + "image-width": number, + "image-height": number +} \ No newline at end of file diff --git a/src/monitor.ts b/src/monitor.ts index 9015c7a..1d17fb0 100644 --- a/src/monitor.ts +++ b/src/monitor.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events'; -import { ServiceType, HapEvInstance } from './interfaces'; import { createConnection, parseMessage } from './eventedHttpClient'; +import { HapEvInstance, ServiceType } from './interfaces'; /** * HapMonitor - Creates a monitor to watch for changes in accessory characteristics. And generates 'service-update' events when they change. @@ -28,8 +28,23 @@ export class HapMonitor extends EventEmitter { this.start(); } + log(message: string) { + this.logger?.log(`[HapMonitor] ${message}`); + } + + error(message: string) { + this.logger?.log(`[HapMonitor] ERROR: ${message}`); + } + start() { for (const instance of this.evInstances) { + this.connectInstance(instance); + } + } + + connectInstance(instance: HapEvInstance) { + try { + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Connecting`); instance.socket = createConnection(instance, this.pin, { characteristics: instance.evCharacteristics }); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Connected`); @@ -38,10 +53,8 @@ export class HapMonitor extends EventEmitter { const message = parseMessage(data); if (message.statusCode === 401) { - if (this.logger) { - this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` + - `${message.statusCode} ${message.statusMessage} - make sure Homebridge pin for this instance is set to ${this.pin}.`); - } + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] ` + + `${message.statusCode} ${message.statusMessage} - make sure Homebridge pin for this instance is set to ${this.pin}.`); } if (message.protocol === 'EVENT') { @@ -76,6 +89,19 @@ export class HapMonitor extends EventEmitter { } } }); + instance.socket.on('close', (hadError) => { + this.emit('monitor-close', instance, hadError); + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] closed: ${hadError}`); + }); + instance.socket.on('error', (error) => { // Even though this is redundant with the close event, it's necessary to catch the error event here + this.emit('monitor-error', instance, error); + this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] error: ${error}`); + }); + } catch (e) { + this.debug(e); + + this.error(`Monitor Start Error [${instance.ipAddress}:${instance.port} (${instance.username})]: ${e.message}`); + } } @@ -84,6 +110,7 @@ export class HapMonitor extends EventEmitter { if (instance.socket) { try { instance.socket.destroy(); + instance.socket.removeAllListeners(); this.debug(`[HapClient] [${instance.ipAddress}:${instance.port} (${instance.username})] Disconnected`); } catch (e) { // do nothing @@ -92,6 +119,21 @@ export class HapMonitor extends EventEmitter { } } + refreshMonitorConnection(refreshInstance: HapEvInstance) { + this.debug(`[HapClient] [${refreshInstance.ipAddress}:${refreshInstance.port} (${refreshInstance.username})] Refreshing Monitor`); + // console.log('this.evInstances', this.evInstances); + const instance = this.evInstances.find(x => x.username === refreshInstance.username); + if (instance) { + instance.socket.destroy(); + instance.socket.removeAllListeners(); + instance.port = refreshInstance.port; + instance.ipAddress = refreshInstance.ipAddress; + + this.connectInstance(instance); + this.emit('monitor-refresh', instance); + } + } + parseServices() { // get a list of characteristics we can watch for each instance for (const service of this.services) {