diff --git a/.eslintrc b/.eslintrc
index 7da6ddc..9096301 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -11,7 +11,8 @@
},
"ignorePatterns": [
"dist",
- "homebridge-ui"
+ "homebridge-ui",
+ "chrome-profile"
],
"rules": {
"quotes": [
diff --git a/.gitignore b/.gitignore
index eb3764e..17f6d73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,7 @@ test/hbConfig/.uix-secrets
/test/hbConfig/persist
/test/hbConfig/accessories
test/hbConfig/config.json
+test/hbConfig/homebridge-gsh-discovery.json
+test/hbConfig/auth.json
+test/testPasswords
+/chrome-profile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c18826..111dfdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,53 @@
All notable changes to `homebridge-gsh` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/).
+## v4.0.2 (2025-06-19)
+
+### Changes
+
+- Launch of Subscription Model for existing users
+
+## v4.0.1 (2025-06-06)
+
+### Changes
+
+- Added new Plugin Config help message for the linking status to Google Home App
+- Added support for Dark Mode switching in the Plugin Config screens, need Homebridge UI version 5.x
+- Added two new config options to support more complex configurations, Discovery Wait and Discovery Timeout #19
+- Add activeThermostatMode to thermostat query response #17, tks @rukuh
+
+### Fixes
+
+- Fix for Thermostat changing modes - Error executing command #12
+
+## v4.0.0 (2025-05-12)
+
+### Changes
+
+- Refreshed plugin config screen to include User ID and updated link account images
+- Added more documentation for new users of the service
+- Migrated cloud server address from older DNS entry to newer DNS entry
+- Added new feature to debug mode, logging of the discovered homebridge services to `homebridge-gsh-discovery.json`. To assist in debugging of issues
+
+### Fixes
+
+- Fix for Thermostat changing modes - Error executing command #12
+
+## v3.1.2 (2025-02-18)
+
+### Changes
+
+- New feature for Heater Cooler devices and the ability to have it be an AC Unit - #13 Tks @noamcohen97
+
+### Fixes
+
+- Fixed an issue with Security Services, and automations not triggering on Security Mode changes
+- Fixed an issue with hap event processing, and not handling concurrent events
+
+### Outstanding Issue
+
+- Changing thermostat modes - not working #12
+
## v3.1.1 (2024-12-05)
### Changes
diff --git a/README.md b/README.md
index a9c5639..af2eb19 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
[](https://paypal.me/oznu) -->
-Control your supported [Homebridge](https://github.com/nfarina/homebridge) accessories from any Google Home speaker or the Google Home mobile app. Inspired by [homebridge-alexa](https://github.com/NorthernMan54/homebridge-alexa).
+Control your supported [Homebridge](https://github.com/homebridge/homebridge) accessories from any Google Home speaker or the Google Home mobile app. Inspired by [homebridge-alexa](https://github.com/NorthernMan54/homebridge-alexa).
- [Supported Device Types](#supported-device-types)
- [Installation Instructions](#installation-instructions)
@@ -55,11 +55,17 @@ Control your supported [Homebridge](https://github.com/nfarina/homebridge) acces
_Note: Google Smart Home does not currently support all "sensor" devices such as Motion Sensors or Occupancy Sensors etc._
+## Subscription Required
+
+As of May 2025, homebridge-gsh requires a subscription to operate. This change supports the maintenance of the secure cloud infrastructure that enables integration with Google Smart Home. New users will receive a 7-day free trial. Existing users will be gradually enrolled starting in mid June 2025.
+
+For more details on pricing, enrollment, and frequently asked questions, please visit the [Subscription Service wiki page](https://github.com/homebridge-plugins/homebridge-gsh/wiki/Subscription-Service).
+
## Installation Instructions
#### Option 1: Install via Homebridge Config UI X:
-Search for "Google Home" in [homebridge-config-ui-x](https://github.com/oznu/homebridge-config-ui-x) and install `homebridge-gsh`.
+Search for "Google Home" in [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x) and install `homebridge-gsh`.
#### Option 2: Manually Install:
@@ -69,27 +75,27 @@ sudo npm install -g homebridge-gsh
## Configuration
-To configure `homebridge-gsh` you must also be running [homebridge-config-ui-x](https://github.com/oznu/homebridge-config-ui-x).
+To configure `homebridge-gsh` you must also be running [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x).
-1. Navigate to the Plugins page in [homebridge-config-ui-x](https://github.com/oznu/homebridge-config-ui-x).
+1. Navigate to the Plugins page in [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x).
2. Click the **Settings** button for the Google Smart Home plugin.
3. Click the **Link Account** button.
4. Sign in with your Google or GitHub account.
5. Your account is now linked.
6. Restart Homebridge for the changes to take effect.
-7. Add the [Homebridge Action](https://assistant.google.com/services/a/uid/000000b558f0d5d1?hl=en) using the Google Home mobile app. [See Wiki](https://github.com/oznu/homebridge-gsh/wiki#add-homebridge-to-google-home-app) for detailed instructions.
+7. Add the [Homebridge Action](https://assistant.google.com/services/a/uid/000000b558f0d5d1?hl=en) using the Google Home mobile app. [See Wiki](https://github.com/homebridge/homebridge-gsh/wiki#add-homebridge-to-google-home-app) for detailed instructions.

### Enabling Accessory Control
-Homebridge must be running in insecure mode to allow accessory control via this plugin. See [Enabling Accessory Control](https://github.com/oznu/homebridge-config-ui-x/wiki/Enabling-Accessory-Control) for instructions.
+Homebridge must be running in insecure mode to allow accessory control via this plugin. See [Enabling Accessory Control](https://github.com/homebridge/homebridge-config-ui-x/wiki/Enabling-Accessory-Control) for instructions.
### Multiple Homebridge Instances
This plugin **must** only be configured on one Homebridge instance on your network as the plugin will discover all your other Homebridge instances and be able to control them. For this to work:
-- all instances must be running [in insecure mode](https://github.com/oznu/homebridge-config-ui-x/wiki/Enabling-Accessory-Control)
+- all instances must be running [in insecure mode](https://github.com/homebridge/homebridge-config-ui-x/wiki/Enabling-Accessory-Control)
- all instances must have the same PIN defined in the `config.json`
## Known Issues
@@ -112,7 +118,7 @@ sudo npm install -g homebridge-gsh
#### 2. Cannot control accessories
-See [Enabling Accessory Control](https://github.com/oznu/homebridge-config-ui-x/wiki/Enabling-Accessory-Control) and [Multiple Homebridge Instances](#multiple-homebridge-instances).
+See [Enabling Accessory Control](https://github.com/homebridge/homebridge-config-ui-x/wiki/Enabling-Accessory-Control) and [Multiple Homebridge Instances](#multiple-homebridge-instances).
#### 3. Ask on Discord
@@ -128,7 +134,7 @@ Please see [CONTRIBUTING.md](CONTRIBUTING.md).
## License
-Copyright (C) 2019 oznu
+Copyright (C) 2025 Homebridge
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
diff --git a/config.schema.json b/config.schema.json
index fa3ee3f..4f9bb46 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -74,10 +74,26 @@
"title": "Force Degrees Fahrenheit",
"type": "boolean"
},
+ "showHeaterCoolerAsACUnit": {
+ "title": "Force Heater-Cooler devices to show as AC_UNIT",
+ "type": "boolean"
+ },
"betaServer": {
"title": "Use beta cloud server",
"type": "boolean",
"default": false
+ },
+ "discoveryTimeout": {
+ "type": "integer",
+ "default": 5,
+ "minimum": 5,
+ "maximum": 300
+ },
+ "discoveryWait": {
+ "type": "integer",
+ "default": 15,
+ "minimum": 15,
+ "maximum": 300
}
}
},
@@ -195,7 +211,22 @@
"forceFahrenheit",
{
"type": "help",
- "helpvalue": "
Beta Cloud Used for cloud server testing only. Change plugin cloud endpoint to beta test server. "
+ "helpvalue": "Force Heater-Cooler devices to show as AC_UNIT This makes heater cooler advertise as an air conditioning unit. "
+ },
+ "showHeaterCoolerAsACUnit",
+ {
+ "type": "help",
+ "helpvalue": "Delay before starting Discovery Time in seconds after plugin startup to wait before starting discovery. Default is 15 seconds "
+ },
+ "discoveryWait",
+ {
+ "type": "help",
+ "helpvalue": "Discovery Timeout Time in seconds after last Homebridge instance is discovered to publish devices to Google. Default is 5 seconds "
+ },
+ "discoveryTimeout",
+ {
+ "type": "help",
+ "helpvalue": "Beta Testing Cloud Server Used for cloud server testing only. Change plugin cloud endpoint to beta test server. "
},
"betaServer"
]
diff --git a/homebridge-ui/public/angular.json b/homebridge-ui/public/angular.json
index 519f789..8b2e4bd 100644
--- a/homebridge-ui/public/angular.json
+++ b/homebridge-ui/public/angular.json
@@ -127,4 +127,4 @@
"@angular-eslint/schematics"
]
}
-}
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/package-lock.json b/homebridge-ui/public/package-lock.json
index 6ce546b..0733e18 100644
--- a/homebridge-ui/public/package-lock.json
+++ b/homebridge-ui/public/package-lock.json
@@ -20,6 +20,7 @@
"@homebridge/plugin-ui-utils": "1.0.3",
"@typescript-eslint/types": "^8.11.0",
"@typescript-eslint/utils": "^8.11.0",
+ "marked": "^15.0.11",
"rxjs": "~7.8.1",
"tslib": "^2.8.0",
"zone.js": "~0.13.0"
@@ -329,11 +330,14 @@
"license": "Apache-2.0"
},
"node_modules/@angular-devkit/build-angular/node_modules/@babel/code-frame": {
- "version": "7.25.9",
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/highlight": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
@@ -786,50 +790,42 @@
}
},
"node_modules/@angular-devkit/build-angular/node_modules/@babel/helpers": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+ "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/template": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/@babel/helpers/node_modules/@babel/template": {
- "version": "7.25.9",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@angular-devkit/build-angular/node_modules/@babel/highlight": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+ "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/@babel/parser": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.9"
+ "@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2022,7 +2018,9 @@
}
},
"node_modules/@angular-devkit/build-angular/node_modules/@babel/types": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+ "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2698,17 +2696,6 @@
"node": ">=8"
}
},
- "node_modules/@angular-devkit/build-angular/node_modules/ansi-styles": {
- "version": "3.2.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular-devkit/build-angular/node_modules/anymatch": {
"version": "3.1.3",
"dev": true,
@@ -3123,19 +3110,6 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/@angular-devkit/build-angular/node_modules/chalk": {
- "version": "2.4.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular-devkit/build-angular/node_modules/chardet": {
"version": "0.7.0",
"dev": true,
@@ -3242,19 +3216,6 @@
"node": ">=6"
}
},
- "node_modules/@angular-devkit/build-angular/node_modules/color-convert": {
- "version": "1.9.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@angular-devkit/build-angular/node_modules/color-name": {
- "version": "1.1.3",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@angular-devkit/build-angular/node_modules/colorette": {
"version": "2.0.20",
"dev": true,
@@ -3552,7 +3513,9 @@
}
},
"node_modules/@angular-devkit/build-angular/node_modules/cross-spawn": {
- "version": "7.0.3",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4055,7 +4018,9 @@
}
},
"node_modules/@angular-devkit/build-angular/node_modules/express": {
- "version": "4.21.1",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4078,7 +4043,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -4093,6 +4058,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/@angular-devkit/build-angular/node_modules/express/node_modules/debug": {
@@ -4470,14 +4439,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@angular-devkit/build-angular/node_modules/has-flag": {
- "version": "3.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular-devkit/build-angular/node_modules/has-property-descriptors": {
"version": "1.0.2",
"dev": true,
@@ -5128,6 +5089,8 @@
},
"node_modules/@angular-devkit/build-angular/node_modules/js-tokens": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
@@ -5740,7 +5703,9 @@
"license": "ISC"
},
"node_modules/@angular-devkit/build-angular/node_modules/nanoid": {
- "version": "3.3.7",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@@ -6265,7 +6230,9 @@
"license": "MIT"
},
"node_modules/@angular-devkit/build-angular/node_modules/path-to-regexp": {
- "version": "0.1.10",
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true,
"license": "MIT"
},
@@ -7408,17 +7375,6 @@
"node": ">=6"
}
},
- "node_modules/@angular-devkit/build-angular/node_modules/supports-color": {
- "version": "5.5.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular-devkit/build-angular/node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"dev": true,
@@ -8934,10 +8890,11 @@
}
},
"node_modules/@angular-eslint/schematics/node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
- "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz",
+ "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@@ -8957,6 +8914,7 @@
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -8967,6 +8925,7 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -8991,6 +8950,7 @@
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -9002,6 +8962,7 @@
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.3",
@@ -9017,6 +8978,7 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=12.22"
@@ -9032,6 +8994,7 @@
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead",
"dev": true,
+ "license": "BSD-3-Clause",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/@nodelib/fs.scandir": {
@@ -9198,17 +9161,19 @@
}
},
"node_modules/@angular-eslint/schematics/node_modules/@ungap/structured-clone": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
- "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true,
+ "license": "ISC",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
@@ -9222,6 +9187,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -9232,6 +9198,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -9249,6 +9216,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -9259,6 +9227,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"color-convert": "^2.0.1"
@@ -9275,6 +9244,7 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
+ "license": "Python-2.0",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/aria-query": {
@@ -9378,6 +9348,7 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -9388,6 +9359,7 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -9405,6 +9377,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"color-name": "~1.1.4"
@@ -9418,6 +9391,7 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/concat-map": {
@@ -9426,10 +9400,11 @@
"license": "MIT"
},
"node_modules/@angular-eslint/schematics/node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"path-key": "^3.1.0",
@@ -9492,6 +9467,7 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/define-data-property": {
@@ -9542,6 +9518,7 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"esutils": "^2.0.2"
@@ -9593,6 +9570,7 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -9607,6 +9585,7 @@
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@@ -9711,6 +9690,7 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
@@ -9728,6 +9708,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=4.0"
@@ -9738,6 +9719,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
+ "license": "ISC",
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
@@ -9751,6 +9733,7 @@
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"acorn": "^8.9.0",
@@ -9769,6 +9752,7 @@
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
+ "license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.1.0"
@@ -9782,6 +9766,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=4.0"
@@ -9819,6 +9804,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -9829,6 +9815,7 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/fast-glob": {
@@ -9851,6 +9838,7 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/fast-levenshtein": {
@@ -9858,6 +9846,7 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/fastq": {
@@ -9873,6 +9862,7 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"flat-cache": "^3.0.4"
@@ -9897,6 +9887,7 @@
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"locate-path": "^6.0.0",
@@ -9914,6 +9905,7 @@
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"flatted": "^3.2.9",
@@ -9925,10 +9917,11 @@
}
},
"node_modules/@angular-eslint/schematics/node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
+ "license": "ISC",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/for-each": {
@@ -10013,6 +10006,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"type-fest": "^0.20.2"
@@ -10059,6 +10053,7 @@
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/has-bigints": {
@@ -10074,6 +10069,7 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -10146,10 +10142,11 @@
}
},
"node_modules/@angular-eslint/schematics/node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
@@ -10167,6 +10164,7 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.19"
@@ -10337,6 +10335,7 @@
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -10446,6 +10445,7 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
+ "license": "ISC",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/js-yaml": {
@@ -10453,6 +10453,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
@@ -10466,6 +10467,7 @@
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/json-schema-traverse": {
@@ -10473,6 +10475,7 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/json-stable-stringify-without-jsonify": {
@@ -10480,6 +10483,7 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/keyv": {
@@ -10487,6 +10491,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"json-buffer": "3.0.1"
@@ -10497,6 +10502,7 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
@@ -10511,6 +10517,7 @@
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-locate": "^5.0.0"
@@ -10527,6 +10534,7 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/merge2": {
@@ -10570,6 +10578,7 @@
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/object-inspect": {
@@ -10636,6 +10645,7 @@
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
@@ -10654,6 +10664,7 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -10670,6 +10681,7 @@
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-limit": "^3.0.2"
@@ -10686,6 +10698,7 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
@@ -10699,6 +10712,7 @@
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -10717,6 +10731,7 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -10754,6 +10769,7 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8.0"
@@ -10764,6 +10780,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -10810,6 +10827,7 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
@@ -10906,6 +10924,7 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -10919,6 +10938,7 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -10965,6 +10985,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -10989,6 +11010,7 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"has-flag": "^4.0.0"
@@ -11002,6 +11024,7 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@angular-eslint/schematics/node_modules/tmp": {
@@ -11050,6 +11073,7 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
@@ -11063,6 +11087,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
+ "license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
@@ -11076,6 +11101,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"punycode": "^2.1.0"
@@ -11086,6 +11112,7 @@
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
+ "license": "ISC",
"peer": true,
"dependencies": {
"isexe": "^2.0.0"
@@ -11152,6 +11179,7 @@
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -11167,6 +11195,7 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -11957,7 +11986,9 @@
"license": "ISC"
},
"node_modules/@angular/cli/node_modules/cross-spawn": {
- "version": "7.0.3",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14239,11 +14270,14 @@
}
},
"node_modules/@angular/compiler-cli/node_modules/@babel/code-frame": {
- "version": "7.25.9",
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+ "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/highlight": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
@@ -14398,37 +14432,27 @@
}
},
"node_modules/@angular/compiler-cli/node_modules/@babel/helpers": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+ "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.25.9"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@angular/compiler-cli/node_modules/@babel/highlight": {
- "version": "7.25.9",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.25.9",
- "chalk": "^2.4.2",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.0.0"
+ "@babel/template": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@angular/compiler-cli/node_modules/@babel/parser": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.9"
+ "@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -14438,13 +14462,15 @@
}
},
"node_modules/@angular/compiler-cli/node_modules/@babel/template": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+ "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
@@ -14468,7 +14494,9 @@
}
},
"node_modules/@angular/compiler-cli/node_modules/@babel/types": {
- "version": "7.25.9",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+ "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14530,17 +14558,6 @@
"node": ">=8"
}
},
- "node_modules/@angular/compiler-cli/node_modules/ansi-styles": {
- "version": "3.2.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular/compiler-cli/node_modules/anymatch": {
"version": "3.1.3",
"dev": true,
@@ -14625,19 +14642,6 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/@angular/compiler-cli/node_modules/chalk": {
- "version": "2.4.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular/compiler-cli/node_modules/chokidar": {
"version": "3.6.0",
"dev": true,
@@ -14674,19 +14678,6 @@
"node": ">=12"
}
},
- "node_modules/@angular/compiler-cli/node_modules/color-convert": {
- "version": "1.9.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@angular/compiler-cli/node_modules/color-name": {
- "version": "1.1.3",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@angular/compiler-cli/node_modules/convert-source-map": {
"version": "1.9.0",
"dev": true,
@@ -14734,14 +14725,6 @@
"node": ">=6"
}
},
- "node_modules/@angular/compiler-cli/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/@angular/compiler-cli/node_modules/fill-range": {
"version": "7.1.1",
"dev": true,
@@ -14800,14 +14783,6 @@
"node": ">=4"
}
},
- "node_modules/@angular/compiler-cli/node_modules/has-flag": {
- "version": "3.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular/compiler-cli/node_modules/is-binary-path": {
"version": "2.1.0",
"dev": true,
@@ -14856,6 +14831,8 @@
},
"node_modules/@angular/compiler-cli/node_modules/js-tokens": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
@@ -14993,17 +14970,6 @@
"node": ">=8"
}
},
- "node_modules/@angular/compiler-cli/node_modules/supports-color": {
- "version": "5.5.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@angular/compiler-cli/node_modules/to-regex-range": {
"version": "5.0.1",
"dev": true,
@@ -16109,31 +16075,33 @@
}
},
"node_modules/eslint": {
- "version": "9.13.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz",
- "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==",
+ "version": "9.25.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
+ "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.11.0",
- "@eslint/config-array": "^0.18.0",
- "@eslint/core": "^0.7.0",
- "@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.13.0",
- "@eslint/plugin-kit": "^0.2.0",
- "@humanfs/node": "^0.16.5",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.0",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.13.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.25.1",
+ "@eslint/plugin-kit": "^0.2.8",
+ "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.3.1",
+ "@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
- "cross-spawn": "^7.0.2",
+ "cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.1.0",
- "eslint-visitor-keys": "^4.1.0",
- "espree": "^10.2.0",
+ "eslint-scope": "^8.3.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -16147,8 +16115,7 @@
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
- "optionator": "^0.9.3",
- "text-table": "^0.2.0"
+ "optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
@@ -16169,9 +16136,10 @@
}
},
"node_modules/eslint/node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
- "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz",
+ "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
@@ -16190,6 +16158,7 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -16202,18 +16171,20 @@
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/eslint/node_modules/@eslint/config-array": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
- "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
+ "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
- "@eslint/object-schema": "^2.1.4",
+ "@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -16221,19 +16192,34 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/eslint/node_modules/@eslint/config-helpers": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
+ "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/eslint/node_modules/@eslint/core": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
- "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
+ "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
+ "license": "Apache-2.0",
"peer": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/@eslint/eslintrc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
- "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -16254,29 +16240,33 @@
}
},
"node_modules/eslint/node_modules/@eslint/js": {
- "version": "9.13.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
- "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==",
+ "version": "9.25.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
+ "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/@eslint/object-schema": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz",
- "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==",
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/@eslint/plugin-kit": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz",
- "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==",
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
+ "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
+ "@eslint/core": "^0.13.0",
"levn": "^0.4.1"
},
"engines": {
@@ -16287,6 +16277,7 @@
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18.0"
@@ -16296,6 +16287,7 @@
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanfs/core": "^0.19.1",
@@ -16305,10 +16297,25 @@
"node": ">=18.18.0"
}
},
+ "node_modules/eslint/node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/eslint/node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=12.22"
@@ -16319,9 +16326,10 @@
}
},
"node_modules/eslint/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
+ "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18"
@@ -16332,21 +16340,24 @@
}
},
"node_modules/eslint/node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
@@ -16359,6 +16370,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -16368,6 +16380,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -16384,6 +16397,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"color-convert": "^2.0.1"
@@ -16399,18 +16413,21 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0",
"peer": true
},
"node_modules/eslint/node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
@@ -16421,6 +16438,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -16430,6 +16448,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -16446,6 +16465,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"color-name": "~1.1.4"
@@ -16458,18 +16478,21 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"path-key": "^3.1.0",
@@ -16481,9 +16504,10 @@
}
},
"node_modules/eslint/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.1.3"
@@ -16501,12 +16525,14 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -16516,9 +16542,10 @@
}
},
"node_modules/eslint/node_modules/eslint-scope": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
- "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
+ "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
@@ -16535,6 +16562,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -16547,6 +16575,7 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"acorn": "^8.14.0",
@@ -16564,6 +16593,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.1.0"
@@ -16576,6 +16606,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.2.0"
@@ -16588,6 +16619,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=4.0"
@@ -16597,6 +16629,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -16606,24 +16639,28 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"flat-cache": "^4.0.0"
@@ -16636,6 +16673,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"locate-path": "^6.0.0",
@@ -16652,6 +16690,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"flatted": "^3.2.9",
@@ -16662,15 +16701,17 @@
}
},
"node_modules/eslint/node_modules/flatted": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
- "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "license": "ISC",
"peer": true
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
@@ -16683,6 +16724,7 @@
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
@@ -16695,6 +16737,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -16704,15 +16747,17 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/eslint/node_modules/import-fresh": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
- "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
@@ -16729,6 +16774,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.19"
@@ -16738,6 +16784,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -16747,6 +16794,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"is-extglob": "^2.1.1"
@@ -16759,12 +16807,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC",
"peer": true
},
"node_modules/eslint/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
@@ -16777,24 +16827,28 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"json-buffer": "3.0.1"
@@ -16804,6 +16858,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
@@ -16817,6 +16872,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-locate": "^5.0.0"
@@ -16832,12 +16888,14 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -16850,18 +16908,21 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "license": "MIT",
"peer": true
},
"node_modules/eslint/node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
@@ -16879,6 +16940,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -16894,6 +16956,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-limit": "^3.0.2"
@@ -16909,6 +16972,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
@@ -16921,6 +16985,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -16930,6 +16995,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -16939,6 +17005,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8.0"
@@ -16948,6 +17015,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -16957,6 +17025,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
@@ -16966,6 +17035,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -16978,6 +17048,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -16987,6 +17058,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -16999,6 +17071,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"has-flag": "^4.0.0"
@@ -17007,16 +17080,11 @@
"node": ">=8"
}
},
- "node_modules/eslint/node_modules/text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "peer": true
- },
"node_modules/eslint/node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
@@ -17029,6 +17097,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"punycode": "^2.1.0"
@@ -17038,6 +17107,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
"peer": true,
"dependencies": {
"isexe": "^2.0.0"
@@ -17053,6 +17123,7 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -17062,6 +17133,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -18940,6 +19012,18 @@
"node": ">=10"
}
},
+ "node_modules/marked": {
+ "version": "15.0.11",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz",
+ "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/rxjs": {
"version": "7.8.1",
"license": "Apache-2.0",
diff --git a/homebridge-ui/public/package.json b/homebridge-ui/public/package.json
index 7c92151..865f7c4 100644
--- a/homebridge-ui/public/package.json
+++ b/homebridge-ui/public/package.json
@@ -6,6 +6,7 @@
"ng": "ng",
"start": "ng serve --port 4500 --no-live-reload",
"build": "ng build --configuration production --output-path ../../dist/homebridge-ui/public",
+ "testbuild": "ng build --output-path ../../dist/homebridge-ui/public",
"test": "ng test",
"lint": "ng lint"
},
@@ -22,6 +23,7 @@
"@homebridge/plugin-ui-utils": "1.0.3",
"@typescript-eslint/types": "^8.11.0",
"@typescript-eslint/utils": "^8.11.0",
+ "marked": "^15.0.11",
"rxjs": "~7.8.1",
"tslib": "^2.8.0",
"zone.js": "~0.13.0"
@@ -53,4 +55,4 @@
"ts-node": "~10.9.2",
"typescript": "~4.9.0"
}
-}
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/app.component.html b/homebridge-ui/public/src/app/app.component.html
index cd1ccf3..85a9c30 100644
--- a/homebridge-ui/public/src/app/app.component.html
+++ b/homebridge-ui/public/src/app/app.component.html
@@ -4,12 +4,9 @@
{{ 'accessories.message_please_see' | translate }}
-
- https://github.com/oznu/homebridge-config-ui-x/wiki/Enabling-Accessory-Control
+
+ https://github.com/homebridge/homebridge-config-ui-x/wiki/Enabling-Accessory-Control
{{ 'accessories.message_for_more_information' | translate }}
@@ -17,35 +14,82 @@
-
-
+
+
+
+
+
{{ 'plugins.settings.custom.homebridge-gsh.label_link_account' | translate }}
-
-
-
- {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' | translate }}
-
-
-
-
-
- {{ 'plugins.settings.custom.homebridge-gsh.message_homebridge_restart_required' | translate }}
-
+
+
+
+
+
+
+ {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' |
+ translate }}
+
+
+
+
+
+
+ Google Home App:
+
+
+ {{ userData.gshNotLinked ? 'Not Linked' : 'Linked' }}
+
+
+
+
+ User ID:
+ {{ user_id }}
+
+
+
+ Account Status:
+ {{ userData.accountStatus.text }}
+
+
+
+
+
+
+
+ Complete Setup: Search for Homebridge in the Google Home app and link the
+ action to finalize your setup.
+
-
-
- Search for
-
- Homebridge in the Google Home app
-
- and link the action to complete setup.
+
+
+
+
+
+ {{ 'plugins.settings.custom.homebridge-gsh.message_homebridge_restart_required' | translate }}
+
+
-
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/app.component.ts b/homebridge-ui/public/src/app/app.component.ts
index 4a8605b..04e4614 100644
--- a/homebridge-ui/public/src/app/app.component.ts
+++ b/homebridge-ui/public/src/app/app.component.ts
@@ -1,11 +1,13 @@
-import { Component, OnDestroy, OnInit } from '@angular/core'
-import { JwtHelperService } from '@auth0/angular-jwt'
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { JwtHelperService } from '@auth0/angular-jwt';
-import { PluginConfig, PluginSchema, ServerEnvMetadata } from '@homebridge/plugin-ui-utils/dist/ui.interface'
-import { TranslateService } from './translate.service'
-import { SERVER_ADDRESS } from '../../../../src/settings'
+import { PluginConfig, PluginSchema, ServerEnvMetadata } from '@homebridge/plugin-ui-utils/dist/ui.interface';
+import { SERVER_ADDRESS } from '../../../../src/settings';
-const jwtHelper = new JwtHelperService()
+import { TranslateService } from './translate.service';
+import { UserDataService } from './user-data.service';
+
+const jwtHelper = new JwtHelperService();
@Component({
selector: 'app-root',
@@ -13,126 +15,144 @@ const jwtHelper = new JwtHelperService()
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit, OnDestroy {
- private linkDomain: string
- private linkUrl: string
-
- private popup: Window
- private originCheckInterval
+ private linkDomain: string;
+ private linkUrl: string;
+ private popup: Window;
+ private originCheckInterval;
- public pluginConfig: PluginConfig
- public schema: PluginSchema
- public env: ServerEnvMetadata['env'] = window.homebridge.serverEnv.env
+ public pluginConfig: PluginConfig;
+ public schema: PluginSchema;
+ public env: ServerEnvMetadata['env'] = window.homebridge.serverEnv.env;
- public linkType: string
- public justLinked = false
+ public linkType: string;
+ public user_id: string;
+ public justLinked = false;
- public showAdvanced = false
- public ready = false
+ public ready = false;
+ public userData: any;
constructor(
public translateService: TranslateService,
+ private userDataService: UserDataService,
) { }
async ngOnInit(): Promise {
- this.schema = await window.homebridge.getPluginConfigSchema()
- const configBlocks = await window.homebridge.getPluginConfig()
+ this.schema = await window.homebridge.getPluginConfigSchema();
+ const configBlocks = await window.homebridge.getPluginConfig();
if (!configBlocks.length) {
this.pluginConfig = {
name: 'Google Smart Home',
platform: this.schema.pluginAlias,
- }
+ };
} else {
- this.pluginConfig = configBlocks[0]
- window.homebridge.showSchemaForm()
+ this.pluginConfig = configBlocks[0];
+ window.homebridge.showSchemaForm();
}
- this.linkDomain = this.pluginConfig.betaServer ? `https://${SERVER_ADDRESS.beta}` : `https://${SERVER_ADDRESS.prod}`;
+ this.linkDomain = this.pluginConfig.betaServer
+ ? `https://${SERVER_ADDRESS.beta}`
+ : `https://${SERVER_ADDRESS.prod}`;
this.linkUrl = this.linkDomain + '/link-account';
- console.log(this.linkUrl);
- this.parseToken()
- this.ready = true
+
+ this.parseToken();
+ this.ready = true;
window.homebridge.addEventListener('configChanged', (event: MessageEvent) => {
- this.pluginConfig = event.data[0]
- })
+ this.pluginConfig = event.data[0];
+ });
}
async updateConfig() {
- return window.homebridge.updatePluginConfig([this.pluginConfig])
+ return window.homebridge.updatePluginConfig([this.pluginConfig]);
}
linkAccount() {
- window.addEventListener('message', this.windowMessageListener, false)
+ window.addEventListener('message', this.windowMessageListener, false);
- console.log('linkAccount', this.linkUrl);
- const w = 450
- const h = 700
- const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2)
- const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2)
+ const w = 450;
+ const h = 700;
+ const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2);
+ const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2);
this.popup = window.open(
this.linkUrl,
'oznu-google-smart-home-auth',
- 'toolbar=no, location=no, directories=no, status=no, menubar=no scrollbars=no, resizable=no, copyhistory=no, '
- + `width=${w}, height=${h}, top=${y}, left=${x}`,
- )
+ `toolbar=no, location=no, directories=no, status=no, menubar=no scrollbars=no, resizable=no, copyhistory=no, width=${w}, height=${h}, top=${y}, left=${x}`
+ );
- // simple message to popup to provide the current hostname
this.originCheckInterval = setInterval(() => {
- this.popup.postMessage('origin-check', this.linkDomain)
- }, 2000)
+ this.popup.postMessage('origin-check', this.linkDomain);
+ }, 2000);
}
- async processToken(token) {
- clearInterval(this.originCheckInterval)
+ async processToken(token: string) {
+ clearInterval(this.originCheckInterval);
if (this.popup) {
- this.popup.close()
+ this.popup.close();
}
- this.pluginConfig.token = token
- this.pluginConfig.notice = 'Keep your token a secret!'
-
- this.parseToken()
- this.justLinked = true
- await this.updateConfig()
- await window.homebridge.savePluginConfig()
- window.homebridge.showSchemaForm()
+
+ this.pluginConfig.token = token;
+ this.pluginConfig.notice = 'Keep your token a secret!';
+
+ this.parseToken();
+ this.justLinked = true;
+ await this.updateConfig();
+ await window.homebridge.savePluginConfig();
+ window.homebridge.showSchemaForm();
}
parseToken() {
if (this.pluginConfig.token) {
try {
- const decoded = jwtHelper.decodeToken(this.pluginConfig.token)
- this.linkType = decoded.id.split('|')[0].split('-')[0]
+ const decoded = jwtHelper.decodeToken(this.pluginConfig.token);
+ this.linkType = decoded.id.split('|')[0].split('-')[0];
+ this.user_id = decoded.id;
} catch (e) {
window.homebridge.toast.error(
'Invalid account linking token in config.json',
- this.translateService.translations['toast.title_error'],
- )
- delete this.pluginConfig.token
+ this.translateService.translations['toast.title_error']
+ );
+ delete this.pluginConfig.token;
}
}
}
- windowMessageListener = (e) => {
- if (e.origin !== this.linkDomain) {
- return
- }
+ windowMessageListener = (e: MessageEvent) => {
+ if (e.origin !== this.linkDomain) return;
try {
- const data = JSON.parse(e.data)
+ const data = JSON.parse(e.data);
if (data.token) {
- this.processToken(data.token)
+ this.processToken(data.token);
+ } else {
+ console.log('Received message from popup:', data);
}
} catch (e) {
- console.error(e)
+ console.error(e);
}
+ };
+
+ onUserDataChange(userData: any) {
+ this.userData = userData;
}
ngOnDestroy() {
- clearInterval(this.originCheckInterval)
- window.removeEventListener('message', this.windowMessageListener)
+ clearInterval(this.originCheckInterval);
+ window.removeEventListener('message', this.windowMessageListener);
if (this.popup) {
- this.popup.close()
+ this.popup.close();
}
}
-}
+
+ copyToClipboard(input: string): void {
+ navigator.clipboard.writeText(input).then(
+ () => {
+ window.homebridge.toast.success(`Copied ${input} to clipboard`);
+ },
+ (err) => {
+ console.error('❌ Failed to copy:', err);
+ window.homebridge.toast.error('Error', 'Failed to copy');
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/app.module.ts b/homebridge-ui/public/src/app/app.module.ts
index 6b3e289..f103a8f 100644
--- a/homebridge-ui/public/src/app/app.module.ts
+++ b/homebridge-ui/public/src/app/app.module.ts
@@ -1,20 +1,30 @@
-import { NgModule } from '@angular/core'
-import { BrowserModule } from '@angular/platform-browser'
+import { HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { AppComponent } from './app.component';
+import { MarkdownViewerComponent } from './markdown-viewer.component';
+import { UserDataComponent } from './user-data.component';
-import { AppComponent } from './app.component'
-
-import { TranslatePipe } from './translate.pipe'
-import '@homebridge/plugin-ui-utils/dist/ui.interface'
+import '@homebridge/plugin-ui-utils/dist/ui.interface';
+import { TranslatePipe } from './translate.pipe';
+import { DateToStringPipe } from './user-data.pipe';
@NgModule({
declarations: [
AppComponent,
TranslatePipe,
+ MarkdownViewerComponent,
+ UserDataComponent,
+ DateToStringPipe,
],
imports: [
BrowserModule,
+ HttpClientModule,
],
providers: [],
bootstrap: [AppComponent],
+ exports: [
+ DateToStringPipe // so it can be used elsewhere
+ ]
})
export class AppModule { }
diff --git a/homebridge-ui/public/src/app/markdown-viewer.component.html b/homebridge-ui/public/src/app/markdown-viewer.component.html
new file mode 100644
index 0000000..1c62dd4
--- /dev/null
+++ b/homebridge-ui/public/src/app/markdown-viewer.component.html
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/homebridge-ui/public/src/app/markdown-viewer.component.scss b/homebridge-ui/public/src/app/markdown-viewer.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/homebridge-ui/public/src/app/markdown-viewer.component.ts b/homebridge-ui/public/src/app/markdown-viewer.component.ts
new file mode 100644
index 0000000..27c02d8
--- /dev/null
+++ b/homebridge-ui/public/src/app/markdown-viewer.component.ts
@@ -0,0 +1,48 @@
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { marked } from 'marked';
+import { environment } from '../environments/environment';
+
+@Component({
+ selector: 'app-markdown-viewer',
+ templateUrl: './markdown-viewer.component.html',
+ styleUrls: ['./markdown-viewer.component.scss']
+})
+export class MarkdownViewerComponent implements OnChanges {
+ @Input() filename!: string;
+
+ public html = '';
+ public loading = false;
+
+ private readonly baseUrl = 'https://raw.githubusercontent.com/homebridge-plugins/homebridge-gsh/refs/heads/latest/homebridge-ui/public/src/assets/markdown/';
+
+ async ngOnChanges(changes: SimpleChanges): Promise {
+ if (changes['filename'] && this.filename) {
+ await this.loadMarkdown(this.filename);
+ }
+ }
+
+ async loadMarkdown(filename: string): Promise {
+ this.loading = true;
+ this.html = '';
+
+ // Determine path based on TESTING env variable
+
+ const url = !environment.production
+ ? `assets/markdown/${filename}` // Dev/testing: local assets
+ : this.baseUrl + filename;
+
+ console.log('Loading markdown from:', url);
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Failed to fetch ${filename}: ${res.status}`);
+ const markdown = await res.text();
+ this.html = await marked.parse(markdown);
+ } catch (err: any) {
+ console.error('Markdown fetch failed:', err);
+ window.homebridge.toast.error(`Failed to load ${filename}`, 'Error');
+ } finally {
+ this.loading = false;
+ }
+ }
+
+}
diff --git a/homebridge-ui/public/src/app/user-data.component.html b/homebridge-ui/public/src/app/user-data.component.html
new file mode 100644
index 0000000..d49d154
--- /dev/null
+++ b/homebridge-ui/public/src/app/user-data.component.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+ Create Subscription
+
+
+
+
+ Subscription Start Date: {{ userData.startDate | dateToString }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ plan.name }}
+
{{ plan.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Subscription Details
+
+
+
+
+
+
+
+
+
+
+
Cancel your subscription?
+
Are you sure you want to cancel your {{ userData.paypalPlanName
+ }} subscription? This action cannot be undone.
+
Your service will continue until the next billing date on {{
+ userData.expiryDate | dateToString }} .
+
+
+ Keep Subscription
+
+
+ {{ isCancelling ? 'Cancelling...' : 'Cancel Subscription' }}
+
+
+
+
+
+
+
+
+
+ Plan:
+ {{ userData.paypalPlanName }}
+
+
+ Description:
+ {{ userData.paypalPlanDescription }}
+
+
+ Subscription ID:
+
+
+ {{ userData.paypalSubscriptionID }}
+
+
+
+
+ Next Billing Date:
+ {{ userData.expiryDate | dateToString }}
+
+
+
+
+
+ Cancel Subscription
+
+
+
+
+
+
+
+
+ 📖
+ Subscription Service Details
+
+
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/user-data.component.scss b/homebridge-ui/public/src/app/user-data.component.scss
new file mode 100644
index 0000000..c1f609c
--- /dev/null
+++ b/homebridge-ui/public/src/app/user-data.component.scss
@@ -0,0 +1,29 @@
+/* Chevron before expandable / expanded legends */
+.expandable>legend::before,
+.expanded>legend::before {
+ content: '\f054';
+ /* right-chevron for collapsed */
+ font-family: 'Font Awesome 6 Free', 'FontAwesome';
+ font-weight: 900;
+ padding-right: 0.3em;
+}
+
+.expanded>legend::before {
+ content: '\f078';
+ /* down-chevron for expanded */
+ padding-right: 0.2em;
+}
+
+/* Fieldset and Legend Adjustments */
+fieldset {
+ padding: 0;
+ border: none;
+ /* no border */
+}
+
+legend {
+ font-weight: bold;
+ cursor: pointer;
+ user-select: none;
+ margin-bottom: 0.5rem;
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/user-data.component.ts b/homebridge-ui/public/src/app/user-data.component.ts
new file mode 100644
index 0000000..9d27c50
--- /dev/null
+++ b/homebridge-ui/public/src/app/user-data.component.ts
@@ -0,0 +1,241 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { PluginConfig } from '@homebridge/plugin-ui-utils/dist/ui.interface';
+import { TranslateService } from './translate.service';
+import { LocalUserData, UserDataService } from './user-data.service';
+
+import { GITHUB_REPO } from '../../../../src/settings';
+
+@Component({
+ selector: 'app-user-data',
+ templateUrl: './user-data.component.html',
+ styleUrls: ['./user-data.component.scss']
+})
+export class UserDataComponent implements OnInit {
+ @Input() pluginConfig!: PluginConfig;
+ @Input() linkDomain!: string;
+ @Input() user_id!: string;
+ @Output() userDataChange = new EventEmitter();
+
+ public userData!: LocalUserData;
+ public createSubscriptionExpanded = false;
+ public subscriptionDetailsExpanded = false;
+
+ public isCancelling: boolean = false;
+ public isLoadingPayPalButtons: boolean = true;
+
+ public readonly subscriptionDetailsURL = GITHUB_REPO + 'wiki/Subscription-Service#Background';
+
+ constructor(
+ private userDataService: UserDataService,
+ private translateService: TranslateService
+ ) { }
+
+ ngOnInit(): void {
+ this.loadUserData();
+ }
+
+ async loadUserData() {
+ this.userDataService.getUserData(this.linkDomain, this.pluginConfig.token).subscribe({
+ next: data => {
+ this.userData = data;
+ this.userDataChange.emit(this.userData); // Emit userData to parent
+ console.log('✅ User data loaded:', data);
+ },
+ error: err => {
+ console.log('❌ Failed to load user data:', err);
+ }
+ });
+ }
+
+ cancelSubscription() {
+ this.showConfirmModal = true; // Show modal
+ }
+
+ public showConfirmModal = false;
+
+ async confirmCancel() {
+ this.showConfirmModal = false;
+ this.isCancelling = true; // Start spinner
+
+ if (!this.userData?.paypalSubscriptionID) {
+ console.error('No subscription ID found');
+ window.homebridge.toast.error('Failed to cancel subscription', this.translateService.translations['toast.title_error']);
+ this.isCancelling = false; // Stop spinner
+ return;
+ }
+
+ console.log('Cancelling subscription:', this.userData.paypalSubscriptionID);
+
+ const body = {
+ subscriptionID: this.userData.paypalSubscriptionID
+ };
+
+ fetch(`${this.linkDomain}/userData/cancel`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.pluginConfig.token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body)
+ })
+ .then(async res => {
+ if (!res.ok) {
+ const errorText = await res.text(); // prevent json() on bad body
+ throw new Error(`Backend responded with ${res.status}: ${errorText}`);
+ }
+ return res.json();
+ })
+ .then(response => {
+ if (response.success) {
+ window.homebridge.toast.success('Subscription Cancelled', this.translateService.translations['toast.title_success']);
+ // Refresh UI after cancel
+ this.userDataService.getUserData(this.linkDomain, this.pluginConfig.token).subscribe({
+ next: updatedData => {
+ this.userData = updatedData;
+ this.userDataChange.emit(this.userData); // Emit updated userData
+ console.log('✅ Refreshed user data after cancel:', updatedData);
+ },
+ error: err => {
+ console.error('❌ Failed to refresh user data after cancel:', err);
+ }
+ });
+ } else {
+ window.homebridge.toast.error('Failed to cancel subscription', this.translateService.translations['toast.title_error']);
+ console.error('❌ Failed to cancel subscription:', response.message);
+ }
+ })
+ .catch(err => {
+ // Suppress fetch errors and log cleanly
+ window.homebridge.toast.error('Failed to cancel subscription', this.translateService.translations['toast.title_error']);
+ console.error('❌ Cancel subscription failed:', err.message || err);
+ }).finally(() => {
+ this.isCancelling = false; // Stop spinner
+ });
+ }
+
+ toggleCreateSubscriptionExpand(): void {
+ this.createSubscriptionExpanded = !this.createSubscriptionExpanded;
+ this.subscriptionDetailsExpanded = false;
+
+ if (this.createSubscriptionExpanded) {
+ setTimeout(() => {
+ console.log('Expanded and DOM updated, now rendering PayPal buttons');
+ this.renderPayPalButtons();
+ }, 0);
+ }
+ }
+
+ toggleSubscriptionDetailsExpand() {
+ this.subscriptionDetailsExpanded = !this.subscriptionDetailsExpanded;
+ this.createSubscriptionExpanded = false;
+ }
+
+ renderPayPalButtons(): void {
+ if (!this.createSubscriptionExpanded || !this.userData?.paypalPlans?.length) {
+ console.log('❌ No PayPal plans available or subscription creation not expanded');
+ return;
+ }
+
+ this.userDataService.loadScript()
+ .then(async () => {
+ const renderPromises: Promise[] = [];
+
+ this.userData?.paypalPlans?.forEach((plan, index) => {
+ const container = document.getElementById(`paypal-button-container-${index}`);
+ if (container) {
+ container.innerHTML = '';
+ }
+ renderPromises.push(this.renderPayPalButton(plan, index));
+ });
+
+ try {
+ await Promise.all(renderPromises);
+ } catch (error) {
+ console.error('Error rendering PayPal buttons:', error);
+ } finally {
+ this.isLoadingPayPalButtons = false;
+ console.log('✅ All PayPal buttons rendered');
+ }
+ })
+ .catch(error => {
+ console.error('PayPal script failed to load', error);
+ this.isLoadingPayPalButtons = false;
+ });
+ }
+
+ async renderPayPalButton(plan: any, index: number): Promise {
+ if (!(window as any).paypal?.Buttons) {
+ console.error('PayPal SDK not available.');
+ throw new Error('PayPal SDK not available');
+ }
+
+ console.log(`Rendering PayPal button for plan ${plan.id}`);
+
+ return new Promise((resolve, reject) => {
+ (window as any).paypal.Buttons({
+ createSubscription: (data, actions) => {
+ console.log('Creating subscription for plan:', this.userData.startDate.toISOString());
+ return actions.subscription.create({
+ plan_id: plan.id,
+ custom_id: 'GSH|' + this.user_id,
+ start_time: this.userData.startDate.toISOString(),
+ application_context: {
+ shipping_preference: "NO_SHIPPING"
+ },
+ });
+ },
+ onApprove: async (data, actions) => {
+ console.log('✅ Subscription approved:', data);
+
+ const body = {
+ planId: plan.id,
+ subscriptionID: data.subscriptionID,
+ orderID: data.orderID,
+ paymentSource: data.paymentSource,
+ facilitatorAccessToken: data.facilitatorAccessToken,
+ };
+
+ try {
+ const res = await fetch(`${this.linkDomain}/userData/subscribe`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.pluginConfig.token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body)
+ });
+ const response = await res.json();
+ console.log('✅ Backend subscription saved:', response);
+ window.homebridge.toast.success('Service Subscription Created', this.translateService.translations['toast.title_success']);
+ } catch (err) {
+ console.error('❌ Failed to store subscription in backend:', err);
+ window.homebridge.toast.error('Failed to create subscription', this.translateService.translations['toast.title_error']);
+ }
+
+ this.userDataService.getUserData(this.linkDomain, this.pluginConfig.token).subscribe({
+ next: updatedData => {
+ this.userData = updatedData;
+ this.userDataChange.emit(this.userData); // Emit updated userData
+ console.log('✅ Refreshed user data after subscription:', updatedData);
+ },
+ error: err => {
+ console.error('❌ Failed to refresh user data after subscription:', err);
+ }
+ });
+ },
+ onError: (err) => {
+ console.error(`PayPal error for ${plan.id}:`, err);
+ window.homebridge.toast.error('Failed to create subscription', this.translateService.translations['toast.title_error']);
+ }
+ }).render(`#paypal-button-container-${index}`)
+ .then(() => {
+ console.log(`✅ PayPal button rendered for plan ${plan.id}`);
+ resolve(); // resolve the promise when rendering succeeds
+ })
+ .catch((err) => {
+ console.error(`❌ Failed to render PayPal button for plan ${plan.id}:`, err);
+ reject(err); // reject the promise if rendering fails
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/src/app/user-data.pipe.ts b/homebridge-ui/public/src/app/user-data.pipe.ts
new file mode 100644
index 0000000..0a18b3c
--- /dev/null
+++ b/homebridge-ui/public/src/app/user-data.pipe.ts
@@ -0,0 +1,22 @@
+// src/app/shared/pipes/user-data.pipe.ts (adjust path as needed)
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'dateToString',
+ pure: true,
+})
+export class DateToStringPipe implements PipeTransform {
+ transform(value: string | Date | undefined | null): string {
+ console.log('DateToStringPipe', value);
+ if (!value) return 'N/A';
+
+ const date = value instanceof Date ? value : new Date(value);
+ if (isNaN(date.getTime())) return 'Invalid Date';
+
+ return date.toLocaleDateString('en-GB', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+}
diff --git a/homebridge-ui/public/src/app/user-data.service.ts b/homebridge-ui/public/src/app/user-data.service.ts
new file mode 100644
index 0000000..19f5edc
--- /dev/null
+++ b/homebridge-ui/public/src/app/user-data.service.ts
@@ -0,0 +1,131 @@
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable, map } from 'rxjs';
+
+export interface UserDataResponse {
+ user_id: string;
+
+ subscriptionRequired: boolean; // Causes UI to display subscription box, set for accounts created after launch date, and 30 days after launch for accounts created before launch
+ subscriptionActive: boolean; // Used by client to determine if the subscription is active or not
+ serviceActive: boolean; // Used by client to determine if the service is active or not
+ expiryDate: Date; // Date of the next payment or trial period end - service s/b active until this date
+ subscriptionType: number; // 0 - Contributor, 1 - Trial ( trialExpiryDate), 2 - Vendor Managed Subscription ( no expiry ), 3 - Manual Subscription ( subscriptionExpiryDate )
+
+ accountStatus: { text: string; color: string };
+
+ paypalSubscriptionID: string;
+ // paypalProductID: string;
+ paypalPlanID: string;
+ paypalPlanName?: string;
+ paypalPlanDescription?: string;
+ // paypalSubscribeDate: Date;
+
+ paypalPlans: PayPalPlanResponse[];
+ paypalScript: string;
+};
+
+export interface PayPalPlanResponse {
+ id: string;
+ name: string;
+ description: string;
+}
+
+export interface LocalUserData extends UserDataResponse {
+ startDate?: Date; // client-only field
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserDataService {
+
+ constructor(private http: HttpClient) { }
+
+ private userData: LocalUserData;
+
+ private createHeaders(token: string): HttpHeaders {
+ return new HttpHeaders({
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ });
+ }
+
+ getUserData(domain: string, token: string): Observable {
+
+ if (token) {
+ const headers = this.createHeaders(token);
+ const url = `${domain}/userData/userData`;
+
+ return this.http.get<{ userData: UserDataResponse }>(url, { headers }).pipe(
+ map(response => {
+ this.userData = response.userData;
+ this.userData.startDate = startDate(this.userData.expiryDate);
+ return this.userData;
+ })
+ );
+ } else {
+ return new Observable(observer => {
+ observer.next(this.userData);
+ observer.complete();
+ });
+ }
+ }
+
+ cancelSubscription(subscriptionID: string, linkDomain: string, token: string): Promise {
+ const headers = this.createHeaders(token);
+ return fetch(`${linkDomain}/userData/cancel`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ subscriptionID }),
+ }).then(res => res.json());
+ }
+
+ saveSubscription(details: any, linkDomain: string, token: string): Promise {
+ return fetch(`${linkDomain}/userData/subscribe`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(details),
+ }).then(res => res.json());
+ }
+
+ private scriptLoaded = false;
+
+ loadScript(): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.scriptLoaded) {
+ resolve();
+ return;
+ }
+
+ if (!this.userData?.paypalScript) {
+ reject(new Error('PayPal script URL is not available.'));
+ return;
+ }
+ const script = document.createElement('script');
+ script.src = this.userData?.paypalScript;
+ script.onload = () => {
+ this.scriptLoaded = true;
+ resolve();
+ };
+ script.onerror = reject;
+
+ document.body.appendChild(script);
+ });
+ }
+}
+
+function startDate(expiryDate: Date | string | undefined): Date {
+ const expiry = new Date(expiryDate);
+ const now = new Date();
+ const startDate = !isNaN(expiry.getTime()) && expiry > now
+ ? expiry
+ : new Date(now.getTime() + 10 * 60 * 1000); // Add 10 minutes to current time
+
+ return startDate;
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/src/assets/github-mark.svg b/homebridge-ui/public/src/assets/github-mark.svg
new file mode 100644
index 0000000..37fa923
--- /dev/null
+++ b/homebridge-ui/public/src/assets/github-mark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/assets/google-home.svg b/homebridge-ui/public/src/assets/google-home.svg
new file mode 100644
index 0000000..099132b
--- /dev/null
+++ b/homebridge-ui/public/src/assets/google-home.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/assets/google-logo.svg b/homebridge-ui/public/src/assets/google-logo.svg
new file mode 100644
index 0000000..088288f
--- /dev/null
+++ b/homebridge-ui/public/src/assets/google-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/assets/markdown/EXISTING.md b/homebridge-ui/public/src/assets/markdown/EXISTING.md
new file mode 100644
index 0000000..58e7d8c
--- /dev/null
+++ b/homebridge-ui/public/src/assets/markdown/EXISTING.md
@@ -0,0 +1,31 @@
+Follow the steps below to link your account and start controlling your Homebridge accessories with Google Home.
+
+---
+
+### 1. Link Your Account
+
+Click the **Link Account** button on this page and sign in using your **Google** or **GitHub** account.
+
+---
+
+### 2. Restart Homebridge
+
+After linking your account, restart Homebridge for the changes to take effect.
+
+---
+
+### 3. Add Homebridge to Google Home App
+
+Follow these steps inside the Google Home app:
+
+1. Open the **Google Home** app on your mobile device.
+2. Tap the **➕ Add** button.
+3. Choose **Set up device** → **Works with Google**.
+4. Tap the 🔍 **search icon**, and search for **Homebridge**.
+5. Select **Homebridge** from the list.
+6. Sign in using the same account you linked above.
+7. Your accessories will sync automatically.
+
+> 📖 See the Wiki for more detailed instructions
+
+---
diff --git a/homebridge-ui/public/src/assets/markdown/NEWUSER.md b/homebridge-ui/public/src/assets/markdown/NEWUSER.md
new file mode 100644
index 0000000..17ef5f0
--- /dev/null
+++ b/homebridge-ui/public/src/assets/markdown/NEWUSER.md
@@ -0,0 +1,37 @@
+
+This plugin uses cloud services to integrate with Google Smart Home, which requires ongoing infrastructure and support. A subscription is required after a 7 day trial period to keep the service running smoothly.
+> 📖 Learn more: Subscription Service wiki page
+
+---
+
+Follow the steps below to link your account and start controlling your Homebridge accessories with Google Home.
+
+---
+
+### 1. Link Your Account
+
+Click the **Link Account** button on this page and sign in using your **Google** or **GitHub** account.
+
+---
+
+### 2. Restart Homebridge
+
+After linking your account, restart Homebridge for the changes to take effect.
+
+---
+
+### 3. Add Homebridge to Google Home App
+
+Follow these steps inside the Google Home app:
+
+1. Open the **Google Home** app on your mobile device.
+2. Tap the **➕ Add** button.
+3. Choose **Set up device** → **Works with Google**.
+4. Tap the 🔍 **search icon**, and search for **Homebridge**.
+5. Select **Homebridge** from the list.
+6. Sign in using the same account you linked above.
+7. Your accessories will sync automatically.
+
+> 📖 See the Wiki for more detailed installation instructions
+
+---
diff --git a/homebridge-ui/public/src/environments/environment.prod.ts b/homebridge-ui/public/src/environments/environment.prod.ts
index 970e25b..ad55946 100644
--- a/homebridge-ui/public/src/environments/environment.prod.ts
+++ b/homebridge-ui/public/src/environments/environment.prod.ts
@@ -1,3 +1,4 @@
export const environment = {
production: true,
+ enableLogging: false
}
diff --git a/homebridge-ui/public/src/environments/environment.ts b/homebridge-ui/public/src/environments/environment.ts
index 5c68c17..1a53553 100644
--- a/homebridge-ui/public/src/environments/environment.ts
+++ b/homebridge-ui/public/src/environments/environment.ts
@@ -4,13 +4,6 @@
export const environment = {
production: false,
-}
+ enableLogging: true
+};
-/*
- * For easier debugging in development mode, you can import the following file
- * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
- *
- * This import should be commented out in production mode because it will have a negative impact
- * on performance if an error is thrown.
- */
-// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
diff --git a/homebridge-ui/public/src/index.html b/homebridge-ui/public/src/index.html
index 61ec5dd..5f00356 100644
--- a/homebridge-ui/public/src/index.html
+++ b/homebridge-ui/public/src/index.html
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/homebridge-ui/public/src/main.ts b/homebridge-ui/public/src/main.ts
index 55d15ae..1963327 100644
--- a/homebridge-ui/public/src/main.ts
+++ b/homebridge-ui/public/src/main.ts
@@ -1,9 +1,14 @@
-import { enableProdMode } from '@angular/core'
-import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
-import { AppModule } from './app/app.module'
-import { environment } from './environments/environment'
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+if (!environment.enableLogging) {
+ console.log = () => { };
+ console.warn = () => { };
+ // console.error = () => { };
+}
if (environment.production) {
enableProdMode()
}
diff --git a/homebridge-ui/public/src/styles.scss b/homebridge-ui/public/src/styles.scss
index ed88948..0459257 100644
--- a/homebridge-ui/public/src/styles.scss
+++ b/homebridge-ui/public/src/styles.scss
@@ -1,5 +1,50 @@
-/* You can add global styles to this file, and also import other style files */
+.invert-on-dark path {
+ fill: #000000;
+ /* Black for light mode */
+}
+
+.dark-mode .invert-on-dark path {
+ fill: #ffffff !important;
+ /* White for dark mode, with !important to override potential conflicts */
+}
-.pointer {
- cursor: pointer;
+/* Fallback filter for additional visibility */
+.dark-mode .invert-on-dark {
+ filter: brightness(100);
+ /* Brighten the SVG in dark mode as a fallback */
}
+
+// Override default font size for modal content
+
+.modal-content {
+ font-size: inherit !important;
+}
+
+b,
+strong {
+ font-weight: 700 !important;
+ /* Ensure bold text is consistently styled */
+}
+
+td {
+ color: inherit !important;
+}
+
+.subscription-table th {
+ font-size: 1rem;
+ font-weight: 700;
+ /* Bold labels */
+ line-height: 1;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+}
+
+.subscription-table td {
+ font-size: 1rem;
+ font-weight: 300;
+ // color: var(--bs-body-color) !important;
+ /* Light values */
+ line-height: 1;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+}
\ No newline at end of file
diff --git a/homebridge-ui/public/tsconfig.json b/homebridge-ui/public/tsconfig.json
index 326e89d..ca86f49 100644
--- a/homebridge-ui/public/tsconfig.json
+++ b/homebridge-ui/public/tsconfig.json
@@ -2,7 +2,7 @@
{
"compileOnSave": false,
"compilerOptions": {
- "target": "es2015",
+ "target": "ES2022",
"lib": [
"es2018",
"dom"
@@ -17,4 +17,4 @@
"outDir": "./dist/out-tsc",
"sourceMap": true
}
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 352d902..fe23878 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "homebridge-gsh",
- "version": "3.1.1",
+ "version": "4.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebridge-gsh",
- "version": "3.1.1",
+ "version": "4.0.2",
"bundleDependencies": [
"@homebridge/hap-client",
"@homebridge/ws-connect",
@@ -15,7 +15,7 @@
],
"license": "GPL-3.0",
"dependencies": {
- "@homebridge/hap-client": "^2.0.5",
+ "@homebridge/hap-client": "^2.1.0",
"@homebridge/ws-connect": "^3.0.0",
"chalk": "^5.3.0",
"fs-extra": "^11.2.0",
@@ -34,6 +34,7 @@
"@typescript-eslint/parser": "^7.16.1",
"actions-on-google": "^3.0.0",
"babel-jest": "^29.7.0",
+ "dotenv": "^16.5.0",
"eslint": "^8.57.0",
"eslint-plugin-format": "^0.1.2",
"eslint-plugin-jest": "^28.8.3",
@@ -42,6 +43,7 @@
"jest": "^29.7.0",
"nodemon": "^3.1.7",
"rimraf": "^5.0.1",
+ "selenium-webdriver": "^4.31.0",
"ts-expect": "^1.3.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
@@ -49,7 +51,7 @@
},
"engines": {
"homebridge": "^1.6.0 || ^2.0.0-beta.0",
- "node": "^18.20.4 || ^20.15.1 || ^22.0.0"
+ "node": "^18.20.4 || ^20.15.1 || ^22.0.0 || ^24.0.0"
}
},
"node_modules/@babel/core": {
@@ -208,25 +210,27 @@
}
},
"node_modules/@babel/core/node_modules/@babel/helpers": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
- "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
+ "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.0"
+ "@babel/template": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core/node_modules/@babel/parser": {
- "version": "7.26.2",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
- "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.0"
+ "@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -236,14 +240,15 @@
}
},
"node_modules/@babel/core/node_modules/@babel/template": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
- "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
+ "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.25.9",
- "@babel/parser": "^7.25.9",
- "@babel/types": "^7.25.9"
+ "@babel/code-frame": "^7.26.2",
+ "@babel/parser": "^7.27.0",
+ "@babel/types": "^7.27.0"
},
"engines": {
"node": ">=6.9.0"
@@ -268,10 +273,11 @@
}
},
"node_modules/@babel/core/node_modules/@babel/types": {
- "version": "7.26.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
- "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
+ "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -827,16 +833,16 @@
}
},
"node_modules/@homebridge/hap-client": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@homebridge/hap-client/-/hap-client-2.0.5.tgz",
- "integrity": "sha512-oIWTb9jHJkA5awWEQGu8s2H7DXSUpv+4ZF5RNFOkSmXRu8OZJrC0uV3Ps12or/0IVIWO0jyLJHPOWNUWqZ3/mA==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@homebridge/hap-client/-/hap-client-2.1.0.tgz",
+ "integrity": "sha512-8OpTYaC9McLAWoY6vv8CmYMZ+RnJbsOh8Tc9rGgeuHE3x+Tx+T0hh2B726WAZ5VEgoUNKY8oP+7Zfo/2qQ7SHw==",
"inBundle": true,
"license": "MIT",
"dependencies": {
- "axios": "1.7.9",
+ "axios": "1.8.4",
"bonjour-service": "1.3.0",
"decamelize": "5.0.1",
- "inflection": "3.0.0",
+ "inflection": "3.0.2",
"source-map-support": "0.5.21"
}
},
@@ -855,9 +861,9 @@
"license": "MIT"
},
"node_modules/@homebridge/hap-client/node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -883,6 +889,20 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"inBundle": true
},
+ "node_modules/@homebridge/hap-client/node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/@homebridge/hap-client/node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -931,6 +951,70 @@
"node": ">=6"
}
},
+ "node_modules/@homebridge/hap-client/node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@homebridge/hap-client/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==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/@homebridge/hap-client/node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -960,29 +1044,145 @@
}
},
"node_modules/@homebridge/hap-client/node_modules/form-data": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
- "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"inBundle": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
+ "node_modules/@homebridge/hap-client/node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "inBundle": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@homebridge/hap-client/node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/@homebridge/hap-client/node_modules/inflection": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.0.tgz",
- "integrity": "sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz",
+ "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==",
"inBundle": true,
+ "license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
+ "node_modules/@homebridge/hap-client/node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/@homebridge/hap-client/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -6109,6 +6309,19 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -13805,6 +14018,183 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"inBundle": true
},
+ "node_modules/selenium-webdriver": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.31.0.tgz",
+ "integrity": "sha512-0MWEwypM0+c1NnZ87UEMxZdwphKoaK2UJ2qXzKWrJiM0gazFjgNVimxlHTOO90G2cOhphZqwpqSCJy62NTEzyA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/SeleniumHQ"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/selenium"
+ }
+ ],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@bazel/runfiles": "^6.3.1",
+ "jszip": "^3.10.1",
+ "tmp": "^0.2.3",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">= 18.20.5"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/@bazel/runfiles": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.3.1.tgz",
+ "integrity": "sha512-1uLNT5NZsUVIGS4syuHwTzZ8HycMPyr6POA3FCE4GbMtc4rhoJk8aZKtNIRthJYfL+iioppi+rTfH3olMPr9nA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/selenium-webdriver/node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/selenium-webdriver/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dev": true,
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true,
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/selenium-webdriver/node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/tmp": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
+ "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/selenium-webdriver/node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selenium-webdriver/node_modules/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
diff --git a/package.json b/package.json
index de2bf52..cc6bdf5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "homebridge-gsh",
"displayName": "Homebridge Google Smart Home",
- "version": "3.1.1",
+ "version": "4.0.2",
"description": "Google Smart Home",
"homepage": "https://github.com/homebridge-plugins/homebridge-gsh/blob/latest/README.md",
"license": "GPL-3.0",
@@ -28,7 +28,7 @@
"main": "dist/index.js",
"engines": {
"homebridge": "^1.6.0 || ^2.0.0-beta.0",
- "node": "^18.20.4 || ^20.15.1 || ^22.0.0"
+ "node": "^18.20.4 || ^20.15.1 || ^22.0.0 || ^24.0.0"
},
"scripts": {
"watch": "nodemon",
@@ -36,11 +36,13 @@
"build": "rimraf dist && npm run build:gen-hap-types && npm run build:plugin && npm run build:ui",
"build:plugin": "tsc",
"build:ui": "npm run build --prefix homebridge-ui/public",
+ "testbuild:ui": "npm run testbuild --prefix homebridge-ui/public",
"lint": "eslint --max-warnings=0 .",
"lint:fix": "eslint --fix --max-warnings=0 .",
"prepublishOnly": "npm run build",
- "test": "jest --detectOpenHandles",
+ "test": "jest --forceExit --detectOpenHandles --verbose=true ",
"test-coverage": "jest --coverage",
+ "test-watch": "jest --forceExit --detectOpenHandles --verbose=true --watchAll --testMatch '**/test/**/*.test.ts'",
"build:gen-hap-types": "ts-node scripts/gen-hap-types.ts"
},
"bundledDependencies": [
@@ -50,7 +52,7 @@
"rxjs"
],
"dependencies": {
- "@homebridge/hap-client": "^2.0.5",
+ "@homebridge/hap-client": "^2.1.0",
"@homebridge/ws-connect": "^3.0.0",
"chalk": "^5.3.0",
"fs-extra": "^11.2.0",
@@ -69,6 +71,7 @@
"@typescript-eslint/parser": "^7.16.1",
"actions-on-google": "^3.0.0",
"babel-jest": "^29.7.0",
+ "dotenv": "^16.5.0",
"eslint": "^8.57.0",
"eslint-plugin-format": "^0.1.2",
"eslint-plugin-jest": "^28.8.3",
@@ -77,6 +80,7 @@
"jest": "^29.7.0",
"nodemon": "^3.1.7",
"rimraf": "^5.0.1",
+ "selenium-webdriver": "^4.31.0",
"ts-expect": "^1.3.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
@@ -84,14 +88,15 @@
},
"nodemonConfig": {
"watch": [
- "src"
+ "src",
+ "homebridge-ui/public/src"
],
- "ext": "ts",
+ "ext": "ts,html,scss,md",
"ignore": [
"**/*.spec.ts",
"**/*.test.ts"
],
- "exec": "tsc && DEBUG= homebridge -U ./test/hbConfig -T -D -I -P ./",
+ "exec": "rimraf dist && npm run testbuild:ui & tsc & wait && DEBUG= node ~/npm/lib/node_modules/homebridge-config-ui-x/dist/bin/hb-service.js run --stdout -U ./test/hbConfig -T -D -I -P ./",
"signal": "SIGTERM",
"env": {
"NODE_OPTIONS": "--trace-warnings"
diff --git a/src/dev.ts b/src/dev.ts
index 1a6f021..947acf6 100644
--- a/src/dev.ts
+++ b/src/dev.ts
@@ -2,9 +2,9 @@
* This is used to run the plugin during development
*/
+import * as fs from 'fs-extra';
import * as os from 'node:os';
import * as path from 'node:path';
-import * as fs from 'fs-extra';
import { Plugin } from './main';
@@ -45,4 +45,5 @@ class Log {
}
}
-new Plugin(new Log('Google Smart Home'), pluginConfig, homebridgeConfig);
+const api = {}; // Initialize the 'api' variable with an empty object.
+new Plugin(new Log('Google Smart Home'), pluginConfig, homebridgeConfig, api);
diff --git a/src/hap.spec.ts b/src/hap.spec.ts
index ce45ebc..170e64e 100644
--- a/src/hap.spec.ts
+++ b/src/hap.spec.ts
@@ -31,21 +31,28 @@ const config: PluginConfig = {
debug: false,
platform: 'google-smarthome',
twoFactorAuthPin: '1234',
+ accessoryFilter: [
+ 'West Bedroom',
+ 'Wasaga',
+ 'Garage Door',
+ ],
+ accessoryFilterInverse: true,
+
};
const log = new Log(console, true);
-const hap = new Hap(socketMock, log, '031-45-154', config);
+const hap = new Hap(socketMock, log, '031-45-154', config, {});
describe('hap', () => {
- describe.skip('process the QUERY intent', () => {
+ describe('process the QUERY intent', () => {
test('wait for HAP to be Ready', async () => {
while (!hap.ready) {
// console.log('waiting for hap to be ready');
await sleep(500);
}
// eslint-disable-next-line no-console
- console.log('hap ready, testing started');
+ console.log('hap ready, testing started', hap.services);
}, 30000);
describe('QUERY message with delay to allow manual testing', () => {
@@ -69,11 +76,6 @@ describe('hap', () => {
});
});
- afterAll(async () => {
- // eslint-disable-next-line no-console
- console.log('destroy');
- await hap.destroy();
- });
});
describe('process the SYNC intent', () => {
@@ -143,7 +145,7 @@ describe('hap', () => {
[
{
ids: [
- '53d899e23044252d020ef417d472697eaea748bb9c7b3e860cda6b8b1253ab18'
+ '11d20d713a1ea46cd7e9b524bda80eb6d5bc7df2ee813903c697edad4a700390'
],
status: 'ERROR',
errorCode: 'challengeNeeded',
@@ -199,6 +201,7 @@ describe('hap', () => {
afterAll(async () => {
// eslint-disable-next-line no-console
+ console.log('destroy');
await hap.destroy();
});
});
@@ -219,7 +222,7 @@ const executeLightOff =
'instancePort': 42909,
'instanceUsername': '1C:22:3D:E3:CF:34',
},
- 'id': 'c4644ccdad8201ccee9ae469f20ea3f6dc5f8338525729da5e51bbc005c00e44',
+ 'id': '3844a66b29d217daeeede4f8026fb7d8492d9e3fb53650dd67b9da977ee0ca03',
},
],
'execution': [
@@ -243,7 +246,7 @@ const executeGarageOpen =
'instancePort': 42909,
'instanceUsername': '1C:22:3D:E3:CF:34',
},
- 'id': '53d899e23044252d020ef417d472697eaea748bb9c7b3e860cda6b8b1253ab18',
+ 'id': '11d20d713a1ea46cd7e9b524bda80eb6d5bc7df2ee813903c697edad4a700390',
},
],
'execution': [
@@ -268,7 +271,7 @@ const executeGarageDoorOpenWithIncorrectPin =
'instancePort': 42909,
'instanceUsername': '1C:22:3D:E3:CF:34',
},
- 'id': '53d899e23044252d020ef417d472697eaea748bb9c7b3e860cda6b8b1253ab18',
+ 'id': '11d20d713a1ea46cd7e9b524bda80eb6d5bc7df2ee813903c697edad4a700390',
},
],
'execution': [
@@ -296,7 +299,7 @@ const executeGarageDoorOpenWithCorrectPin =
'instancePort': 42909,
'instanceUsername': '1C:22:3D:E3:CF:34',
},
- 'id': '53d899e23044252d020ef417d472697eaea748bb9c7b3e860cda6b8b1253ab18',
+ 'id': '11d20d713a1ea46cd7e9b524bda80eb6d5bc7df2ee813903c697edad4a700390',
},
],
'execution': [
@@ -324,7 +327,7 @@ const executeGarageClose =
'instancePort': 42909,
'instanceUsername': '1C:22:3D:E3:CF:34',
},
- 'id': '53d899e23044252d020ef417d472697eaea748bb9c7b3e860cda6b8b1253ab18',
+ 'id': '11d20d713a1ea46cd7e9b524bda80eb6d5bc7df2ee813903c697edad4a700390',
},
],
'execution': [
diff --git a/src/hap.ts b/src/hap.ts
index b77dc56..a55ddbc 100644
--- a/src/hap.ts
+++ b/src/hap.ts
@@ -1,5 +1,6 @@
import { HapClient, ServiceType } from '@homebridge/hap-client';
import { SmartHomeV1ExecuteRequestCommands, SmartHomeV1ExecuteResponseCommands, SmartHomeV1SyncDevices } from 'actions-on-google';
+import * as fs from 'fs';
import { Subject } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
import { Characteristic } from './hap-types';
@@ -8,6 +9,7 @@ import { PluginConfig } from './interfaces';
import { Log } from './logger';
import { Door } from './types/door';
+import type { API } from 'homebridge';
import { createHash } from 'node:crypto';
import { Fan } from './types/fan';
import { Fanv2 } from './types/fan-v2';
@@ -34,6 +36,9 @@ export class Hap {
private startTimeout: NodeJS.Timeout;
private discoveryTimeout: NodeJS.Timeout;
private syncTimeout: NodeJS.Timeout;
+ private api: API;
+ private configDiscoveryTimeout: number;
+ private configDiscoveryWait: number;
public ready: boolean;
@@ -92,21 +97,26 @@ export class Hap {
accessorySerialFilter: Array = [];
// deviceNameMap: Array<{ replace: string; with: string }> = [];
- constructor(socket, log, pin: string, config: PluginConfig) {
+ constructor(socket, log, pin: string, config: PluginConfig, api) {
this.config = config;
this.socket = socket;
this.log = log;
this.pin = pin;
+ this.api = api;
+
+ this.configDiscoveryTimeout = (config.discoveryTimeout ? config.discoveryTimeout : 5);
+ this.configDiscoveryWait = (config.discoveryWait ? config.discoveryWait : 15);
this.accessoryFilter = config.accessoryFilter || [];
this.accessoryFilterInverse = config.accessoryFilterInverse || false;
this.accessorySerialFilter = config.accessorySerialFilter || [];
this.instanceBlacklist = config.instanceDenylist || [];
- this.log.debug('Waiting 15 seconds before starting instance discovery...');
+ // eslint-disable-next-line max-len
+ this.log.debug(`Waiting ${this.configDiscoveryWait} seconds before starting instance discovery, and ${this.configDiscoveryTimeout} seconds after last device is discovered to publish to Google.`);
this.startTimeout = setTimeout(() => {
this.discover();
- }, 15000);
+ }, this.configDiscoveryWait * 1000);
this.reportStateSubject
.pipe(
@@ -156,7 +166,7 @@ export class Hap {
this.start();
this.requestSync();
this.hapClient.on('instance-discovered', this.requestSync.bind(this)); // Request sync on new instance discovery
- }, 5000);
+ }, this.configDiscoveryTimeout * 1000);
};
/**
@@ -172,7 +182,11 @@ export class Hap {
const monitor = await this.hapClient.monitorCharacteristics(evServices);
monitor.on('service-update', (services) => {
- this.reportStateSubject.next(services[0].uniqueId);
+ // this.log.debug(`Service Update ${services}`);
+ services.map((service: any) => {
+ this.reportStateSubject.next(service.uniqueId);
+ });
+ // this.reportStateSubject.next(services[0].uniqueId);
});
}
@@ -260,6 +274,11 @@ export class Hap {
try {
response.push(await this.types[service.type].execute(service, command));
} catch (error) {
+ if (this.config.debug) {
+ this.log.debug(`Error executing service: ${JSON.stringify(service)}`);
+ this.log.debug(`Error executing command: ${JSON.stringify(command)}`);
+ this.log.debug(error);
+ }
this.log.error(`Error executing command: ${error.message}`);
response.push({
ids: [device.id],
@@ -294,6 +313,16 @@ export class Hap {
*/
public async loadAccessories(): Promise {
return this.hapClient.getAllServices().then((services) => {
+ if (this.config.debug && process.uptime() < 600) {
+ try {
+ // write the discovery response to a file for debugging
+ const storagePath = this.api.user.storagePath() + '/homebridge-gsh-discovery.json';
+ this.log.warn(`Writing Discovery Response to ${storagePath}`);
+ fs.writeFileSync(storagePath, JSON.stringify(services, null, 2));
+ } catch (e) {
+ this.log.error(`Failed to write discovery response to file: ${e.message}`);
+ }
+ }
services = services.filter(x => this.types[x.type] !== undefined);
this.log.debug(`Loaded ${services.length} accessories from Homebridge - pre filter`);
if (this.accessoryFilterInverse) {
@@ -401,7 +430,6 @@ export class Hap {
* Close the HAP connection, used for testing
*/
public async destroy() {
- // console.log('destroy');
if (this.startTimeout) {
clearTimeout(this.startTimeout);
}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 5ff4c13..ab9009b 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -15,6 +15,9 @@ export interface PluginConfig extends PlatformConfig {
accessorySerialFilter?: Array;
forceFahrenheit?: boolean;
betaServer?: boolean;
+ showHeaterCoolerAsACUnit?: boolean;
+ discoveryTimeout?: number;
+ discoveryWait?: number;
}
export interface Instance {
diff --git a/src/main.ts b/src/main.ts
index 1c83ee4..99ce7bf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,26 +1,33 @@
+import { WebSocket } from '@homebridge/ws-connect';
+import * as fs from 'fs-extra';
import * as crypto from 'node:crypto';
import * as path from 'node:path';
import * as querystring from 'node:querystring';
-import { WebSocket } from '@homebridge/ws-connect';
-import * as fs from 'fs-extra';
+import type { API } from 'homebridge';
import { Hap } from './hap';
import { PluginConfig } from './interfaces';
import { Log } from './logger';
import { SERVER_ADDRESS } from './settings';
+import * as WebSocketClient from 'ws';
+
export class Plugin {
public log: Log;
public config: PluginConfig;
public homebridgeConfig;
+ public api: API;
public hap: Hap;
public package = fs.readJsonSync(path.resolve(__dirname, '../package.json'));
- constructor(log, config: PluginConfig, homebridgeConfig) {
+
+
+ constructor(log, config: PluginConfig, homebridgeConfig, api) {
this.log = new Log(log, config.debug);
this.config = config;
this.homebridgeConfig = homebridgeConfig;
+ this.api = api;
const qs = {
// generate unique id for service based on the username, sha256 for privacy
@@ -30,24 +37,31 @@ export class Plugin {
n: this.package.name,
};
+ const options: WebSocketClient.ClientOptions = {
+ headers: {
+ 'user-agent': `${this.package.name}: ${this.package.version}`,
+ },
+ };
+
const serverUrl = this.config.betaServer ? `wss://${SERVER_ADDRESS.beta}/socket` : `wss://${SERVER_ADDRESS.prod}/socket`;
if (this.config.betaServer) {
this.log.warn(`Using beta server ${serverUrl}`);
+ options.rejectUnauthorized = false;
}
- const socket = new WebSocket(`${serverUrl}?${querystring.stringify(qs)}`);
+ const socket = new WebSocket(`${serverUrl}?${querystring.stringify(qs)}`, { options: options });
- this.hap = new Hap(socket, this.log, this.homebridgeConfig.bridge.pin, this.config);
+ this.hap = new Hap(socket, this.log, this.homebridgeConfig.bridge.pin, this.config, this.api);
// listen for websocket status events, connect and disconnect events, errors, etc.
socket.on('websocket-status', (status) => {
- this.log.info(status);
+ this.log.info(`Cloud Server Status: ${status}`);
});
socket.on('json', async (req) => {
if (req.serverMessage) {
- this.log.warn(req.serverMessage);
+ this.log.warn(`Cloud Server Message: ${req.serverMessage}`);
}
if (!req.body || !req.body.inputs) {
diff --git a/src/platform.ts b/src/platform.ts
index 4b10a8f..13455f2 100644
--- a/src/platform.ts
+++ b/src/platform.ts
@@ -2,9 +2,9 @@
* Homebridge Entry Point
*/
+import * as fs from 'fs-extra';
import type { API } from 'homebridge';
import * as path from 'node:path';
-import * as fs from 'fs-extra';
import { PluginConfig } from './interfaces';
export class HomebridgeGoogleSmartHome {
@@ -21,6 +21,6 @@ export class HomebridgeGoogleSmartHome {
async start() {
const { Plugin } = await import('./main');
const homebridgeConfig = await fs.readJson(path.resolve(this.api.user.configPath()));
- return new Plugin(this.log, this.config, homebridgeConfig);
+ return new Plugin(this.log, this.config, homebridgeConfig, this.api);
}
}
diff --git a/src/settings.ts b/src/settings.ts
index 4f41f09..f1c278e 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -12,6 +12,8 @@ export const PLUGIN_NAME = 'homebridge-gsh';
* This is the base ADDRESS for the cloud service
*/
export const SERVER_ADDRESS = {
- prod: 'homebridge-gsh.iot.oz.nu',
+ prod: 'gsh.homebridge.ca',
beta: 'clone-gsh.homebridge.ca',
};
+
+export const GITHUB_REPO: string = 'https://github.com/homebridge-plugins/homebridge-gsh/';
diff --git a/src/types/heater-cooler.spec.ts b/src/types/heater-cooler.spec.ts
index 30c2ad9..42cb136 100644
--- a/src/types/heater-cooler.spec.ts
+++ b/src/types/heater-cooler.spec.ts
@@ -33,7 +33,7 @@ const config: PluginConfig = {
const log = new Log(console, true);
-const hap = new Hap(socketMock, log, '031-45-154', config);
+const hap = new Hap(socketMock, log, '031-45-154', config, {});
const heaterCooler = new HeaterCooler(hap);
@@ -84,7 +84,7 @@ describe('heaterCooler', () => {
expect(response.attributes.thermostatTemperatureUnit).toBeDefined();
// await sleep(10000)
});
- it('heaterCooler ac', async () => {
+ it('heaterCooler ac as thermostat', async () => {
const response: any = heaterCooler.sync(heaterCoolerAC);
expect(response).toBeDefined();
expect(response.type).not.toBe('action.devices.types.AC_UNIT');
@@ -107,113 +107,143 @@ describe('heaterCooler', () => {
expect(response.attributes.thermostatTemperatureUnit).toBeDefined();
// await sleep(10000)
});
- });
+ describe('showHeaterCoolerAsACUnit', () => {
+ const hap = new Hap(socketMock, log, '031-45-154', { ...config, showHeaterCoolerAsACUnit: true }, {});
+ it('heaterCooler ac as ac_unit', async () => {
+ const heaterCooler = new HeaterCooler(hap);
+ const response: any = heaterCooler.sync(heaterCoolerAC);
+ expect(response).toBeDefined();
+ expect(response.type).toBe('action.devices.types.AC_UNIT');
+ expect(response.type).not.toBe('action.devices.types.THERMOSTAT');
+ expect(response.traits).toContain('action.devices.traits.TemperatureSetting');
+ expect(response.traits).toContain('action.devices.traits.OnOff');
+ expect(response.traits).toContain('action.devices.traits.FanSpeed');
+ expect(response.traits).not.toContain('action.devices.traits.Brightness');
+ expect(response.traits).not.toContain('action.devices.traits.ColorSetting');
+ expect(response.attributes).toBeDefined();
+ expect(response.attributes.commandOnlyOnOff).toBe(false);
+ expect(response.attributes.queryOnlyOnOff).toBe(false);
+ expect(response.attributes.supportsFanSpeedPercent).toBe(true);
+ expect(response.attributes.availableThermostatModes).toBeDefined();
+ expect(response.attributes.availableThermostatModes).toContain('off');
+ expect(response.attributes.availableThermostatModes).toContain('heat');
+ expect(response.attributes.availableThermostatModes).toContain('cool');
+ expect(response.attributes.availableThermostatModes).toContain('heatcool');
+ expect(response.attributes.availableThermostatModes).not.toContain('auto');
+ expect(response.attributes.thermostatTemperatureUnit).toBeDefined();
- describe('query message', () => {
- it('heaterCooler heat and cool', async () => {
- const response = heaterCooler.query(heaterCoolerTemp);
- expect(response).toBeDefined();
- expect(response.online).toBeDefined();
- expect(response.on).toBeDefined();
- expect(response.thermostatMode).toBeDefined();
- expect(response.thermostatTemperatureAmbient).toBeDefined();
- expect(response.thermostatTemperatureSetpoint).toBeDefined();
- expect(response.currentFanSpeedPercent).toBeUndefined();
- // await sleep(10000)
+ });
+ afterAll(() => {
+ hap.destroy();
+ });
});
- it('heaterCooler cool only', async () => {
- const response = heaterCooler.query(heaterCoolerNoHeat);
- expect(response).toBeDefined();
- expect(response.online).toBeDefined();
- expect(response.on).toBeDefined();
- expect(response.thermostatMode).toBeDefined();
- expect(response.thermostatTemperatureAmbient).toBeDefined();
- expect(response.currentFanSpeedPercent).toBeUndefined();
- // await sleep(10000)
- });
+ describe('query message', () => {
+ it('heaterCooler heat and cool', async () => {
+ const response = heaterCooler.query(heaterCoolerTemp);
+ expect(response).toBeDefined();
+ expect(response.online).toBeDefined();
+ expect(response.on).toBeDefined();
+ expect(response.thermostatMode).toBeDefined();
+ expect(response.thermostatTemperatureAmbient).toBeDefined();
+ expect(response.thermostatTemperatureSetpoint).toBeDefined();
+ expect(response.currentFanSpeedPercent).toBeUndefined();
+ // await sleep(10000)
+ });
- it('heaterCooler ac', async () => {
- const response = heaterCooler.query(heaterCoolerAC);
- expect(response).toBeDefined();
- expect(response.online).toBeDefined();
- expect(response.on).toBeDefined();
- expect(response.thermostatMode).toBeDefined();
- expect(response.thermostatTemperatureAmbient).toBeDefined();
- expect(response.currentFanSpeedPercent).toBeDefined();
- // await sleep(10000)
- });
- });
+ it('heaterCooler cool only', async () => {
+ const response = heaterCooler.query(heaterCoolerNoHeat);
+ expect(response).toBeDefined();
+ expect(response.online).toBeDefined();
+ expect(response.on).toBeDefined();
+ expect(response.thermostatMode).toBeDefined();
+ expect(response.thermostatTemperatureAmbient).toBeDefined();
+ expect(response.currentFanSpeedPercent).toBeUndefined();
+ // await sleep(10000)
+ });
- describe('execute message', () => {
- it('heaterCooler - setMode', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('SUCCESS');
- // await sleep(10000)
+ it('heaterCooler ac', async () => {
+ const response = heaterCooler.query(heaterCoolerAC);
+ expect(response).toBeDefined();
+ expect(response.online).toBeDefined();
+ expect(response.on).toBeDefined();
+ expect(response.thermostatMode).toBeDefined();
+ expect(response.thermostatTemperatureAmbient).toBeDefined();
+ expect(response.currentFanSpeedPercent).toBeDefined();
+ // await sleep(10000)
+ });
});
- it('heaterCooler - setTemp', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatTemperatureSetpoint);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('SUCCESS');
- // await sleep(10000)
- });
+ describe('execute message', () => {
+ it('heaterCooler - setMode', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('SUCCESS');
+ // await sleep(10000)
+ });
- it('heaterCooler - setRange', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatTemperatureSetRange);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('SUCCESS');
- // await sleep(10000)
- });
+ it('heaterCooler - setTemp', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatTemperatureSetpoint);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('SUCCESS');
+ // await sleep(10000)
+ });
- it('heaterCooler - setOnOff', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatOff);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('SUCCESS');
- // await sleep(10000)
- });
+ it('heaterCooler - setRange', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatTemperatureSetRange);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('SUCCESS');
+ // await sleep(10000)
+ });
- it('heaterCooler - setFanSpeed', async () => {
- const response = await heaterCooler.execute(heaterCoolerAC, commandThermostatSetFanSpeed);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('SUCCESS');
- // await sleep(10000)
- });
+ it('heaterCooler - setOnOff', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatOff);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('SUCCESS');
+ // await sleep(10000)
+ });
- it('heaterCooler - setFanSpeed fails', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatSetFanSpeed);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('ERROR');
- // await sleep(10000)
- });
+ it('heaterCooler - setFanSpeed', async () => {
+ const response = await heaterCooler.execute(heaterCoolerAC, commandThermostatSetFanSpeed);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('SUCCESS');
+ // await sleep(10000)
+ });
- it('heaterCooler - commandMalformed', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandMalformed);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('ERROR');
- });
+ it('heaterCooler - setFanSpeed fails', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandThermostatSetFanSpeed);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('ERROR');
+ // await sleep(10000)
+ });
- it('heaterCooler - commandIncorrectCommand', async () => {
- const response = await heaterCooler.execute(heaterCoolerTemp, commandIncorrectCommand);
- expect(response).toBeDefined();
- expect(response.ids).toBeDefined();
- expect(response.status).toBe('ERROR');
- });
+ it('heaterCooler - commandMalformed', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandMalformed);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('ERROR');
+ });
- it('heaterCooler - Error', async () => {
- expect.assertions(1);
- heaterCoolerTemp.serviceCharacteristics[0].setValue = setValueError;
- // const response = heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff);
- expect(heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff)).rejects.toThrow('Error setting value');
- // await sleep(10000)
+ it('heaterCooler - commandIncorrectCommand', async () => {
+ const response = await heaterCooler.execute(heaterCoolerTemp, commandIncorrectCommand);
+ expect(response).toBeDefined();
+ expect(response.ids).toBeDefined();
+ expect(response.status).toBe('ERROR');
+ });
+
+ it('heaterCooler - Error', async () => {
+ expect.assertions(1);
+ heaterCoolerTemp.serviceCharacteristics[0].setValue = setValueError;
+ // const response = heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff);
+ expect(heaterCooler.execute(heaterCoolerTemp, commandThermostatSetModeOff)).rejects.toThrow('Error setting value');
+ // await sleep(10000)
+ });
});
});
afterAll(async () => {
diff --git a/src/types/heater-cooler.ts b/src/types/heater-cooler.ts
index 6f0700f..06f51f6 100644
--- a/src/types/heater-cooler.ts
+++ b/src/types/heater-cooler.ts
@@ -37,10 +37,9 @@ export class HeaterCooler extends ghToHap implements ghToHap_t {
queryOnlyOnOff: false,
};
- const type = 'action.devices.types.THERMOSTAT';
+ const type = this.hap.config.showHeaterCoolerAsACUnit ? 'action.devices.types.AC_UNIT' : 'action.devices.types.THERMOSTAT';
if (service.serviceCharacteristics.find(x => x.uuid === Characteristic.RotationSpeed)) {
- // type = 'action.devices.types.AC_UNIT';
traits.push('action.devices.traits.FanSpeed');
attributes.supportsFanSpeedPercent = true;
}
diff --git a/src/types/security-system.ts b/src/types/security-system.ts
index 54f896b..8218a6a 100644
--- a/src/types/security-system.ts
+++ b/src/types/security-system.ts
@@ -61,7 +61,7 @@ export class SecuritySystem extends ghToHap implements ghToHap_t {
const response = {
on: true,
online: true,
- status: 'SUCCESS',
+ // status: 'SUCCESS',
} as any;
const securitySystemCurrentState: number = Number(service.serviceCharacteristics.find(x => x.uuid === Characteristic.SecuritySystemCurrentState).value);
diff --git a/src/types/temperature-sensor.spec.ts b/src/types/temperature-sensor.spec.ts
index f58dad9..efe897f 100644
--- a/src/types/temperature-sensor.spec.ts
+++ b/src/types/temperature-sensor.spec.ts
@@ -32,7 +32,7 @@ const config: PluginConfig = {
const log = new Log(console, true);
-const hap = new Hap(socketMock, log, '031-45-154', config);
+const hap = new Hap(socketMock, log, '031-45-154', config, {});
const temperatureSensor = new TemperatureSensor(hap);
// https://developers.home.google.com/cloud-to-cloud/intents/sync
diff --git a/src/types/thermostat.spec.ts b/src/types/thermostat.spec.ts
index b908adf..4c78a91 100644
--- a/src/types/thermostat.spec.ts
+++ b/src/types/thermostat.spec.ts
@@ -34,7 +34,7 @@ const config: PluginConfig = {
const log = new Log(console, true);
-const hap = new Hap(socketMock, log, '031-45-154', config);
+const hap = new Hap(socketMock, log, '031-45-154', config, {});
const thermostat = new Thermostat(hap);
@@ -82,6 +82,7 @@ describe('thermostat', () => {
expect(response).toBeDefined();
expect(response.online).toBeDefined();
+ expect(response.activeThermostatMode).toBeDefined();
expect(response.thermostatMode).toBeDefined();
expect(response.thermostatTemperatureSetpoint).toBeDefined();
expect(response.thermostatTemperatureAmbient).toBeDefined();
@@ -93,6 +94,7 @@ describe('thermostat', () => {
expect(response).toBeDefined();
expect(response.online).toBeDefined();
+ expect(response.activeThermostatMode).toBeDefined();
expect(response.thermostatMode).toBeDefined();
expect(response.thermostatTemperatureAmbient).toBeDefined();
// await sleep(10000)
@@ -140,7 +142,8 @@ describe('thermostat', () => {
it('thermostat - Error', async () => {
expect.assertions(1);
- thermostatTemp.serviceCharacteristics[0].setValue = setValueError;
+ thermostatTemp.serviceCharacteristics.find(x => x.uuid === Characteristic.TargetHeatingCoolingState).setValue = setValueError;
+ // thermostatTemp.serviceCharacteristics[0].setValue = setValueError;
// const response = thermostat.execute(thermostatTemp, commandThermostatSetModeOff);
expect(thermostat.execute(thermostatTemp, commandThermostatSetModeOff)).rejects.toThrow('Error setting value');
// await sleep(10000)
@@ -295,7 +298,7 @@ const thermostatTemp: ServiceType = {
type: 'TargetHeaterCoolerState',
serviceType: 'Active',
serviceName: 'Shed Light',
- description: 'Configured Name',
+ description: 'TargetHeaterCoolerState',
value: 1,
format: 'string',
perms: ['ev', 'pr', 'pw'],
@@ -311,7 +314,7 @@ const thermostatTemp: ServiceType = {
},
{
aid: 13,
- iid: 11,
+ iid: 9,
uuid: '000000B0-0000-1000-8000-0026BB765291',
type: 'ConfiguredName',
serviceType: 'Active',
@@ -456,6 +459,27 @@ const thermostatTemp: ServiceType = {
setValue,
getValue,
},
+ {
+ aid: 13,
+ iid: 11,
+ uuid: Characteristic.CurrentHeatingCoolingState,
+ type: 'CurrentHeatingCoolingState',
+ serviceType: 'CurrentHeatingCoolingState',
+ serviceName: 'Shed Light',
+ description: 'Configured Name',
+ value: 1,
+ format: 'string',
+ perms: ['ev', 'pr', 'pw'],
+ unit: undefined,
+ maxValue: undefined,
+ minValue: undefined,
+ minStep: undefined,
+ canRead: true,
+ canWrite: true,
+ ev: true,
+ setValue,
+ getValue,
+ },
{
aid: 13,
iid: 11,
@@ -463,7 +487,7 @@ const thermostatTemp: ServiceType = {
type: 'TargetHeatingCoolingState',
serviceType: 'TargetHeatingCoolingState',
serviceName: 'Shed Light',
- description: 'Configured Name',
+ description: 'TargetHeatingCoolingState',
value: 1,
format: 'string',
perms: ['ev', 'pr', 'pw'],
@@ -616,6 +640,27 @@ const thermostatNoHeat: ServiceType = {
setValue,
getValue,
},
+ {
+ aid: 13,
+ iid: 11,
+ uuid: Characteristic.CurrentHeatingCoolingState,
+ type: 'CurrentHeatingCoolingState',
+ serviceType: 'CurrentHeatingCoolingState',
+ serviceName: 'Shed Light',
+ description: 'Configured Name',
+ value: 1,
+ format: 'string',
+ perms: ['ev', 'pr', 'pw'],
+ unit: undefined,
+ maxValue: undefined,
+ minValue: undefined,
+ minStep: undefined,
+ canRead: true,
+ canWrite: true,
+ ev: true,
+ setValue,
+ getValue,
+ },
{
aid: 13,
iid: 11,
@@ -623,7 +668,7 @@ const thermostatNoHeat: ServiceType = {
type: 'TargetHeatingCoolingState',
serviceType: 'TargetHeatingCoolingState',
serviceName: 'Shed Light',
- description: 'Configured Name',
+ description: 'TargetHeatingCoolingState',
value: 1,
format: 'string',
perms: ['ev', 'pr', 'pw'],
diff --git a/src/types/thermostat.ts b/src/types/thermostat.ts
index f9cece4..30d2851 100644
--- a/src/types/thermostat.ts
+++ b/src/types/thermostat.ts
@@ -1,7 +1,7 @@
/* eslint-disable max-len */
-import type { SmartHomeV1ExecuteRequestCommands, SmartHomeV1ExecuteResponseCommands } from 'actions-on-google';
import { ServiceType } from '@homebridge/hap-client';
+import type { SmartHomeV1ExecuteRequestCommands, SmartHomeV1ExecuteResponseCommands } from 'actions-on-google';
import { Hap } from '../hap';
import { Characteristic } from '../hap-types';
import { ghToHap, ghToHap_t } from './ghToHapTypes';
@@ -38,11 +38,14 @@ export class Thermostat extends ghToHap implements ghToHap_t {
}
query(service: ServiceType) {
+ const currentHeatingCoolingState: number = Number(service.serviceCharacteristics.find(x => x.uuid === Characteristic.CurrentHeatingCoolingState).value);
+ const activeThermostatMode = ['off', 'heat', 'cool'][currentHeatingCoolingState];
const targetHeatingCoolingState: number = Number(service.serviceCharacteristics.find(x => x.uuid === Characteristic.TargetHeatingCoolingState).value);
const thermostatMode = ['off', 'heat', 'cool', 'auto'][targetHeatingCoolingState];
const response = {
online: true,
+ activeThermostatMode,
thermostatMode,
thermostatTemperatureSetpoint: service.serviceCharacteristics.find(x => x.uuid === Characteristic.TargetTemperature).value,
thermostatTemperatureAmbient: service.serviceCharacteristics.find(x => x.uuid === Characteristic.CurrentTemperature).value,
@@ -82,7 +85,7 @@ export class Thermostat extends ghToHap implements ghToHap_t {
auto: 3,
heatcool: 3,
};
- await service.serviceCharacteristics.find(x => x.uuid === Characteristic.TargetHeaterCoolerState).setValue(mode[command.execution[0].params.thermostatMode]);
+ await service.serviceCharacteristics.find(x => x.uuid === Characteristic.TargetHeatingCoolingState).setValue(mode[command.execution[0].params.thermostatMode]);
return { ids: [service.uniqueId], status: 'SUCCESS' };
}
case ('action.devices.commands.ThermostatTemperatureSetpoint'): {
diff --git a/test/hbConfig/auth.json b/test/hbConfig/auth.json
deleted file mode 100644
index 8ae211f..0000000
--- a/test/hbConfig/auth.json
+++ /dev/null
@@ -1,10 +0,0 @@
-[
- {
- "id": 1,
- "username": "test",
- "name": "test",
- "hashedPassword": "df121e72b850a058bd68f3d05b1f94dc985821a3885b5da38e263654f29843f98bb6d8e594f6042ff871a424602a9bff50dffa97f258bdc1dca4c79e87bac20c",
- "salt": "64085e70da64670349f042d4c3d3cac75b20233ffeb71db68399973f60da077d",
- "admin": true
- }
-]
diff --git a/test/hbConfig/config.json b/test/hbConfig/config.json
deleted file mode 100644
index 71bc8b3..0000000
--- a/test/hbConfig/config.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "bridge": {
- "name": "HB GSH Test",
- "username": "AA:BB:CC:DD:EE:02",
- "port": 51826,
- "pin": "031-45-154"
- },
- "description": "homebridge-gsh testing bridge",
- "plugins": [
- "homebridge-gsh",
- "homebridge-gsh-beta",
- "homebridge-config-ui-x"
- ],
- "platforms": [
- {
- "name": "Config",
- "port": 8581,
- "auth": "none",
- "theme": "auto",
- "tempUnits": "c",
- "lang": "auto",
- "sudo": false,
- "platform": "config",
- "debug": false
- },
- {
- "name": "Google Smart Home",
- "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imdvb2dsZS1vYXV0aDJ8MTA1ODUzNzk1MjkwNTA0NDIyNDg1IiwiaWF0IjoxNzMwOTkxNTczfQ.IjMjg58QG2YwJu2V1_h1bD6HgqVxIAfWvz4CgThoUw8",
- "notice": "Keep your token a secret!",
- "debug": true,
- "accessoryFilter": [
- "West Bedroom"
- ],
- "accessoryFilterInverse": true,
- "betaServer": true,
- "platform": "google-smarthome"
- }
- ],
- "accessories": []
-}
diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts
new file mode 100644
index 0000000..48e6387
--- /dev/null
+++ b/test/plugin-config.test.ts
@@ -0,0 +1,721 @@
+/* eslint-disable no-console */
+
+import fs from 'fs';
+import path from 'path';
+import { Builder, By, until, WebDriver } from 'selenium-webdriver';
+import chrome from 'selenium-webdriver/chrome';
+
+import dotenv from 'dotenv';
+
+const envPath = '../homebridge-gsh-server/lightsail/installStack/.env.clone-gsh.homebridge.ca';
+// Load environment variables from .env
+dotenv.config({ path: envPath });
+
+let driver: WebDriver;
+
+const describeIf = (condition: boolean, ...args: Parameters) =>
+ condition ? describe(...args) : describe.skip(...args);
+
+const testIf = (condition: boolean, ...args: Parameters) =>
+ condition ? test(...args) : test.skip(...args);
+let cancelCreateTests = false;
+let cancelCancelTests = false;
+const trace = true;
+
+describe('Prepare Environment', () => {
+ test.skip('should clear the Google Smart Home token in config.json', () => {
+ const configPath = path.resolve(process.cwd(), 'test/hbConfig/config.json');
+ // console.log('Config path:', configPath);
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+
+ const gsh = config.platforms?.find((p: any) => p.platform === 'google-smarthome');
+ if (gsh) {
+ gsh.token = '';
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
+ // console.log('✅ Cleared token in config.json');
+ } else {
+ throw new Error('❌ Google Smart Home platform not found in config.json');
+ }
+ expect(gsh).toBeDefined();
+ });
+});
+
+beforeEach(() => {
+ checkCancel();
+});
+
+beforeAll(async () => {
+ const userProfileDir = path.resolve(process.cwd(), 'chrome-profile');
+
+ const options = new chrome.Options();
+ options.addArguments(
+ `--user-data-dir=${userProfileDir}`,
+ '--profile-directory=Default',
+ '--no-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-blink-features=AutomationControlled',
+ '--disable-infobars',
+ // '--start-maximized',
+ '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
+ );
+
+ options.excludeSwitches('enable-automation');
+ options.setUserPreferences({
+ 'profile.default_content_setting_values.notifications': 2,
+ 'credentials_enable_service': false,
+ });
+
+ driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
+ // Clear cookies and local storage
+ await driver.get('https://clone-gsh.homebridge.ca');
+ await driver.manage().deleteAllCookies();
+ await driver.executeScript('window.localStorage.clear(); window.sessionStorage.clear();');
+ await driver.get('http://localhost:8581/plugins');
+});
+
+afterAll(async () => {
+ await driver.quit();
+});
+
+async function openPluginConfig(driver: WebDriver) {
+ try {
+ const closeBtn = await driver.findElement(By.css('.modal .btn-close'));
+ if (await closeBtn.isDisplayed()) {
+ await closeBtn.click();
+ await driver.wait(until.stalenessOf(closeBtn), 3000);
+ }
+ } catch (e) {
+ console.error('Error closing modal:', e);
+ }
+
+ const dropdownToggle = await driver.wait(
+ until.elementLocated(By.css('a[ngbdropdowntoggle].dropdown-toggle')),
+ 5000,
+ );
+ await driver.wait(until.elementIsVisible(dropdownToggle), 5000);
+ await dropdownToggle.click();
+
+ const pluginConfigButton = await driver.wait(
+ until.elementLocated(By.xpath('//button[contains(@class, \'dropdown-item\') and contains(normalize-space(), \'Plugin Config\')]')),
+ 5000,
+ );
+ await driver.wait(until.elementIsVisible(pluginConfigButton), 5000);
+ await pluginConfigButton.click();
+
+ const modalTitle = await driver.wait(
+ until.elementLocated(By.css('.modal-title')),
+ 5000,
+ );
+ const text = await modalTitle.getText();
+ expect(text).toBe('Homebridge Google Smart Home');
+}
+
+describe('Plugin Config', () => {
+ test('Ready for Testing', () => {
+ if (process.env.PAYPAL_PER_USERNAME === undefined || process.env.PAYPAL_PER_PASSWORD === undefined) {
+ const cancelCreateTests = true;
+ const cancelCancelTests = true;
+ }
+ expect(process.env.PAYPAL_PER_USERNAME).toBeDefined();
+ expect(process.env.PAYPAL_PER_PASSWORD).toBeDefined();
+ });
+ describe.skip('New User', () => {
+ test('should load NEWUSER.md content in iframe', async () => {
+ await openPluginConfig(driver);
+
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ const body = await driver.findElement(By.css('body'));
+ const text = await body.getText();
+ // eslint-disable-next-line max-len
+ expect(text).toContain('The Homebridge Google Smart Home plugin allows you to control your Homebridge accessories from a Google Home enabled smart speaker or the Google Home mobile app');
+
+ await driver.switchTo().defaultContent();
+ });
+ describe('Account Linking', () => {
+ describe('when clicking Link Account', () => {
+ let originalWindow: string;
+ let popupWindow: string;
+
+ beforeAll(async () => {
+ // Clear the Google Smart Home token in config.json
+ const configPath = path.resolve(process.cwd(), 'test/hbConfig/config.json');
+ // console.log('Config path:', configPath);
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
+
+ const gsh = config.platforms?.find((p: any) => p.platform === 'google-smarthome');
+ if (gsh) {
+ gsh.token = '';
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
+ // console.log('✅ Cleared token in config.json');
+ } else {
+ throw new Error('❌ Google Smart Home platform not found in config.json');
+ }
+ expect(gsh).toBeDefined();
+
+ await openPluginConfig(driver);
+
+ originalWindow = await driver.getWindowHandle();
+
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ const linkBtn = await driver.findElement(By.xpath('//button[contains(text(), \'Link Account\')]'));
+ await linkBtn.click();
+
+ await driver.wait(async () => {
+ const handles = await driver.getAllWindowHandles();
+ return handles.length > 1;
+ }, 10000);
+
+ const handles = await driver.getAllWindowHandles();
+ popupWindow = handles.find(h => h !== originalWindow)!;
+ });
+
+ afterAll(async () => {
+ const handles = await driver.getAllWindowHandles();
+
+ if (handles.includes(popupWindow)) {
+ await driver.switchTo().window(popupWindow);
+ await driver.close();
+ }
+
+ await driver.switchTo().window(originalWindow);
+ });
+
+ test('should open a new popup window', async () => {
+ expect(popupWindow).toBeDefined();
+ });
+
+ test('should redirect to Auth0', async () => {
+ await safeSwitchToWindow(popupWindow);
+ console.log('-1 Current URL:', await driver.getCurrentUrl());
+ // await driver.wait(until.urlContains('https://clone-gsh.homebridge.ca/link-account'));
+ await driver.wait(until.urlContains('auth0.com'));
+ const url = await driver.getCurrentUrl();
+ expect(url).toContain('auth0.com');
+ expect(url).not.toContain('https://clone-gsh.homebridge.ca/link-account');
+ });
+
+ test('should click the Log in with Google button', async () => {
+ await safeSwitchToWindow(popupWindow);
+ console.log('1 Current URL:', await driver.getCurrentUrl());
+ const googleBtn = await driver.wait(
+ until.elementLocated(By.css('button[data-provider="google-oauth2"]')),
+ );
+ await driver.wait(until.elementIsVisible(googleBtn));
+ console.log('2 Current URL:', await driver.getCurrentUrl());
+ const googleText = await googleBtn.getText();
+ console.log('3 Google login button text:', googleText);
+ expect(googleText).toContain('LOG IN WITH GOOGLE');
+ await googleBtn.click();
+
+ const url = await driver.getCurrentUrl();
+ console.log('Redirected URL after click:', url);
+ expect(url).toContain('https://accounts.google.com/v3/signin');
+ }, 20000);
+
+
+ test('should enter Google login credentials', async () => {
+ await safeSwitchToWindow(popupWindow);
+ console.log('4 Current URL:', await driver.getCurrentUrl());
+ const emailInput = await driver.wait(
+ until.elementLocated(By.id('identifierId')),
+ );
+ await emailInput.clear();
+ await emailInput.sendKeys(process.env.GOOGLE_USERNAME);
+ console.log('5 Current URL:', await driver.getCurrentUrl());
+ await driver.findElement(By.id('identifierNext')).click();
+ console.log('6 Current URL:', await driver.getCurrentUrl());
+ // const html = await driver.getPageSource();
+ // fs.writeFileSync('test/hbConfig/google-login.html', html);
+ // console.log(html);
+ const passwordInput = await driver.wait(
+ until.elementLocated(By.name('password')),
+ );
+ console.log('7 Current URL:', await driver.getCurrentUrl());
+ await passwordInput.clear();
+ await passwordInput.sendKeys(process.env.GOOGLE_PASSWORD);
+ console.log('8 Current URL:', await driver.getCurrentUrl());
+ await driver.findElement(By.id('passwordNext')).click();
+
+ });
+
+ // No need for actual login, browser used stored credentials
+
+ test('Confirm account linking', async () => {
+ await safeSwitchToWindow(popupWindow);
+
+ const confirmButton = await driver.wait(
+ until.elementLocated(By.xpath('//button[contains(text(), \'Confirm\')]')),
+ );
+ console.log('a Current URL:', await driver.getCurrentUrl());
+ await driver.wait(until.elementIsVisible(confirmButton));
+ console.log('b Current URL:', await driver.getCurrentUrl());
+ await confirmButton.click();
+ console.log('c Current URL:', await driver.getCurrentUrl());
+
+ await driver.wait(async () => {
+ const handles = await driver.getAllWindowHandles();
+ return !handles.includes(popupWindow);
+ });
+ console.log('d Popup window closed');
+
+ const remainingHandles = await driver.getAllWindowHandles();
+ console.log('e Popup window closed');
+ expect(remainingHandles).not.toContain(popupWindow);
+ });
+
+ test('should confirm the popup window has closed', async () => {
+ const handles = await driver.getAllWindowHandles();
+
+ // Optional debug
+ console.log('223: Remaining window handles:', handles);
+
+ // Expect only the main window to remain
+ expect(handles.length).toBe(1);
+ await safeSwitchToWindow(originalWindow);
+ });
+
+
+ });
+ test('sleep 10 seconds to observe the popup', async () => {
+ console.log('Sleeping for 10 seconds to observe the popup...');
+ await driver.sleep(120000);
+ }, 121000);
+ });
+ });
+
+ describe('Create Subscription', () => {
+ let originalWindow: string;
+ let popupWindow: string;
+ beforeAll(async () => {
+ await openPluginConfig(driver);
+ });
+ test('should show Account Status as Trial with expiry', async () => {
+
+
+ console.log('256: AS Current URL:', await driver.getCurrentUrl());
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ const statusElement = await driver.wait(
+ until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
+ 5000,
+ );
+ const statusText = await statusElement.getText();
+ console.log('265: Account status text:', statusText);
+ if (!statusText.includes('Trial')) {
+ cancelCreateTests = true;
+ console.log('268: Canceling all tests due to Trial status');
+ }
+ expect(statusText).toMatch(/Account Status: Trial, Expiry: \d{1,2} \w{3} 20\d{2}/); // ✅ RegExp
+ originalWindow = await driver.getWindowHandle();
+ await driver.switchTo().defaultContent();
+ });
+
+ test('should expand the Create Subscription section', async () => {
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ // Click the legend
+ const legend = await driver.wait(
+ until.elementLocated(By.xpath('//legend[contains(normalize-space(), \'Create Subscription\')]')),
+ 5000,
+ );
+ expect(legend).toBeDefined();
+ await legend.click();
+
+ await driver.switchTo().defaultContent();
+ });
+
+ test('should click the PayPal button in first container', async () => {
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ const paypalContainer = await driver.findElement(By.id('paypal-button-container-0'));
+ // console.log('297: 💡 Dumping page source before wait...');
+ // const html = await driver.getPageSource();
+ // console.log(html);
+ const paypalIframe = await driver.wait(
+ until.elementLocated(By.css('#paypal-button-container-0 iframe.component-frame')),
+ 10000, // wait up to 10s
+ );
+ await driver.switchTo().frame(paypalIframe);
+
+ const paypalButton = await driver.wait(
+ until.elementLocated(By.css('div.paypal-button[data-funding-source="paypal"]')),
+ 10000,
+ );
+
+ expect(paypalButton).toBeDefined();
+
+ await driver.wait(until.elementIsVisible(paypalButton), 5000);
+ await paypalButton.click();
+
+ console.log('296: PayPal button clicked.');
+
+ await driver.switchTo().defaultContent();
+ });
+
+ test('Enter PayPal login credentials in popup', async () => {
+ // Wait for popup to open
+ await driver.wait(async () => (await driver.getAllWindowHandles()).length > 1, 10000);
+ const handles = await driver.getAllWindowHandles();
+ const popupHandle = handles.find(h => h !== handles[0])!;
+ await driver.switchTo().window(popupHandle);
+ await driver.wait(until.titleIs('Log in to your PayPal account'));
+ expect(await driver.getTitle()).toContain('Log in to your PayPal account');
+ if (trace) {
+ console.log('314: ', await driver.getTitle());
+ }
+ // expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
+ if (trace) {
+ console.log('316: ', await driver.findElement(By.css('body')).getText());
+ }
+
+ // expect(await driver.findElement(By.css("body")).getText()).toContain('Pay with PayPal');
+
+ const emailInput = await driver.findElement(By.id('email'));
+ await emailInput.clear();
+ await emailInput.sendKeys(process.env.PAYPAL_PER_USERNAME);
+ if (trace) {
+ console.log('323: ', await driver.findElement(By.css('body')).getText());
+ }
+ //await driver.findElement(By.id('btnNext')).click();
+
+ try {
+ const nextButton = await driver.findElement(By.id('btnNext'));
+ await nextButton.click();
+ if (trace) {
+ console.log('400: Clicked Next button');
+ }
+ // Optionally wait for password field to be present
+ await driver.wait(until.elementLocated(By.id('password')), 5000);
+ } catch (err) {
+ if (trace) {
+ console.log('Next button not shown, skipping to password entry');
+ }
+ }
+ // sleep(1000);
+ if (trace) {
+ console.log('327: ', await driver.getTitle());
+ }
+ if (trace) {
+ console.log('328: ', await driver.findElement(By.css('body')).getText());
+ }
+ //await driver.wait(until.titleIs('Log in to your PayPal account'));
+ //expect(await driver.findElement(By.css("body")).getText()).toContain('Pay with PayPal');
+
+ await driver.findElement(By.id('password')).sendKeys(process.env.PAYPAL_PER_PASSWORD);
+
+ console.log('421: password entered');
+ await driver.findElement(By.id('btnLogin')).click();
+ console.log('423: password entered');
+ await driver.wait(until.titleIs('PayPal Checkout - Choose a way to pay'));
+ console.log('425: Choose a way to pay');
+ expect(await driver.getTitle()).toContain('PayPal Checkout - Choose a way to pay');
+
+ if (trace) {
+ console.log('341: ', await driver.getTitle());
+ }
+ if (trace) {
+ console.log('342: ', await driver.findElement(By.css('body')).getText());
+ }
+
+ // expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
+ await driver.findElement(By.xpath('//button[contains(@ng-click, \'continue()\')]')).click();
+
+ await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
+ expect(await driver.getTitle()).toContain('PayPal Checkout - Review your payment');
+ // sleep(1000);
+ if (trace) {
+ console.log('334', await driver.getTitle());
+ }
+ if (trace) {
+ console.log('335', await driver.findElement(By.css('body')).getText());
+ }
+
+ // expect(await driver.findElement(By.css("body")).getText()).toContain('Subscription Options');
+ if ((await driver.getTitle()) === 'PayPal Checkout - Choose a way to pay') {
+ if (trace) {
+ console.log('339 - Clicking continue', await driver.getTitle());
+ }
+ await driver.findElement(By.xpath('//button[contains(@ng-click, \'continue()\')]')).click();
+ }
+
+ // await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
+ // sleep(1000);
+ if (trace) {
+ console.log('344', await driver.getTitle());
+ }
+ const confirmButton = await driver.wait(
+ until.elementLocated(By.id('confirmButtonTop')),
+ 5000,
+ );
+ if (trace) {
+ console.log('345', await driver.findElement(By.css('body')).getText());
+ }
+ expect(confirmButton).toBeDefined();
+ await driver.findElement(By.id('confirmButtonTop')).click();
+ if (trace) {
+ console.log('348', await driver.getTitle());
+ }
+ await driver.wait(until.titleIs('PayPal Checkout - Review your payment'));
+ if (trace) {
+ console.log('350', await driver.getTitle());
+ }
+ expect(await driver.getTitle()).toContain('PayPal Checkout - Review your payment');
+ // sleep(1000);
+ if (trace) {
+ console.log('358', await driver.getTitle());
+ }
+ if (trace) {
+ console.log('359', await driver.findElement(By.css('body')).getText());
+ }
+
+ await driver.wait(async () => {
+ const handles = await driver.getAllWindowHandles();
+ return handles.length === 1;
+ }, 10000);
+
+ await safeSwitchToWindow(originalWindow);
+ if (trace) {
+ console.log('362: ', await driver.getTitle());
+ }
+ expect(await driver.getTitle()).toContain('HB GSH Test');
+ // await driver.wait(until.elementLocated(By.id('notification')));
+ // Click the legend
+ // console.log('💡 Dumping page source before wait...');
+ // const html = await driver.getPageSource();
+ // console.log(html);
+ await driver.wait(
+ until.elementLocated(By.xpath('//div[contains(@class, \'toast-message\') and contains(text(), \'Service Subscription Created\')]')),
+ 10000,
+ );
+ if (trace) {
+ console.log('388: ', await driver.getTitle());
+ }
+ // Wait for the toast notification to appear
+ /*
+ await driver.wait(
+ until.elementLocated(By.id('toast-container')),
+ 3000
+ );
+ const toast = await driver.wait(
+ until.elementLocated(By.css('#toast-container .toast')),
+ 7000
+ );
+ expect(toast).toBeDefined();
+ const toastText = await toast.getText();
+ expect(toastText).toBeDefined();
+ // Assert the toast message confirms cancellation
+ expect(toastText.toLowerCase()).toContain('cancelled');
+ */
+ if (trace) {
+ console.log('364', await driver.getTitle());
+ }
+ // sleep(1000);
+ }, 30000);
+
+
+ test('should confirm the popup window has closed', async () => {
+ const handles = await driver.getAllWindowHandles();
+
+ // Optional debug
+ console.log('399: Remaining window handles:', handles);
+
+ // Expect only the main window to remain
+ expect(handles.length).toBe(1);
+ await safeSwitchToWindow(originalWindow);
+ });
+
+ test('should show Account Status as Subscription:', async () => {
+ if (trace) {
+ console.log('407', await driver.getTitle());
+ }
+ expect(await driver.getTitle()).toContain('HB GSH Test');
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ // Wait for the paypal buttons to disappear
+
+ await driver.wait(async () => {
+ const frames = await driver.findElements(By.css('iframe.component-frame'));
+ return frames.length === 0;
+ }, 10000); // wait up to 10s
+
+ const statusElement = await driver.wait(
+ until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
+ 5000,
+ );
+
+ // console.log('💡 Dumping page source before wait...');
+ // const html = await driver.getPageSource();
+ // console.log(html);
+
+ const statusText = await statusElement.getText();
+ console.log('417: Account status text:', statusText);
+ if (statusText.includes('Trial')) {
+ // cancelAllTests = true;
+ console.log('448: Canceling all tests due to Trial status');
+ }
+ expect(statusText).toMatch(/Account Status: Subscription: Euro Monthly/); // ✅ RegExp
+ originalWindow = await driver.getWindowHandle();
+ await driver.switchTo().defaultContent();
+ });
+
+ test('sleep 10 seconds to observe the popup', async () => {
+ console.log('Sleeping for 10 seconds to observe the popup...');
+ await driver.sleep(10000);
+ }, 121000);
+ });
+
+ describe('Manage Subscription', () => {
+ let originalWindow: string;
+ let popupWindow: string;
+ beforeAll(async () => {
+ await openPluginConfig(driver);
+ });
+
+ test('should show Account Status as Subscription:', async () => {
+ if (trace) {
+ console.log('440', await driver.getTitle());
+ }
+ if (await driver.getTitle() !== 'HB GSH Test') {
+ console.log('Canceling all tests due to incorrect title');
+ cancelCancelTests = true;
+ }
+ expect(await driver.getTitle()).toContain('HB GSH Test');
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ const statusElement = await driver.wait(
+ until.elementLocated(By.xpath('//p[contains(., \'Account Status:\')]')),
+ 5000,
+ );
+ const statusText = await statusElement.getText();
+ console.log('475: Account status text:', statusText);
+ if (statusText.includes('Trial')) {
+ cancelCancelTests = true;
+ console.log('Canceling all tests due to Trial status');
+ }
+ expect(statusText).toMatch(/Account Status: Subscription: Euro Monthly/); // ✅ RegExp
+ originalWindow = await driver.getWindowHandle();
+ await driver.switchTo().defaultContent();
+ });
+
+ test('should expand the Subscription Details section', async () => {
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ // Click the legend
+ const legend = await driver.wait(
+ until.elementLocated(By.xpath('//legend[contains(normalize-space(), \'Subscription Details\')]')),
+ 5000,
+ );
+ expect(legend).toBeDefined();
+ await legend.click();
+
+ await driver.switchTo().defaultContent();
+ });
+
+ test('should click the Cancel Subscription button', async () => {
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+ const button = await driver.wait(
+ until.elementLocated(By.xpath('//button[contains(text(), \'Cancel Subscription\')]')),
+ 1000,
+ );
+
+ await driver.wait(until.elementIsVisible(button), 3000);
+ await driver.wait(until.elementIsEnabled(button), 3000);
+
+ expect(button).toBeDefined();
+ await button.click();
+ await driver.switchTo().defaultContent();
+ });
+
+ test('should confirm subscription cancellation', async () => {
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ await driver.switchTo().frame(iframe);
+
+ // Click "Yes, Cancel" in the confirm dialog
+ const confirmButton = await driver.wait(
+ until.elementLocated(By.xpath('//button[contains(text(), \'Yes, Cancel\')]')),
+ 3000,
+ );
+
+ await driver.wait(until.elementIsVisible(confirmButton), 3000);
+ await driver.wait(until.elementIsEnabled(confirmButton), 3000);
+ await confirmButton.click();
+
+ // Wait for overlay to disappear
+ await driver.wait(
+ until.stalenessOf(confirmButton),
+ 1000,
+ );
+ });
+
+ test('Validate updated status', async () => {
+ await driver.switchTo().defaultContent();
+ const iframe = await driver.findElement(By.css('.modal-body iframe'));
+ expect(iframe).toBeDefined();
+ // await driver.switchTo().frame(iframe);
+ // Wait for the toast notification to appear
+ // console.log('550: 💡 Dumping page source before wait...');
+ // const html = await driver.getPageSource();
+ // console.log(html);
+
+ // Verify updated account status
+ const toast = await driver.wait(
+ until.elementLocated(By.xpath('//div[contains(@class, \'toast-message\')]')),
+ 10000,
+ );
+ if (trace) {
+ console.log('580: Toast message found', await toast.getText());
+ }
+ const toastText = await toast.getText();
+ expect(toastText).toBe('Subscription Cancelled');
+ const bodyText = await driver.findElement(By.css('body')).getText();
+ expect(bodyText).not.toContain('Subscription: Euro Monthly');
+
+ await driver.switchTo().defaultContent();
+ });
+
+ test('confirm no popup windows are open', async () => {
+ const handles = await driver.getAllWindowHandles();
+
+ // Optional debug
+ if (trace) {
+ console.log('590: Remaining window handles:', handles);
+ }
+
+ // Expect only the main window to remain
+ expect(handles.length).toBe(1);
+ await safeSwitchToWindow(originalWindow);
+ });
+
+ test('sleep 10 seconds to observe the popup', async () => {
+ console.log('Sleeping for 10 seconds to observe the popup...');
+ await driver.sleep(10000);
+ }, 11000);
+ });
+});
+
+// Helpers
+
+async function safeSwitchToWindow(handle: string) {
+ const handles = await driver.getAllWindowHandles();
+ if (!handles.includes(handle)) {
+ throw new Error(`Window handle ${handle} no longer exists`);
+ }
+ await driver.switchTo().window(handle);
+}
+
+function checkCancel() {
+ if (cancelCancelTests || cancelCreateTests) {
+ throw new Error('Test run canceled');
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index d87531e..973fca5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,5 +17,6 @@
"homebridge-ui",
"node_modules",
"./dist",
+ "./test"
]
-}
\ No newline at end of file
+}
\ No newline at end of file
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy