From 298c94e0f32dbe42e4290e400e11c693725a214f Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Wed, 14 Feb 2024 00:02:38 -0600 Subject: [PATCH] v1.1.0 (#4) ## [1.1.0](https://github.com/donavanbecker/homebridge-meater/releases/tag/v1.1.0) (2024-02-13) ### What's Changed - Added `device` config so that config can be assign on each device. - Housekeeping and updated dependencies. **Full Changelog**: https://github.com/donavanbecker/homebridge-meater/compare/v1.0.0...v1.1.0 --- .github/workflows/beta-release.yml | 14 + .github/workflows/changerelease.yml | 14 +- .github/workflows/dependabot.yml | 10 +- .github/workflows/discord-webhooks.yml | 17 - .github/workflows/labeler.yml | 9 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/release.yml | 12 + CHANGELOG.md | 10 +- config.schema.json | 244 ++++++++++++--- nodemon.json | 5 +- package-lock.json | 409 ++++++++++++++----------- package.json | 18 +- src/device/device.ts | 147 +++++++++ src/device/meater.ts | 321 ++++++++++--------- src/homebridge-ui/public/index.html | 23 +- src/homebridge-ui/server.ts | 5 +- src/index.ts | 7 +- src/platform.ts | 358 +++++++++++++--------- src/settings.ts | 33 +- 19 files changed, 1062 insertions(+), 596 deletions(-) delete mode 100644 .github/workflows/discord-webhooks.yml create mode 100644 src/device/device.ts diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index df9fd87..d114344 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -3,6 +3,8 @@ name: Node-CI Beta on: push: branches: [beta-*.*.*, beta] + release: + types: [prereleased] workflow_dispatch: jobs: @@ -29,3 +31,15 @@ jobs: pre_id: 'beta' secrets: npm_auth_token: ${{ secrets.npm_token }} + + github-releases-to-discord: + needs: publish + + if: ${{ github.repository == 'donavanbecker/homebridge-meater' && github.event.release.prerelease == true }} + + uses: OpenWonderLabs/.github/.github/workflows/discord-webhooks.yml@latest + with: + footer_title: "August" + secrets: + DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} + DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/.github/workflows/changerelease.yml b/.github/workflows/changerelease.yml index e66082d..111ba97 100644 --- a/.github/workflows/changerelease.yml +++ b/.github/workflows/changerelease.yml @@ -1,13 +1,11 @@ name: Changelog to Release on: - workflow_dispatch: - push: - paths: [CHANGELOG.md] - branches: [latest] + release: + types: [published] jobs: - changerelease: - uses: donavanbecker/.github/.github/workflows/changerelease.yml@latest - secrets: - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + changerelease: + uses: donavanbecker/.github/.github/workflows/changerelease.yml@latest + secrets: + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 568711a..525d2b2 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,16 +2,12 @@ name: AutoDependabot on: pull_request: - branches: - - beta - - latest + branches: [ beta, latest ] pull_request_target: - branches: - - beta - - latest + branches: [ beta, latest ] jobs: - label: + dependabot: uses: donavanbecker/.github/.github/workflows/dependabot.yml@latest secrets: token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/discord-webhooks.yml b/.github/workflows/discord-webhooks.yml deleted file mode 100644 index 7037ba9..0000000 --- a/.github/workflows/discord-webhooks.yml +++ /dev/null @@ -1,17 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Discord Webhooks - -# Controls when the workflow will run -on: - release: - types: [released, prereleased] - -jobs: - github-releases-to-discord: - uses: donavanbecker/.github/.github/workflows/discord-webhooks.yml@latest - with: - footer_title: "Meater" - secrets: - DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} - DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d31b349..6eb3c01 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,16 +1,9 @@ -# This workflow will triage pull requests and apply a label based on the -# paths that are modified in the pull request. -# -# To use this workflow, you will need to set up a .github/labeler.yml -# file with configuration. For more information, see: -# https://github.com/actions/labeler/blob/main/README.md - name: Labeler on: [pull_request] jobs: - label: + labeler: uses: donavanbecker/.github/.github/workflows/labeler.yml@latest secrets: token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index e521f3a..3754194 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: jobs: - stale: + release-drafter: uses: donavanbecker/.github/.github/workflows/release-drafter.yml@latest secrets: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58e56a8..4aa6958 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,3 +22,15 @@ jobs: uses: donavanbecker/.github/.github/workflows/npm-publish.yml@latest secrets: npm_auth_token: ${{ secrets.npm_token }} + + github-releases-to-discord: + needs: publish + + if: ${{ github.repository == 'donavanbecker/homebridge-meater' }} + + uses: donavanbecker/.github/.github/workflows/discord-webhooks.yml@latest + with: + footer_title: "Meater" + secrets: + DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} + DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 884815e..96516a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,17 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) -## [1.0.0](https://github.com/donavanbecker/homebridge-meater/releases/tag/v1.0.0) (2024-01-31) +## [1.1.0](https://github.com/donavanbecker/homebridge-meater/releases/tag/v1.1.0) (2024-02-13) ### What's Changed +- Added `device` config so that config can be assign on each device. +- Housekeeping and updated dependencies. + +**Full Changelog**: https://github.com/donavanbecker/homebridge-meater/compare/v1.0.0...v1.1.0 + +## [1.0.0](https://github.com/donavanbecker/homebridge-meater/releases/tag/v1.0.0) (2024-01-31) +### What's Changed - Release of [homebridge-meater](https://github.com/donavanbecker/homebridge-meater) which allows you to see the temperature to your Meater Thermometer throw HomeKit. **Full Changelog**: https://github.com/donavanbecker/homebridge-meater/compare/v0.1.0...v1.0.0 @@ -13,5 +20,4 @@ All notable changes to this project will be documented in this file. This projec ## [0.1.0](https://github.com/donavanbecker/homebridge-meater/releases/tag/v0.1.0) (2024-01-12) ### What's Changed - - Initial release of homebridge-meater. diff --git a/config.schema.json b/config.schema.json index 80b9621..90fe95f 100644 --- a/config.schema.json +++ b/config.schema.json @@ -14,56 +14,216 @@ "title": "Name", "default": "Meater" }, - "email": { - "type": "string", - "title": "email", - "placeholder": "apple@icloud.com", - "format": "email" - }, - "password": { - "type": "string", - "title": "Password", - "x-schema-form": { - "type": "password" - } - }, - "logging": { - "title": "Logging Setting", - "type": "string", - "required": true, - "default": "", - "oneOf": [ - { - "title": "Default Logging", - "enum": [ - "" - ] + "credentials": { + "type": "object", + "properties": { + "email": { + "type": "string", + "title": "email", + "placeholder": "apple@icloud.com", + "format": "email" }, - { - "title": "Standard Logging", - "enum": [ - "standard" - ] + "password": { + "type": "string", + "title": "Password", + "x-schema-form": { + "type": "password" + } }, - { - "title": "No Logging", - "enum": [ - "none" - ] + "token": { + "type": "string", + "title": "Token", + "x-schema-form": { + "type": "token" + } + }, + "notice": { + "title": "Notice", + "type": "string", + "default": "Keep your tokens a secret!" + } + }, + "required": ["email", "password"] + }, + "options": { + "type": "object", + "properties": { + "devices": { + "type": "array", + "items": { + "title": "Devices", + "type": "object", + "properties": { + "id": { + "title": "Device ID", + "type": "string", + "placeholder": "81F3UT59513F" + }, + "configDeviceName": { + "title": "Device Name", + "type": "string", + "placeholder": "Hallway Thermostat", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id);" + } + }, + "hide_device": { + "title": "Hide Device", + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id);" + } + }, + "external": { + "title": "External Accessory", + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id && !model.options.devices[arrayIndices].hide_device);" + } + }, + "firmware": { + "title": "Firmware Override", + "type": "string", + "placeholder": "1.2.8", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id && !model.options.devices[arrayIndices].hide_device);" + } + }, + "refreshRate": { + "title": "Device Refresh Rate", + "type": "number", + "minimum": 30, + "placeholder": 360, + "description": "Indicates the number of seconds between polls of Meater API.", + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].deviceClass === 'Thermostat' || model.options.devices[arrayIndices].deviceClass === 'LeakDetector'));" + } + }, + "logging": { + "title": "Device Logging Override Setting", + "type": "string", + "required": true, + "default": "", + "oneOf": [ + { + "title": "Default Logging", + "enum": [""] + }, + { + "title": "Standard Logging", + "enum": ["standard"] + }, + { + "title": "No Logging", + "enum": ["none"] + }, + { + "title": "Debug Logging", + "enum": ["debug"] + } + ], + "condition": { + "functionBody": "return (model.options && model.options.devices && model.options.devices[arrayIndices].id && !model.options.devices[arrayIndices].hide_device && (model.options.devices[arrayIndices].deviceClass === 'Thermostat' || model.options.devices[arrayIndices].deviceClass === 'LeakDetector'));" + } + } + }, + "required": ["id", "configDeviceName", "logging"] + } }, - { - "title": "Debug Logging", - "enum": [ - "debug" + "refreshRate": { + "title": "Refresh Rate", + "type": "number", + "minimum": 30, + "placeholder": 120, + "description": "Indicates the number of seconds between polls of the Meater service." + }, + "logging": { + "title": "Logging Setting", + "type": "string", + "required": true, + "default": "", + "oneOf": [ + { + "title": "Default Logging", + "enum": [""] + }, + { + "title": "Standard Logging", + "enum": ["standard"] + }, + { + "title": "No Logging", + "enum": ["none"] + }, + { + "title": "Debug Logging", + "enum": ["debug"] + } ] } - ] + }, + "required": ["logging"] } } }, "layout": [ - "email", - "password", - "logging" + { + "type": "fieldset", + "title": "Meater Account Info", + "expandable": true, + "expanded": false, + "items": [ + { + "type": "help", + "helpvalue": "
This is for Manual Setup Only." + }, + "credentials.email", + "credentials.password", + "credentials.token" + ] + }, + { + "type": "fieldset", + "title": "Meater Device Settings", + "expandable": true, + "expanded": false, + "items": [ + { + "key": "options.devices", + "notitle": true, + "type": "tabarray", + "title": "{{ value.configDeviceName || value.id || 'New Meater Device' }}", + "expandable": true, + "expanded": false, + "orderable": false, + "items": [ + "options.devices[].configDeviceName", + "options.devices[].id", + "options.devices[].hide_device", + "options.devices[].firmware", + "options.devices[].external", + "options.devices[].refreshRate", + "options.devices[].logging" + ] + } + ] + }, + { + "type": "fieldset", + "title": "Advanced Settings", + "expandable": true, + "expanded": false, + "items": [ + { + "type": "help", + "helpvalue": "
Refresh Rate
Refresh Rate indicates the number of seconds between polls of the Meater service." + }, + { + "key": "options.refreshRate", + "notitle": true + }, + "options.logging" + ] + } ] } \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index db352ce..758b873 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,8 +1,9 @@ { "watch": [ - "src" + "src", + "config.schema.json" ], - "ext": "ts", + "ext": "ts, html, json", "ignore": [], "exec": "DEBUG= tsc && homebridge -T -D -P -I -U ~/.homebridge-dev ..", "signal": "SIGTERM", diff --git a/package-lock.json b/package-lock.json index 75a52d7..e275e33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-meater", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-meater", - "version": "1.0.0", + "version": "1.1.0", "funding": [ { "type": "Paypal", @@ -21,16 +21,16 @@ "dependencies": { "@homebridge/plugin-ui-utils": "^1.0.1", "rxjs": "^7.8.1", - "undici": "^6.5.0" + "undici": "^6.6.2" }, "devDependencies": { - "@types/node": "^20.11.13", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", + "@types/node": "^20.11.17", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", "homebridge": "^1.7.0", "nodemon": "^3.0.3", - "npm-check-updates": "^16.14.14", + "npm-check-updates": "^16.14.15", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "typescript": "^5.3.3" @@ -810,31 +810,31 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.13.tgz", - "integrity": "sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", - "integrity": "sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", + "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/type-utils": "6.20.0", - "@typescript-eslint/utils": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "7.0.1", + "@typescript-eslint/type-utils": "7.0.1", + "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/visitor-keys": "7.0.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -850,8 +850,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -860,15 +860,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", - "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", + "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "7.0.1", + "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/visitor-keys": "7.0.1", "debug": "^4.3.4" }, "engines": { @@ -879,7 +879,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -888,13 +888,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", - "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", + "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" + "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/visitor-keys": "7.0.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -905,13 +905,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz", - "integrity": "sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", + "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/utils": "6.20.0", + "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/utils": "7.0.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -923,7 +923,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -932,9 +932,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", - "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", + "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -945,13 +945,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", - "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", + "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/visitor-keys": "7.0.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -973,17 +973,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", - "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", + "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/scope-manager": "7.0.1", + "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.1", "semver": "^7.5.4" }, "engines": { @@ -994,16 +994,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", + "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/types": "7.0.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1187,13 +1187,16 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1215,9 +1218,9 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "dev": true, "engines": { "node": ">= 0.4" @@ -1446,14 +1449,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1497,16 +1505,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1519,6 +1521,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1826,17 +1831,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -1965,6 +1973,27 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -2256,9 +2285,9 @@ } }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2538,16 +2567,20 @@ "dev": true }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "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" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2758,12 +2791,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2794,12 +2827,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -2827,9 +2860,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -2949,9 +2982,9 @@ } }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -3050,12 +3083,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -3069,6 +3102,19 @@ "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", "dev": true }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -3086,14 +3132,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3372,21 +3420,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -3472,6 +3505,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4404,9 +4443,9 @@ } }, "node_modules/npm-check-updates": { - "version": "16.14.14", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.14.tgz", - "integrity": "sha512-Y3ajS/Ep40jM489rLBdz9jehn/BMil5s9fA4PSr2ZJxxSmtLWCSmRqsI2IEZ9Nb3MTMu8a3s7kBs0l+JbjdkTA==", + "version": "16.14.15", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", + "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", "dev": true, "dependencies": { "chalk": "^5.3.0", @@ -5149,14 +5188,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "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==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -5342,9 +5382,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5396,14 +5436,15 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -5447,14 +5488,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5529,16 +5574,16 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz", + "integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -5556,12 +5601,6 @@ "node": ">= 10" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5620,9 +5659,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/split": { @@ -5637,6 +5676,12 @@ "node": "*" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/ssri": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", @@ -5878,12 +5923,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -6010,9 +6055,9 @@ "dev": true }, "node_modules/undici": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.5.0.tgz", - "integrity": "sha512-/MUmPb2ptTvp1j7lPvdMSofMdqPxcOhAaKZi4k55sqm6XMeKI3n1dZJ5cnD4gLjpt2l7CIlthR1IXM59xKhpxw==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.6.2.tgz", + "integrity": "sha512-vSqvUE5skSxQJ5sztTZ/CdeJb1Wq0Hf44hlYMciqHghvz+K88U0l7D6u1VsndoFgskDcnU+nG3gYmMzJVzd9Qg==", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -6213,16 +6258,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 15fd356..268029d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Meater", "name": "homebridge-meater", - "version": "1.0.0", + "version": "1.1.0", "description": "The Meater plugin allows you to access your Meater device(s) from HomeKit.", "author": { "name": "donavanbecker", @@ -25,15 +25,13 @@ "scripts": { "check": "npm install && npm outdated", "update": "ncu -u && npm update && npm install", - "updateDependencies": "npm run check && npm run update", - "lint": "eslint src/**.ts", + "lint": "eslint src/**/*.ts", "watch": "npm run build && npm run plugin-ui && npm link && nodemon", "plugin-ui": "rsync ./src/homebridge-ui/public/index.html ./dist/homebridge-ui/public/", "build": "rimraf ./dist && tsc", "prepublishOnly": "npm run lint && npm run build && npm run plugin-ui", "postpublish": "npm run clean", - "clean": "rimraf ./dist", - "test": "eslint src/**.ts" + "clean": "rimraf ./dist" }, "funding": [ { @@ -56,16 +54,16 @@ "dependencies": { "@homebridge/plugin-ui-utils": "^1.0.1", "rxjs": "^7.8.1", - "undici": "^6.5.0" + "undici": "^6.6.2" }, "devDependencies": { - "@types/node": "^20.11.13", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", + "@types/node": "^20.11.17", + "@typescript-eslint/eslint-plugin": "^7.0.1", + "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", "homebridge": "^1.7.0", "nodemon": "^3.0.3", - "npm-check-updates": "^16.14.14", + "npm-check-updates": "^16.14.15", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "typescript": "^5.3.3" diff --git a/src/device/device.ts b/src/device/device.ts new file mode 100644 index 0000000..8d6cdfb --- /dev/null +++ b/src/device/device.ts @@ -0,0 +1,147 @@ +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * device.ts: homebridge-meater. + */ +import { API, HAP, Logging, PlatformAccessory } from 'homebridge'; + +import { MeaterPlatform } from '../platform.js'; +import { MeaterPlatformConfig, device, devicesConfig } from '../settings.js'; + +export abstract class deviceBase { + public readonly api: API; + public readonly log: Logging; + public readonly config!: MeaterPlatformConfig; + protected readonly hap: HAP; + + // Config + protected deviceLogging!: string; + protected deviceRefreshRate!: number; + + constructor( + protected readonly platform: MeaterPlatform, + protected accessory: PlatformAccessory, + protected device: device, + ) { + this.api = this.platform.api; + this.log = this.platform.log; + this.config = this.platform.config; + this.hap = this.api.hap; + + this.deviceLogs(device); + this.getDeviceRefreshRate(device); + this.deviceConfigOptions(device); + this.deviceContext(accessory, device); + + // Set accessory information + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Meater') + .setCharacteristic(this.hap.Characteristic.Model, 'Smart Meat Thermometer') + .setCharacteristic(this.hap.Characteristic.SerialNumber, device.id) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); + } + + async deviceLogs(device: device & devicesConfig): Promise { + if (this.platform.debugMode) { + this.deviceLogging = this.accessory.context.logging = 'debugMode'; + this.debugWarnLog(`Lock: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); + } else if (device.logging) { + this.deviceLogging = this.accessory.context.logging = device.logging; + this.debugWarnLog(`Lock: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); + } else if (this.config.logging) { + this.deviceLogging = this.accessory.context.logging = this.config.logging; + this.debugWarnLog(`Lock: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + } else { + this.deviceLogging = this.accessory.context.logging = 'standard'; + this.debugWarnLog(`Lock: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + } + } + + async getDeviceRefreshRate(device: device & devicesConfig): Promise { + if (device.refreshRate) { + if (device.refreshRate < 1800) { + device.refreshRate = 1800; + this.warnLog('Refresh Rate cannot be set to lower the 5 mins, as Lock detail (battery level, etc) are unlikely to change within that period'); + } + this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; + this.debugLog(`Lock: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); + } else if (this.config.refreshRate) { + this.deviceRefreshRate = this.accessory.context.refreshRate = this.config.refreshRate; + this.debugLog(`Lock: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); + } + } + + async deviceConfigOptions(device: device & devicesConfig): Promise { + const deviceConfig = {}; + if (device.logging !== undefined) { + deviceConfig['logging'] = device.logging; + } + if (device.refreshRate !== undefined) { + deviceConfig['refreshRate'] = device.refreshRate; + } + if (Object.entries(deviceConfig).length !== 0) { + this.infoLog(`Lock: ${this.accessory.displayName} Config: ${JSON.stringify(deviceConfig)}`); + } + } + + async deviceContext(accessory: PlatformAccessory, device: device & devicesConfig): Promise { + if (device.firmware) { + accessory.context.FirmwareRevision = device.firmware; + } else if (accessory.context.FirmwareRevision === undefined) { + accessory.context.FirmwareRevision = await this.platform.getVersion(); + } else { + accessory.context.FirmwareRevision = '3'; + } + } + + /** + * Logging for Device + */ + infoLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.info(String(...log)); + } + } + + warnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.warn(String(...log)); + } + } + + debugWarnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.warn('[DEBUG]', String(...log)); + } + } + } + + errorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.error(String(...log)); + } + } + + debugErrorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.error('[DEBUG]', String(...log)); + } + } + } + + debugLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging === 'debug') { + this.log.info('[DEBUG]', String(...log)); + } else { + this.log.debug(String(...log)); + } + } + } + + enablingDeviceLogging(): boolean { + return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + } +} \ No newline at end of file diff --git a/src/device/meater.ts b/src/device/meater.ts index 0ecaa92..af352ee 100644 --- a/src/device/meater.ts +++ b/src/device/meater.ts @@ -1,158 +1,162 @@ -import { Service, PlatformAccessory, CharacteristicValue, API, HAP, Logging } from 'homebridge'; -import { MeaterPlatform } from '../platform.js'; -import { interval } from 'rxjs'; +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * meater.ts: homebridge-meater. + */ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; import { request } from 'undici'; -import { MeaterPlatformConfig, device, meaterUrl } from '../settings.js'; +import { interval, skipWhile } from 'rxjs'; + +import { deviceBase } from './device.js'; +import { MeaterPlatform } from '../platform.js'; +import { device, devicesConfig, meaterUrl } from '../settings.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class Meater { - public readonly api: API; - public readonly log: Logging; - public readonly config!: MeaterPlatformConfig; - protected readonly hap: HAP; - // Services - serviceLabel!: Service; - cookRefreshSwitchService!: Service; - internalTemperatureService!: Service; - ambientTemperatureService!: Service; - - // Characteristic Values - internalCurrentTemperature: CharacteristicValue; - ambientCurrentTemperature: CharacteristicValue; +export class Meater extends deviceBase { + // Service + private serviceLabel!: { + service: Service; + }; + + private cookRefresh!: { + service: Service; + on: CharacteristicValue; + }; + + private internal!: { + service: Service; + currentTemperature: CharacteristicValue; + }; + + private ambient!: { + service: Service; + currentTemperature: CharacteristicValue; + }; // Cofiguration - cookRefresh!: boolean; + CookRefresh!: boolean; // Updates SensorUpdateInProgress!: boolean; constructor( - private readonly platform: MeaterPlatform, - private readonly accessory: PlatformAccessory, - public device: device, + readonly platform: MeaterPlatform, + accessory: PlatformAccessory, + device: device & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - - this.cookRefresh = accessory.context.cookRefresh; - this.internalCurrentTemperature = accessory.context.internalCurrentTemperature; - this.ambientCurrentTemperature = accessory.context.ambientCurrentTemperature; - accessory.context.FirmwareRevision = 'v1.0.0'; - - // Retrieve initial values and updateHomekit - this.refreshStatus(); - - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Meater') - .setCharacteristic(this.hap.Characteristic.Model, 'Smart Meat Thermometer') - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.id) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.FirmwareRevision); - - // Service Label Service - this.serviceLabel = this.accessory.getService(this.hap.Service.ServiceLabel) || - this.accessory.addService(this.hap.Service.ServiceLabel, `Meater Thermometer (${device.id.slice(0, 4)})` ); - this.serviceLabel.setCharacteristic(this.hap.Characteristic.Name, `Meater Thermometer (${device.id.slice(0, 4)})`); - if ( - !this.serviceLabel.testCharacteristic(this.hap.Characteristic.ConfiguredName) && - !this.serviceLabel.testCharacteristic(this.hap.Characteristic.Name) - ) { - this.serviceLabel.addCharacteristic( - this.hap.Characteristic.ConfiguredName, `Meater Thermometer (${device.id.slice(0, 4)})`, - ); - } + super(platform, accessory, device); - // Interal Temperature Sensor Service - this.internalTemperatureService = this.accessory.getServiceById(this.hap.Service.TemperatureSensor, 'Internal Temperature'); - if (!this.internalTemperatureService) { - this.internalTemperatureService = new this.hap.Service.TemperatureSensor('Internal Temperature', 'Internal Temperature'); - if (this.internalTemperatureService) { - this.internalTemperatureService = this.accessory.addService(this.internalTemperatureService); - this.log.debug('Internal Temperature Service'); - } else { - this.log.error('Internal Temperature Service -- Failed!'); - } - } - this.internalTemperatureService.setCharacteristic(this.hap.Characteristic.Name, 'Internal Temperature'); - if (!this.internalTemperatureService.testCharacteristic(this.hap.Characteristic.ConfiguredName) && - !this.internalTemperatureService.testCharacteristic(this.hap.Characteristic.Name)) { - this.internalTemperatureService.addCharacteristic(this.hap.Characteristic.ConfiguredName, 'Internal Temperature'); - } + this.deviceContext(); + + // serviceLabel Service + this.debugLog('Configure serviceLabel Service'); + this.serviceLabel = { + service: this.accessory.getService(this.hap.Service.ServiceLabel) + ?? this.accessory.addService(this.hap.Service.ServiceLabel, device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`), + }; + + // Add serviceLabel Service's Characteristics + this.serviceLabel.service + .setCharacteristic(this.hap.Characteristic.Name, device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`); + + // InternalTemperature Senosr Service + this.debugLog('Configure InternalTemperature Service'); + this.internal = { + service: this.accessory.getServiceById(this.hap.Service.TemperatureSensor, 'Internal Temperature'), + currentTemperature: this.accessory.context.internalCurrentTemperature, + }; - this.ambientTemperatureService = this.accessory.getServiceById(this.hap.Service.TemperatureSensor, 'Ambient Temperature'); - if (!this.ambientTemperatureService) { - this.ambientTemperatureService = new this.hap.Service.TemperatureSensor('Ambient Temperature', 'Ambient Temperature'); - if (this.ambientTemperatureService) { - this.ambientTemperatureService = this.accessory.addService(this.ambientTemperatureService); - this.log.debug('Ambient Temperature Service'); - } else { - this.log.error('Ambient Temperature Service -- Failed!'); + if (this.internal) { + if (!this.internal.service) { + this.internal.service = new this.hap.Service.TemperatureSensor('Internal Temperature', 'Internal Temperature'); + if (this.internal.service) { + this.internal.service = this.accessory.addService(this.internal.service); + this.log.debug('Internal Temperature Service'); + } else { + this.log.error('Internal Temperature Service -- Failed!'); + } } } - this.ambientTemperatureService.setCharacteristic(this.hap.Characteristic.Name, 'Ambient Temperature'); - if (!this.ambientTemperatureService.testCharacteristic(this.hap.Characteristic.ConfiguredName) && - !this.ambientTemperatureService.testCharacteristic(this.hap.Characteristic.Name)) { - this.ambientTemperatureService.addCharacteristic( - this.hap.Characteristic.ConfiguredName, 'Ambient Temperature'); - } - // Cook Refresh Switch Service Service - this.cookRefreshSwitchService = this.accessory.getServiceById(this.hap.Service.Switch, 'Cook Refresh'); - if (!this.cookRefreshSwitchService) { - this.cookRefreshSwitchService = new this.hap.Service.Switch('Cook Refresh', 'Cook Refresh'); - if (this.cookRefreshSwitchService) { - this.cookRefreshSwitchService = this.accessory.addService(this.cookRefreshSwitchService); - this.log.debug('Ambient Temperature Service'); - } else { - this.log.error('Ambient Temperature Service -- Failed!'); + // Add InternalTemperature Sensor Service's Characteristics + this.internal.service + .setCharacteristic(this.hap.Characteristic.Name, 'Internal Temperature') + .setCharacteristic(this.hap.Characteristic.ConfiguredName, 'Internal Temperature') + .setCharacteristic(this.hap.Characteristic.CurrentTemperature, this.internal.currentTemperature); + + // AmbientTemperature Senosr Service + this.debugLog('Configure AmbientTemperature Service'); + this.ambient = { + service: this.accessory.getServiceById(this.hap.Service.TemperatureSensor, 'Ambient Temperature'), + currentTemperature: this.accessory.context.ambientCurrentTemperature, + }; + if (this.ambient) { + if (!this.ambient.service) { + this.ambient.service = new this.hap.Service.TemperatureSensor('Ambient Temperature', 'Ambient Temperature'); + if (this.ambient.service) { + this.ambient.service = this.accessory.addService(this.ambient.service); + this.log.debug('Ambient Temperature Service'); + } else { + this.log.error('Ambient Temperature Service -- Failed!'); + } } } - this.cookRefreshSwitchService.setCharacteristic(this.hap.Characteristic.Name, 'Cook Refresh'); - if (!this.cookRefreshSwitchService.testCharacteristic(this.hap.Characteristic.ConfiguredName) && - !this.cookRefreshSwitchService.testCharacteristic(this.hap.Characteristic.Name)) { - this.cookRefreshSwitchService.addCharacteristic( - this.hap.Characteristic.ConfiguredName, 'Cook Refresh'); - } - // create handlers for required characteristics - this.cookRefreshSwitchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.handleOnSet.bind(this)); + + // Add AmbientTemperature Senosr Service's Characteristics + this.ambient.service + .setCharacteristic(this.hap.Characteristic.Name, 'Ambient Temperature') + .setCharacteristic(this.hap.Characteristic.ConfiguredName, 'Ambient Temperature') + .setCharacteristic(this.hap.Characteristic.CurrentTemperature, this.ambient.currentTemperature); + + // cookRefresh Service + this.debugLog('Configure cookRefresh Service'); + this.cookRefresh = { + service: this.accessory.getService(this.hap.Service.Switch) ?? this.accessory.addService(this.hap.Service.Switch, 'Cook Refresh'), + on: this.accessory.context.cookRefreshOn, + }; + + // Add serviceLabel Service's Characteristics + this.cookRefresh.service + .setCharacteristic(this.hap.Characteristic.Name, 'Cook Refresh') + .setCharacteristic(this.hap.Characteristic.ConfiguredName, 'Cook Refresh') + .setCharacteristic(this.hap.Characteristic.On, this.cookRefresh.on); + + // Create handlers for required characteristics + this.cookRefresh.service + .getCharacteristic(this.hap.Characteristic.On) + .onSet(this.handleOnSet.bind(this)); + + // this is subject we use to track when we need to POST changes to the NoIP API + this.SensorUpdateInProgress = false; // Retrieve initial values and update Homekit this.updateHomeKitCharacteristics(); // Start an update interval - (async () => { - interval(await this.refreshRate() * 1000) - .subscribe(async () => { - await this.refreshStatus(); - }); - })(); - } - - async refreshRate() { - return this.platform.config.refreshRate || 60; + interval(this.deviceRefreshRate * 1000) + .pipe(skipWhile(() => this.SensorUpdateInProgress)) + .subscribe(async () => { + await this.refreshStatus(); + }); } /** * Parse the device status from the SwitchBot api */ - async parseStatus(): Promise { + async parseStatus(device: device & devicesConfig): Promise { // Internal Temperature - this.internalCurrentTemperature = this.internalCurrentTemperature!; - if (this.internalCurrentTemperature !== this.accessory.context.internalCurrentTemperature) { - this.log.debug(`${this.accessory.displayName} Internal Current Temperature: ${this.internalCurrentTemperature}°c`); + this.internal.currentTemperature = device.data.temperature.internal; + if (this.internal.currentTemperature !== this.accessory.context.internalCurrentTemperature) { + this.log.debug(`${this.accessory.displayName} Internal Current Temperature: ${this.internal.currentTemperature}°c`); } // Ambient Temperature - this.ambientCurrentTemperature = this.ambientCurrentTemperature!; - if (this.ambientCurrentTemperature !== this.accessory.context.ambientCurrentTemperature) { - this.log.debug(`${this.accessory.displayName} Ambient Current Temperature: ${this.ambientCurrentTemperature}°c`); + this.ambient.currentTemperature = device.data.temperature.ambient; + if (this.ambient.currentTemperature !== this.accessory.context.ambientCurrentTemperature) { + this.log.debug(`${this.accessory.displayName} Ambient Current Temperature: ${this.ambient.currentTemperature}°c`); } } @@ -160,28 +164,27 @@ export class Meater { * Asks the SwitchBot API for the latest device information */ async refreshStatus(): Promise { - this.log.info(`Refreshing ${this.accessory.displayName} Status... Cooking: ${this.cookRefresh}`); - if (this.cookRefresh) { + this.log.info(`Refreshing ${this.accessory.displayName} Status... Cooking: ${this.CookRefresh ? 'On' : 'Off'}`); + if (this.CookRefresh) { try { - if (this.config.token) { - const { body, statusCode, headers } = await request(`${meaterUrl}/${this.device.id}`, { + if (this.config.credentials?.token) { + const { body, statusCode } = await request(`${meaterUrl}/${this.device.id}`, { method: 'GET', headers: { - 'Authorization': 'Bearer ' + this.config.token, + 'Authorization': 'Bearer ' + this.config.credentials?.token, }, }); - this.log.debug(`Device body: ${JSON.stringify(body)}`); this.log.debug(`Device statusCode: ${statusCode}`); - this.log.debug(`Device headers: ${JSON.stringify(headers)}`); const device: any = await body.json(); this.log.debug(`Device: ${JSON.stringify(device)}`); this.log.debug(`Device StatusCode: ${device.statusCode}`); this.log.warn(`Device: ${JSON.stringify(device.data)}`); if (statusCode === 200 && device.statusCode === 200) { - this.internalCurrentTemperature = device.data.temperature.internal; - this.ambientCurrentTemperature = device.data.temperature.ambient; - this.cookRefresh = true; - this.log.info(`${this.accessory.displayName} Internal: ${this.internalCurrentTemperature}, Ambient: ${this.ambientCurrentTemperature}°c`); + this.CookRefresh = true; + await this.parseStatus(device); + await this.updateHomeKitCharacteristics(); + this.log.info(`${this.accessory.displayName} Internal: ${this.internal.currentTemperature}, ` + + `Ambient: ${this.ambient.currentTemperature}°c`); } else { await this.statusCode(statusCode); await this.statusCode(device.statusCode); @@ -195,38 +198,36 @@ export class Meater { } } else { this.log.info(`Cook Refresh is off for ${this.accessory.displayName}`); - this.cookRefresh = false; + this.CookRefresh = false; } - await this.parseStatus(); - await this.updateHomeKitCharacteristics(); } /** * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.internalCurrentTemperature === undefined) { - this.log.debug(`${this.accessory.displayName} Internal Current Temperature: ${this.internalCurrentTemperature}`); + if (this.internal.currentTemperature === undefined) { + this.log.debug(`${this.accessory.displayName} Internal Current Temperature: ${this.internal.currentTemperature}`); } else { - this.accessory.context.internalCurrentTemperature = this.internalCurrentTemperature; - this.internalTemperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.internalCurrentTemperature); - this.log.debug(`${this.accessory.displayName} updateCharacteristic Internal Current Temperature: ${this.internalCurrentTemperature}`); + this.internal.service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.internal.currentTemperature); + this.log.debug(`${this.accessory.displayName} updateCharacteristic Internal Current Temperature: ${this.internal.currentTemperature}`); + this.accessory.context.internalCurrentTemperature = this.internal.currentTemperature; } - if (this.ambientCurrentTemperature === undefined) { - this.log.debug(`${this.accessory.displayName} Ambient Current Temperature: ${this.ambientCurrentTemperature}`); + if (this.ambient.currentTemperature === undefined) { + this.log.debug(`${this.accessory.displayName} Ambient Current Temperature: ${this.ambient.currentTemperature}`); } else { - this.accessory.context.ambientCurrentTemperature = this.ambientCurrentTemperature; - this.ambientTemperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.ambientCurrentTemperature); - this.log.debug(`${this.accessory.displayName} updateCharacteristic Ambient Current Temperature: ${this.ambientCurrentTemperature}`); + this.ambient.service?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.ambient.currentTemperature); + this.log.debug(`${this.accessory.displayName} updateCharacteristic Ambient Current Temperature: ${this.ambient.currentTemperature}`); + this.accessory.context.ambientCurrentTemperature = this.ambient.currentTemperature; } - if (this.cookRefresh === undefined) { - this.log.debug(`${this.accessory.displayName} Cook Refresh Switch: ${this.cookRefresh}`); + if (this.cookRefresh.on === undefined) { + this.log.debug(`${this.accessory.displayName} Cook Refresh Switch: ${this.cookRefresh.on}`); } else { - this.accessory.context.cookRefresh = this.cookRefresh; - this.cookRefreshSwitchService?.updateCharacteristic(this.hap.Characteristic.On, this.cookRefresh); - this.log.debug(`${this.accessory.displayName} updateCharacteristic Cook Refresh Switch: ${this.cookRefresh}`); + this.accessory.context.cookRefreshOn = this.cookRefresh.on; + this.cookRefresh.service?.updateCharacteristic(this.hap.Characteristic.On, this.cookRefresh.on); + this.log.debug(`${this.accessory.displayName} updateCharacteristic Cook Refresh Switch: ${this.cookRefresh.on}`); } } @@ -252,7 +253,7 @@ export class Meater { break; case 404: this.log.error(`${this.accessory.displayName} Not Found, statusCode: ${statusCode}`); - this.cookRefresh = false; + this.CookRefresh = false; break; case 429: this.log.error(`${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}`); @@ -267,7 +268,7 @@ export class Meater { } async apiError(e: any): Promise { - this.internalTemperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + this.internal.service?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } @@ -276,8 +277,26 @@ export class Meater { */ async handleOnSet(value: CharacteristicValue) { this.log.info('Cook Refresh On:', value); - this.cookRefresh = value as boolean; + this.CookRefresh = value as boolean; await this.refreshStatus(); await this.updateHomeKitCharacteristics(); } + + async deviceContext() { + if (this.accessory.context.internalCurrentTemperature === undefined) { + this.accessory.context.internalCurrentTemperature = 0; + } else { + this.accessory.context.internalCurrentTemperature; + } + if (this.accessory.context.ambientCurrentTemperature === undefined) { + this.accessory.context.ambientCurrentTemperature = 0; + } else { + this.accessory.context.ambientCurrentTemperature; + } + if (this.accessory.context.cookRefreshOn === undefined) { + this.accessory.context.cookRefreshOn = true; + } else { + this.accessory.context.cookRefreshOn; + } + } } diff --git a/src/homebridge-ui/public/index.html b/src/homebridge-ui/public/index.html index 9ce9d37..211275f 100644 --- a/src/homebridge-ui/public/index.html +++ b/src/homebridge-ui/public/index.html @@ -52,23 +52,11 @@ Device ID - - - - Model - + Firmware Version - - - - Device Type - - - - Connection Type - + @@ -160,11 +148,8 @@
Help/About
const thisAcc = cachedAccessories.find((x) => x.UUID === UUID); const context = thisAcc.context; document.getElementById('displayName').innerHTML = thisAcc.displayName; - document.getElementById('deviceID').innerHTML = context.deviceID; - document.getElementById('model').innerHTML = context.model; - document.getElementById('firmwareRevision').innerHTML = context.firmwareRevision || 'N/A'; - document.getElementById('deviceType').innerHTML = context.deviceType; - document.getElementById('connectionType').innerHTML = context.connectionType; + document.getElementById('ID').innerHTML = context.device.id; + document.getElementById('FirmwareRevision').innerHTML = context.FirmwareRevision || 'N/A'; document.getElementById('deviceTable').style.display = 'inline-table'; homebridge.hideSpinner(); }; diff --git a/src/homebridge-ui/server.ts b/src/homebridge-ui/server.ts index 92a2afc..05d38eb 100644 --- a/src/homebridge-ui/server.ts +++ b/src/homebridge-ui/server.ts @@ -1,4 +1,7 @@ -/* eslint-disable no-console */ +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * server.ts: homebridge-meater. + */ import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'; import fs from 'fs'; diff --git a/src/index.ts b/src/index.ts index 0686ced..ccde8d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ -/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * - * index.ts: homebridge-august plugin registration. + * index.ts: homebridge-meater. */ -import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; import { API } from 'homebridge'; + import { MeaterPlatform } from './platform.js'; +import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; // Register our platform with homebridge. export default (api: API): void => { diff --git a/src/platform.ts b/src/platform.ts index 5cddc7f..990a9a6 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,13 +1,13 @@ -/* Copyright(C) 2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * - * protect-platform.ts: homebridge-meater platform class. + * platform.ts: homebridge-meater. */ -import { API, DynamicPlatformPlugin, Logging, PlatformAccessory } from 'homebridge'; -import { PLATFORM_NAME, PLUGIN_NAME, MeaterPlatformConfig, meaterUrlLogin, meaterUrl, device, deviceConfig } from './settings.js'; +import { API, DynamicPlatformPlugin, HAP, Logging, PlatformAccessory } from 'homebridge'; +import { readFileSync, writeFileSync } from 'fs'; import { request } from 'undici'; + import { Meater } from './device/meater.js'; -import { readFileSync, writeFileSync } from 'fs'; -import util from 'node:util'; +import { PLATFORM_NAME, PLUGIN_NAME, MeaterPlatformConfig, meaterUrlLogin, meaterUrl, device, devicesConfig } from './settings.js'; /** * HomebridgePlatform @@ -18,19 +18,22 @@ export class MeaterPlatform implements DynamicPlatformPlugin { public accessories: PlatformAccessory[]; public readonly api: API; public readonly log: Logging; + protected readonly hap: HAP; public config!: MeaterPlatformConfig; - public platformLogging!: string; - public debugMode!: boolean; + platformConfig!: MeaterPlatformConfig['options']; + platformLogging!: MeaterPlatformConfig['logging']; + debugMode!: boolean; - constructor(log: Logging, config: MeaterPlatformConfig, api: API) { + constructor( + log: Logging, + config: MeaterPlatformConfig, + api: API, + ) { this.accessories = []; this.api = api; + this.hap = this.api.hap; this.log = log; - //this.log.info = this.info.bind(this); - //this.log.warn = this.warn.bind(this) || this.debugWarn.bind(this); - //this.log.error = this.error.bind(this) || this.debugError.bind(this); - //this.log.debug = this.debug.bind(this); // only load if configured if (!config) { return; @@ -38,62 +41,48 @@ export class MeaterPlatform implements DynamicPlatformPlugin { // Plugin options into our config variables. this.config = { - platform: 'MeaterPlatform', - email: config.email as string, - password: config.password as string, - token: config.token as string, - logging: config.logging as string, + platform: 'Meater', + credentials: config.credentials, + options: config.options, }; - this.logType(); - this.info((`Finished initializing platform: ${config.name}`)); + this.platformConfigOptions(); + this.platformLogs(); + this.debugLog(`Finished initializing platform: ${config.name}`); - this.debug('Debug logging on. Expect a lot of data.'); // verify the config - try { - this.verifyConfig(); - this.log.debug('Config OK'); - } catch (e: any) { - this.log.error(`Verify Config, Error Message: ${e.message}, Submit Bugs Here: ` + 'https://bit.ly/homebridge-meater-bug-report'); - this.log.error(`Verify Config, Error: ${e}`); - return; - } + (async () => { + try { + await this.verifyConfig(); + this.debugLog('Config OK'); + } catch (e: any) { + this.errorLog(`Verify Config, Error Message: ${e.message}, Submit Bugs Here: https://bit.ly/homebridge-meater-bug-report`); + this.debugErrorLog(`Verify Config, Error: ${e}`); + return; + } + })(); // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', async () => { - this.log.debug('Executed didFinishLaunching callback'); + this.debugLog('Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories try { - this.discoverDevices(); + await this.discoverDevices(); } catch (e: any) { - this.log.error(`Failed to Discover, Error Message: ${e.message}, Submit Bugs Here: ` + 'https://bit.ly/homebridge-meater-bug-report'); - this.log.error(`Failed to Discover, Error: ${e}`); + this.errorLog(`Failed to Discover, Error Message: ${e.message}, Submit Bugs Here: ` + 'https://bit.ly/homebridge-meater-bug-report'); + this.debugErrorLog(`Failed to Discover, Error: ${e}`); } }); } - logType() { - this.debugMode = process.argv.includes('-D') || process.argv.includes('--debug'); - if (this.config.logging === 'debug' || this.config.logging === 'standard' || this.config.logging === 'none') { - this.platformLogging = this.config.options!.logging; - this.log.warn(`Using Config Logging: ${this.platformLogging}`); - } else if (this.debugMode) { - this.platformLogging = 'debugMode'; - this.log.warn(`Using ${this.platformLogging} Logging`); - } else { - this.platformLogging = 'standard'; - this.log.warn(`Using ${this.platformLogging} Logging`); - } - } - /** * This function is invoked when homebridge restores cached accessories from disk at startup. * It should be used to setup event handlers for characteristics and update respective values. */ configureAccessory(accessory: PlatformAccessory) { - this.log.debug(`Loading accessory from cache: ${accessory.displayName}`); + this.debugLog(`Loading accessory from cache: ${accessory.displayName}`); // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory); @@ -103,10 +92,10 @@ export class MeaterPlatform implements DynamicPlatformPlugin { * Verify the config passed to the plugin is valid */ async verifyConfig() { - if (!this.config.email) { + if (!this.config.credentials?.email) { throw new Error('Email not provided'); } - if (!this.config.password) { + if (!this.config.credentials?.password) { throw new Error('Password not provided'); } } @@ -114,12 +103,12 @@ export class MeaterPlatform implements DynamicPlatformPlugin { /** * The openToken was old config. * This method saves the openToken as the token in the config.json file - * @param this.config.credentials.openToken + * @param this.config.credentials.token */ - async updateToken() { + async updateToken(login: { data: { token: any; }; }) { try { // check the new token was provided - if (!this.config.token) { + if (!this.config.credentials?.token) { throw new Error('New token not provided'); } @@ -138,16 +127,21 @@ export class MeaterPlatform implements DynamicPlatformPlugin { throw new Error(`Cannot find config for ${PLATFORM_NAME} in platforms array`); } + // check the .credentials is an object before doing object things with it + if (typeof pluginConfig.credentials !== 'object') { + throw new Error('pluginConfig.credentials is not an object'); + } + // set the refresh token - pluginConfig.token = this.config.token; + pluginConfig.credentials.token = login.data.token; - this.log.warn(`token: ${pluginConfig.token}`); + this.debugWarnLog(`token: ${pluginConfig.credentials.token}`); // save the config, ensuring we maintain pretty json writeFileSync(this.api.user.configPath(), JSON.stringify(currentConfig, null, 4)); this.verifyConfig(); } catch (e: any) { - this.log.error(`Update Token: ${e}`); + this.errorLog(`Update Token: ${e}`); } } @@ -156,68 +150,65 @@ export class MeaterPlatform implements DynamicPlatformPlugin { */ async discoverDevices() { try { - if (this.config.token) { - const { body, statusCode, headers } = await request(meaterUrl, { + if (this.config.credentials?.token) { + const { body, statusCode } = await request(meaterUrl, { method: 'GET', headers: { - 'Authorization': 'Bearer ' + this.config.token, + 'Authorization': 'Bearer ' + this.config.credentials.token, }, }); - this.log.info(`Device body: ${JSON.stringify(body)}`); - this.log.info(`Device statusCode: ${statusCode}`); - this.log.info(`Device headers: ${JSON.stringify(headers)}`); + this.debugLog(`Device statusCode: ${statusCode}`); const device: any = await body.json(); - this.log.info(`Device: ${JSON.stringify(device)}`); - this.log.info(`Device StatusCode: ${device.statusCode}`); + this.debugLog(`Device: ${JSON.stringify(device)}`); + this.debugLog(`Device StatusCode: ${device.statusCode}`); if (statusCode === 200 && device.statusCode === 200) { - this.log.info (`Found ${device.data.devices.length} Devices`); + this.infoLog (`Found ${device.data.devices.length} Devices`); + const deviceLists = device.data.devices; + await this.configureDevices(deviceLists); // Meater Devices - device.data.devices.forEach((device: device & deviceConfig) => { + /*device.data.devices.forEach((device: device & deviceConfig) => { this.createMeter(device); - }); + });*/ } else { this.statusCode(statusCode); this.statusCode(device.statusCode); } } else { const payload = JSON.stringify({ - email: this.config.email, - password: this.config.password, + email: this.config.credentials?.email, + password: this.config.credentials?.password, }); - const { body, statusCode, headers } = await request(meaterUrlLogin, { + const { body, statusCode } = await request(meaterUrlLogin, { body: payload, method: 'POST', headers: { 'content-type': 'application/json' }, }); - this.log.debug(`body: ${JSON.stringify(body)}`); - this.log.debug(`statusCode: ${statusCode}`); - this.log.debug(`headers: ${JSON.stringify(headers)}`); + this.debugLog(`statusCode: ${statusCode}`); const login: any = await body.json(); - this.log.debug(`Login: ${JSON.stringify(login)}`); - this.log.debug(`Login Token: ${JSON.stringify(login.data.token)}`); - this.log.debug(`Login StatusCode: ${login.statusCode}`); - this.config.token = login.data.token; - await this.updateToken(); - this.log.debug(`statusCode: ${statusCode} & devicesAPI StatusCode: ${login.statusCode}`); + this.debugLog(`Login: ${JSON.stringify(login)}`); + this.debugLog(`Login Token: ${JSON.stringify(login.data.token)}`); + this.debugLog(`Login StatusCode: ${login.statusCode}`); + await this.updateToken(login); + this.debugLog(`statusCode: ${statusCode} & devicesAPI StatusCode: ${login.statusCode}`); if (statusCode === 200 && login.statusCode === 200) { - const { body, statusCode, headers } = await request(meaterUrl, { + const { body, statusCode } = await request(meaterUrl, { method: 'GET', headers: { Authorization: `Bearer ${login.data.token}}`, }, }); - this.log.debug(`Device body: ${JSON.stringify(body)}`); - this.log.debug(`Device statusCode: ${statusCode}`); - this.log.debug(`Device headers: ${JSON.stringify(headers)}`); + this.debugLog(`Device statusCode: ${statusCode}`); const device: any = await body.json(); - this.log.debug(`Device: ${JSON.stringify(device)}`); - this.log.debug(`Device StatusCode: ${device.statusCode}`); + this.debugLog(`Device: ${JSON.stringify(device)}`); + this.debugLog(`Device StatusCode: ${device.statusCode}`); if (statusCode === 200 && device.statusCode === 200) { - this.log.info (`Found ${device.data.devices.length} Devices`); + this.infoLog (`Found ${device.data.devices.length} Devices`); + const deviceLists = device.data.devices; + await this.configureDevices(deviceLists); // Meater Devices - device.data.devices.forEach((device: device & deviceConfig) => { + /*device.data.devices.forEach((device: device & deviceConfig) => { this.createMeter(device); - }); + });*/ } else { this.statusCode(statusCode); this.statusCode(device.statusCode); @@ -225,14 +216,41 @@ export class MeaterPlatform implements DynamicPlatformPlugin { } } } catch (e: any) { - this.log.error( + this.errorLog( `Failed to Discover Devices, Error Message: ${JSON.stringify(e.message)}, Submit Bugs Here: ` + 'https://bit.ly/homebridge-meater-bug-report', ); - this.log.error(`Failed to Discover Devices, Error: ${e}`); + this.errorLog(`Failed to Discover Devices, Error: ${e}`); + } + } + + private async configureDevices(deviceLists: any) { + if (!this.config.options?.devices) { + this.debugLog(`No Meater Device Config: ${JSON.stringify(this.config.options?.devices)}`); + const devices = deviceLists.map((v: any) => v); + for (const device of devices) { + await this.createMeter(device); + } + } else { + this.debugLog(`Meater Device Config Set: ${JSON.stringify(this.config.options?.devices)}`); + const deviceConfigs = this.config.options?.devices; + if (deviceLists === undefined) { + deviceLists = deviceConfigs; + } + + const mergeByid = (a1: { id: string; }[], a2: any[]) => a1.map((itm: { id: string; }) => ({ + ...a2.find((item: { id: string; }) => item.id === itm.id && item), + ...itm, + })); + + const devices = mergeByid(deviceLists, deviceConfigs); + this.debugLog(`Resideo Devices: ${JSON.stringify(devices)}`); + for (const device of devices) { + await this.createMeter(device); + } } } - private async createMeter(device: device & deviceConfig) { + private async createMeter(device: device & devicesConfig) { const uuid = this.api.hap.uuid.generate(device.id); // see if an accessory with the same uuid has already been registered and restored from @@ -244,60 +262,76 @@ export class MeaterPlatform implements DynamicPlatformPlugin { if (await this.registerDevice(device)) { // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: existingAccessory.context.device.id = device.id; - this.log.info(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.id}`); + existingAccessory.context.displayName = device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`; + existingAccessory.context.FirmwareRevision = await this.FirmwareRevision(device); + this.infoLog(`Restoring existing accessory from cache: ${existingAccessory.displayName} DeviceID: ${device.id}`); this.api.updatePlatformAccessories([existingAccessory]); // create the accessory handler for the restored accessory // this is imported from `platformAccessory.ts` new Meater(this, existingAccessory, device); - this.log.debug(`uuid: ${device.id}, (${existingAccessory.UUID})`); + this.debugLog(`uuid: ${device.id}, (${existingAccessory.UUID})`); } else { this.unregisterPlatformAccessories(existingAccessory); } } else if (await this.registerDevice(device)) { // the accessory does not yet exist, so we need to create it if (!device.external) { - this.log.info(`Adding new accessory, DeviceID: ${device.id}`); + const displayName = device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`; + this.infoLog(`Adding new accessory, Meater: ${displayName}, id: ${device.id}`); } // create a new accessory - const accessory = new this.api.platformAccessory(`Meater Thermometer (${device.id.slice(0, 4)})`, uuid); + const accessory = new this.api.platformAccessory((device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`), uuid); // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device; accessory.context.device.id = device.id; + accessory.context.displayName = device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`; + accessory.context.FirmwareRevision = await this.FirmwareRevision(device); // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` new Meater(this, accessory, device); - this.log.debug(`uuid: ${device.id}, (${accessory.UUID})`); + this.debugLog(`uuid: ${device.id}, (${accessory.UUID})`); // publish device externally or link the accessory to your platform this.externalOrPlatform(device, accessory); this.accessories.push(accessory); } else { - this.log.debug(`Device not registered, DeviceID: ${device.id}`); + this.debugLog(`Device not registered, DeviceID: ${device.id}`); } } - async registerDevice(device: device & deviceConfig): Promise { + async FirmwareRevision(device: device & devicesConfig): Promise { + let firmware: any; + if (device.firmware) { + firmware = device.firmware; + } else { + firmware = await this.getVersion(); + } + return firmware; + } + + async registerDevice(device: device & devicesConfig): Promise { let registerDevice: boolean; if (!device.hide_device) { registerDevice = true; } else { registerDevice = false; - this.log.error( - `DeviceID: ${device.id} will not display in HomeKit, hide_device: ${device.hide_device}`, + const displayName = device.configDeviceName || `Meater Thermometer (${device.id.slice(0, 4)})`; + this.errorLog( + `Meater: ${displayName}, id: ${device.id} will not display in HomeKit, hide_device: ${device.hide_device}`, ); } return registerDevice; } - public async externalOrPlatform(device, accessory: PlatformAccessory) { + public async externalOrPlatform(device: device & devicesConfig, accessory: PlatformAccessory) { if (device.external) { - this.log.warn(`${accessory.displayName} External Accessory Mode`); + this.warnLog(`${accessory.displayName} External Accessory Mode`); this.externalAccessory(accessory); } else { - this.log.debug(`${accessory.displayName} External Accessory Mode: ${device.external}`); + this.debugLog(`${accessory.displayName} External Accessory Mode: ${device.external}`); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } @@ -309,85 +343,133 @@ export class MeaterPlatform implements DynamicPlatformPlugin { public unregisterPlatformAccessories(existingAccessory: PlatformAccessory) { // remove platform accessories when no longer present this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); - this.log.warn(`Removing existing accessory from cache: ${existingAccessory.displayName}`); + this.warnLog(`Removing existing accessory from cache: ${existingAccessory.displayName}`); } async statusCode(statusCode: number): Promise { switch (statusCode) { case 200: - this.log.debug(`Standard Response, statusCode: ${statusCode}`); + this.debugLog(`Standard Response, statusCode: ${statusCode}`); break; case 400: - this.log.error(`Bad Request, statusCode: ${statusCode}`); + this.errorLog(`Bad Request, statusCode: ${statusCode}`); break; case 401: - this.log.error(`Unauthorized, statusCode: ${statusCode}`); + this.errorLog(`Unauthorized, statusCode: ${statusCode}`); break; case 404: - this.log.error(`Not Found, statusCode: ${statusCode}`); + this.errorLog(`Not Found, statusCode: ${statusCode}`); break; case 429: - this.log.error(`Too Many Requests, statusCode: ${statusCode}`); + this.errorLog(`Too Many Requests, statusCode: ${statusCode}`); break; case 500: - this.log.error(`Internal Server Error (Meater Server), statusCode: ${statusCode}`); + this.errorLog(`Internal Server Error (Meater Server), statusCode: ${statusCode}`); break; default: - this.log.info(`Unknown statusCode: ${statusCode}, Report Bugs Here: https://bit.ly/homebridge-meater-bug-report`); + this.infoLog(`Unknown statusCode: ${statusCode}, Report Bugs Here: https://bit.ly/homebridge-meater-bug-report`); } } - // Utility for debug logging. - public info(message: string, ...parameters: unknown[]): void { - if (this.enablingPlatfromLogging()) { - this.log.info(util.format(message, ...parameters)); + async platformConfigOptions() { + const platformConfig: MeaterPlatformConfig['options'] = { + }; + if (this.config.options?.logging) { + platformConfig.logging = this.config.options?.logging; + } + if (this.config.options?.refreshRate) { + platformConfig.refreshRate = this.config.options?.refreshRate; + } + if (Object.entries(platformConfig).length !== 0) { + this.debugLog(`Platform Config: ${JSON.stringify(platformConfig)}`); + } + this.platformConfig = platformConfig; + } + + async platformLogs() { + this.debugMode = process.argv.includes('-D') || process.argv.includes('--debug'); + this.platformLogging = this.config.options?.logging || 'standard'; + if (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard' || this.config.options?.logging === 'none') { + this.platformLogging = this.config.options.logging; + if (this.platformLogging?.includes('debug')) { + this.debugWarnLog(`Using Config Logging: ${this.platformLogging}`); + } + } else if (this.debugMode) { + this.platformLogging = 'debugMode'; + if (this.platformLogging?.includes('debug')) { + this.debugWarnLog(`Using ${this.platformLogging} Logging`); + } + } else { + this.platformLogging = 'standard'; + if (this.platformLogging?.includes('debug')) { + this.debugWarnLog(`Using ${this.platformLogging} Logging`); + } } + if (this.debugMode) { + this.platformLogging = 'debugMode'; + } + } + + async getVersion() { + const json = JSON.parse( + readFileSync( + new URL('../package.json', import.meta.url), + 'utf-8', + ), + ); + this.debugLog(`Plugin Version: ${json.version}`); + return json.version; } - // Utility for warn logging. - public warn(message: string, ...parameters: unknown[]): void { + /** + * If device level logging is turned on, log to log.warn + * Otherwise send debug logs to log.debug + */ + infoLog(...log: any[]): void { if (this.enablingPlatfromLogging()) { - this.log.info(util.format(message, ...parameters)); + this.log.info(String(...log)); } } - // Utility for debug logging. - public error(message: string, ...parameters: unknown[]): void { + warnLog(...log: any[]): void { if (this.enablingPlatfromLogging()) { - this.log.error(util.format(message, ...parameters)); + this.log.warn(String(...log)); } } - // Utility for debug logging. - public debug(message: string, ...parameters: unknown[]): void { + debugWarnLog(...log: any[]): void { if (this.enablingPlatfromLogging()) { - if (this.config.logging === 'debugMode') { - this.log.debug(util.format(message, ...parameters)); - } else if (this.config.logging === 'debug') { - this.log.info(util.format(message, ...parameters)); + if (this.platformLogging?.includes('debug')) { + this.log.warn('[DEBUG]', String(...log)); } } } - // Utility for debug logging. - public debugError(message: string, ...parameters: unknown[]): void { + errorLog(...log: any[]): void { if (this.enablingPlatfromLogging()) { - if (this.config.logging === 'debugMode') { - this.log.debug(util.format(message, ...parameters)); - } else if (this.config.logging === 'debug') { - this.log.info(util.format(message, ...parameters)); + this.log.error(String(...log)); + } + } + + debugErrorLog(...log: any[]): void { + if (this.enablingPlatfromLogging()) { + if (this.platformLogging?.includes('debug')) { + this.log.error('[DEBUG]', String(...log)); } } } - // Utility for debug logging. - public debugWarn(message: string, ...parameters: unknown[]): void { + debugLog(...log: any[]): void { if (this.enablingPlatfromLogging()) { - this.log.info(util.format(message, ...parameters)); + if (this.platformLogging === 'debugMode') { + this.log.debug(String(...log)); + } else if (this.platformLogging === 'debug') { + this.log.info('[DEBUG]', String(...log)); + } } } - private enablingPlatfromLogging() { - return this.config.logging?.includes('debug') || this.config.logging === 'standard'; + enablingPlatfromLogging(): boolean { + return this.platformLogging?.includes('debug') || this.platformLogging === 'standard'; } } diff --git a/src/settings.ts b/src/settings.ts index 2927344..9422457 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,3 +1,7 @@ +/* Copyright(C) 2023-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * setting.ts: homebridge-meater. + */ /* eslint-disable max-len */ import { PlatformConfig } from 'homebridge'; /** @@ -21,16 +25,30 @@ export const meaterUrlLogin = 'https://public-api.cloud.meater.com/v1/login'; //Config export interface MeaterPlatformConfig extends PlatformConfig { + credentials?: credentials; + options?: options | Record; +} + +export type credentials = { email?: string; password?: string; token?: string; - logging?: string; +}; + +export type options = { + devices?: Array; refreshRate?: number; -} + logging?: string; +}; -export type deviceConfig = { - hide_device: boolean; - external: boolean; +export interface devicesConfig extends device { + id: string; + configDeviceName?: string; + hide_device?: boolean; + firmware?: string; + external?: boolean; + refreshRate?: number; + logging?: string; } export type getDevice = { @@ -49,6 +67,11 @@ export type device = { temperature: Temperature; cook: Cook; updated_at: number; + data: deviceData; +} + +export type deviceData = { + temperature: Temperature; } export type Temperature = {