From a8a89c771224fa10144ee18dcf8b482f0a4ffcd3 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:51:07 +0800 Subject: [PATCH 1/6] Release 3.1.2 (#14) * Fixes for Security System Issues * Treat HeaterCooler as AC_UNIT if it `config.showHeaterCoolerAsACUnit` is set (#13) * use `config.showHeaterCoolerAsACUnit` to change heater-cooler type * add to schema * Update CHANGELOG.md --------- Co-authored-by: Noam Cohen --- CHANGELOG.md | 15 +++++++++++++++ config.schema.json | 11 ++++++++++- package.json | 4 ++-- src/hap.ts | 6 +++++- src/interfaces.ts | 1 + src/types/heater-cooler.spec.ts | 26 +++++++++++++++++++++++++- src/types/heater-cooler.ts | 3 +-- src/types/security-system.ts | 2 +- 8 files changed, 60 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c18826..ec46232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to `homebridge-gsh` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/). +## 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/config.schema.json b/config.schema.json index fa3ee3f..64b3c16 100644 --- a/config.schema.json +++ b/config.schema.json @@ -78,6 +78,10 @@ "title": "Use beta cloud server", "type": "boolean", "default": false + }, + "showHeaterCoolerAsACUnit": { + "title": "Force Heater-Cooler devices to show as AC_UNIT", + "type": "boolean" } } }, @@ -197,7 +201,12 @@ "type": "help", "helpvalue": "
Beta Cloud
Used for cloud server testing only. Change plugin cloud endpoint to beta test server." }, - "betaServer" + "betaServer", + { + "type": "help", + "helpvalue": "
Force Heater-Cooler devices to show as AC_UNIT
This makes heater cooler advertise as an air conditioning unit." + }, + "showHeaterCoolerAsACUnit" ] } ] diff --git a/package.json b/package.json index de2bf52..871b20c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-gsh", "displayName": "Homebridge Google Smart Home", - "version": "3.1.1", + "version": "3.1.2", "description": "Google Smart Home", "homepage": "https://github.com/homebridge-plugins/homebridge-gsh/blob/latest/README.md", "license": "GPL-3.0", @@ -121,4 +121,4 @@ "fs-extra", "rxjs" ] -} \ No newline at end of file +} diff --git a/src/hap.ts b/src/hap.ts index b77dc56..dc513fd 100644 --- a/src/hap.ts +++ b/src/hap.ts @@ -172,7 +172,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); }); } diff --git a/src/interfaces.ts b/src/interfaces.ts index 5ff4c13..df322d9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -15,6 +15,7 @@ export interface PluginConfig extends PlatformConfig { accessorySerialFilter?: Array; forceFahrenheit?: boolean; betaServer?: boolean; + showHeaterCoolerAsACUnit?: boolean; } export interface Instance { diff --git a/src/types/heater-cooler.spec.ts b/src/types/heater-cooler.spec.ts index 30c2ad9..68a4514 100644 --- a/src/types/heater-cooler.spec.ts +++ b/src/types/heater-cooler.spec.ts @@ -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,6 +107,30 @@ describe('heaterCooler', () => { expect(response.attributes.thermostatTemperatureUnit).toBeDefined(); // await sleep(10000) }); + it('heaterCooler ac as ac_unit', async () => { + const hap = new Hap(socketMock, log, '031-45-154', { ...config, showHeaterCoolerAsACUnit: true }); + 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', () => { 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); From fbf20791cb17258ac0723bd0e13d3fc4fbb2c724 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Wed, 7 May 2025 16:42:52 -0400 Subject: [PATCH 2/6] Cleanup --- .../public/src/assets/markdown/NEWUSER.md | 32 +++++++++++++++++++ test/hbConfig/config.json | 4 +-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 homebridge-ui/public/src/assets/markdown/NEWUSER.md 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..2d01757 --- /dev/null +++ b/homebridge-ui/public/src/assets/markdown/NEWUSER.md @@ -0,0 +1,32 @@ +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 + +--- + \ No newline at end of file diff --git a/test/hbConfig/config.json b/test/hbConfig/config.json index 71bc8b3..612beff 100644 --- a/test/hbConfig/config.json +++ b/test/hbConfig/config.json @@ -25,7 +25,7 @@ }, { "name": "Google Smart Home", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Imdvb2dsZS1vYXV0aDJ8MTA1ODUzNzk1MjkwNTA0NDIyNDg1IiwiaWF0IjoxNzMwOTkxNTczfQ.IjMjg58QG2YwJu2V1_h1bD6HgqVxIAfWvz4CgThoUw8", + "token": "", "notice": "Keep your token a secret!", "debug": true, "accessoryFilter": [ @@ -37,4 +37,4 @@ } ], "accessories": [] -} +} \ No newline at end of file From 67e59ef78cfaafb69355b4b255e04b37e49cb69a Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Mon, 12 May 2025 11:50:05 -0400 Subject: [PATCH 3/6] Migrated cloud server address from older DNS entry to newer DNS entry (#18) ### 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 - Add activeThermostatMode to thermostat query response #17, tks @rukuh ### Fixes - Fix for Thermostat changing modes - Error executing command #12 --- .eslintrc | 3 +- .gitignore | 4 + CHANGELOG.md | 14 + README.md | 18 +- config.schema.json | 16 +- homebridge-ui/public/angular.json | 2 +- homebridge-ui/public/package-lock.json | 614 +++++++++------- homebridge-ui/public/package.json | 4 +- .../public/src/app/app.component.html | 39 +- .../public/src/app/app.component.scss | 19 + homebridge-ui/public/src/app/app.component.ts | 148 ++-- homebridge-ui/public/src/app/app.module.ts | 22 +- .../src/app/markdown-viewer.component.html | 9 + .../src/app/markdown-viewer.component.scss | 0 .../src/app/markdown-viewer.component.ts | 48 ++ .../public/src/app/user-data.component.html | 121 ++++ .../public/src/app/user-data.component.scss | 146 ++++ .../public/src/app/user-data.component.ts | 240 +++++++ .../public/src/app/user-data.pipe.ts | 22 + .../public/src/app/user-data.service.ts | 131 ++++ .../public/src/assets/github-mark.svg | 1 + .../public/src/assets/google-logo.svg | 1 + .../public/src/assets/markdown/EXISTING.md | 31 + .../public/src/assets/markdown/NEWUSER.md | 1 - .../src/environments/environment.prod.ts | 1 + .../public/src/environments/environment.ts | 11 +- homebridge-ui/public/src/main.ts | 13 +- homebridge-ui/public/tsconfig.json | 4 +- package-lock.json | 460 +++++++++++- package.json | 17 +- src/dev.ts | 5 +- src/hap.spec.ts | 31 +- src/hap.ts | 22 +- src/main.ts | 27 +- src/platform.ts | 4 +- src/settings.ts | 4 +- src/types/heater-cooler.spec.ts | 242 ++++--- src/types/temperature-sensor.spec.ts | 2 +- src/types/thermostat.spec.ts | 13 +- src/types/thermostat.ts | 4 +- test/hbConfig/auth.json | 10 - test/hbConfig/config.json | 40 -- test/plugin-config.test.ts | 672 ++++++++++++++++++ tsconfig.json | 3 +- 44 files changed, 2609 insertions(+), 630 deletions(-) create mode 100644 homebridge-ui/public/src/app/markdown-viewer.component.html create mode 100644 homebridge-ui/public/src/app/markdown-viewer.component.scss create mode 100644 homebridge-ui/public/src/app/markdown-viewer.component.ts create mode 100644 homebridge-ui/public/src/app/user-data.component.html create mode 100644 homebridge-ui/public/src/app/user-data.component.scss create mode 100644 homebridge-ui/public/src/app/user-data.component.ts create mode 100644 homebridge-ui/public/src/app/user-data.pipe.ts create mode 100644 homebridge-ui/public/src/app/user-data.service.ts create mode 100644 homebridge-ui/public/src/assets/github-mark.svg create mode 100644 homebridge-ui/public/src/assets/google-logo.svg create mode 100644 homebridge-ui/public/src/assets/markdown/EXISTING.md delete mode 100644 test/hbConfig/auth.json delete mode 100644 test/hbConfig/config.json create mode 100644 test/plugin-config.test.ts 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 ec46232..f1ba743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ 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.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 +- Add activeThermostatMode to thermostat query response #17, tks @rukuh + +### Fixes + +- Fix for Thermostat changing modes - Error executing command #12 + ## v3.1.2 (2025-02-18) ### Changes diff --git a/README.md b/README.md index a9c5639..0caeaa2 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Donate](https://img.shields.io/badge/donate-paypal-yellowgreen.svg)](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) @@ -59,7 +59,7 @@ _Note: Google Smart Home does not currently support all "sensor" devices such as #### 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 +69,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. ![homebridge-gsh-signup](https://user-images.githubusercontent.com/3979615/62948031-ff228d80-be26-11e9-9e07-ef1023f28fa8.gif) ### 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 +112,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 +128,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 64b3c16..1ba8091 100644 --- a/config.schema.json +++ b/config.schema.json @@ -74,14 +74,14 @@ "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 - }, - "showHeaterCoolerAsACUnit": { - "title": "Force Heater-Cooler devices to show as AC_UNIT", - "type": "boolean" } } }, @@ -199,14 +199,14 @@ "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." }, - "betaServer", + "showHeaterCoolerAsACUnit", { "type": "help", - "helpvalue": "
Force Heater-Cooler devices to show as AC_UNIT
This makes heater cooler advertise as an air conditioning unit." + "helpvalue": "
Beta Testing Cloud Server
Used for cloud server testing only. Change plugin cloud endpoint to beta test server." }, - "showHeaterCoolerAsACUnit" + "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..45f0030 100644 --- a/homebridge-ui/public/src/app/app.component.html +++ b/homebridge-ui/public/src/app/app.component.html @@ -7,9 +7,9 @@ - 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 }}

@@ -19,17 +19,38 @@

+ +
+ +
+ +
-
- -

- {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' | translate }} -

+
+
+

+ Google Logo + GitHub Logo + {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' | translate }} +

+

+ User ID: + + {{ user_id }}  + +

+
+ + +

@@ -41,10 +62,8 @@

- Search for - + Search for Homebridge in the Google Home app - and link the action to complete setup.
diff --git a/homebridge-ui/public/src/app/app.component.scss b/homebridge-ui/public/src/app/app.component.scss index e69de29..52b1451 100644 --- a/homebridge-ui/public/src/app/app.component.scss +++ b/homebridge-ui/public/src/app/app.component.scss @@ -0,0 +1,19 @@ +.copyable { + display: inline-flex; + align-items: center; + background-color: #f0f0f0; /* light gray background */ + padding: 2px 6px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.copyable:hover { + background-color: #d6d6d6; /* darker gray on hover */ + text-decoration: none; +} + +.copyable i { + font-size: 0.9rem; /* slightly smaller icon */ + opacity: 0.7; +} \ 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..7affa24 100644 --- a/homebridge-ui/public/src/app/app.component.ts +++ b/homebridge-ui/public/src/app/app.component.ts @@ -1,9 +1,11 @@ -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'; + +import { TranslateService } from './translate.service'; +import { UserDataService } from './user-data.service'; const jwtHelper = new JwtHelperService() @@ -13,126 +15,140 @@ const jwtHelper = new JwtHelperService() styleUrls: ['./app.component.scss'], }) export class AppComponent implements OnInit, OnDestroy { - private linkDomain: string - private linkUrl: string + private linkDomain: string; + private linkUrl: string; + private popup: Window; + private originCheckInterval; - 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 user_id: string; + public justLinked = false; - public linkType: string - public justLinked = false + public ready = false; - public showAdvanced = false - public ready = false + // public userData: UserDataResponse; 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); } } 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`); + }) + .catch(err => { + console.error('❌ Failed to copy:', err); + window.homebridge.toast.error('Error', 'Failed to copy'); + }); + } } 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 @@ + +
+
+ Loading... +
+
+ + +
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..6ed2cf4 --- /dev/null +++ b/homebridge-ui/public/src/app/user-data.component.html @@ -0,0 +1,121 @@ +
+

+ Account Status: {{ userData.accountStatus.text }} +

+ + +
+
+ + Create Subscription + + +
+
+ +

+ Subscription Start Date: {{ userData.startDate | dateToString }} +

+
+ + +
+ +
+ + +
+
+
+
+
{{ plan.name }}
+

{{ plan.description }}

+ +
+
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + Subscription Details + + +
+
+ + +
+ +
+ + +
+
+

Are you sure you want to cancel your subscription?

+
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
Plan:{{ userData.paypalPlanName }}
Description:{{ userData.paypalPlanDescription }}
Subscription ID: + + {{ userData.paypalSubscriptionID }}  + +
Next Payment Date:{{ userData.expiryDate | dateToString }}
+ +
+ +
+
+ +
+
+
+
+ + 📖 + 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..35ed90a --- /dev/null +++ b/homebridge-ui/public/src/app/user-data.component.scss @@ -0,0 +1,146 @@ +/* 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; +} + +/* Adjust Card Body */ +.card-body { + padding: 1rem; +} + +/* Subscription Button Styling */ +.paypal-button { + min-width: 150px; +} + +/* Fix Plan Cards */ +.paypal-plan-container .card { + width: 100%; +} + +/* Confirm Cancel Overlay */ +.confirm-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1050; +} + +.confirm-dialog { + background: white; + padding: 2rem; + border-radius: 0.5rem; + max-width: 400px; + width: 90%; +} + +/* Table inside Subscription Details */ +table { + margin-bottom: 0; +} + +th, +td { + vertical-align: top; +} + +.position-relative { + position: relative; +} + +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.6); // light semi-transparent background + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + pointer-events: all; + cursor: wait; +} + +.loading-overlay i { + font-size: 2rem; +} + +.copyable { + display: inline-flex; + align-items: center; + background-color: #f0f0f0; /* light gray background */ + padding: 2px 6px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.copyable:hover { + background-color: #d6d6d6; /* darker gray on hover */ + text-decoration: none; +} + +.copyable i { + font-size: 0.9rem; /* slightly smaller icon */ + opacity: 0.7; +} + +fieldset.expandable:not(.expanded) { + margin: 0; + padding: 0; + border: none; +} + +table.table-sm th, table.table-sm td { + vertical-align: middle; +} + +.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; /* Light values */ + line-height: 1; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.inline-link { + white-space: nowrap; +} \ 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..905c9cb --- /dev/null +++ b/homebridge-ui/public/src/app/user-data.component.ts @@ -0,0 +1,240 @@ +import { Component, Input, OnInit } 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; + + 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; + 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; + 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; + 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 + }); + }); + } +} 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-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 index 2d01757..65d92c1 100644 --- a/homebridge-ui/public/src/assets/markdown/NEWUSER.md +++ b/homebridge-ui/public/src/assets/markdown/NEWUSER.md @@ -29,4 +29,3 @@ Follow these steps inside the Google Home app: > 📖 See the Wiki for more detailed instructions --- - \ No newline at end of file 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/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/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..8269ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-gsh", - "version": "3.1.1", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-gsh", - "version": "3.1.1", + "version": "4.0.0", "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", @@ -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 871b20c..71f43d2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-gsh", "displayName": "Homebridge Google Smart Home", - "version": "3.1.2", + "version": "4.0.0", "description": "Google Smart Home", "homepage": "https://github.com/homebridge-plugins/homebridge-gsh/blob/latest/README.md", "license": "GPL-3.0", @@ -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= homebridge -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 dc513fd..a0210eb 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,7 @@ export class Hap { private startTimeout: NodeJS.Timeout; private discoveryTimeout: NodeJS.Timeout; private syncTimeout: NodeJS.Timeout; + private api: API; public ready: boolean; @@ -92,11 +95,12 @@ 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.accessoryFilter = config.accessoryFilter || []; this.accessoryFilterInverse = config.accessoryFilterInverse || false; @@ -264,6 +268,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], @@ -298,6 +307,16 @@ export class Hap { */ public async loadAccessories(): Promise { return this.hapClient.getAllServices().then((services) => { + if (this.config.debug && process.uptime() < 300) { + 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) { @@ -405,7 +424,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/main.ts b/src/main.ts index 1c83ee4..e3312b4 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,30 @@ 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}`); } - 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 68a4514..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); @@ -107,137 +107,143 @@ describe('heaterCooler', () => { expect(response.attributes.thermostatTemperatureUnit).toBeDefined(); // await sleep(10000) }); - it('heaterCooler ac as ac_unit', async () => { - const hap = new Hap(socketMock, log, '031-45-154', { ...config, showHeaterCoolerAsACUnit: true }); - 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('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/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..533550d 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); @@ -140,7 +140,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 +296,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 +312,7 @@ const thermostatTemp: ServiceType = { }, { aid: 13, - iid: 11, + iid: 9, uuid: '000000B0-0000-1000-8000-0026BB765291', type: 'ConfiguredName', serviceType: 'Active', @@ -463,7 +464,7 @@ const thermostatTemp: ServiceType = { type: 'TargetHeatingCoolingState', serviceType: 'TargetHeatingCoolingState', serviceName: 'Shed Light', - description: 'Configured Name', + description: 'TargetHeatingCoolingState', value: 1, format: 'string', perms: ['ev', 'pr', 'pw'], @@ -623,7 +624,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..f567af1 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'; @@ -82,7 +82,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 612beff..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": "", - "notice": "Keep your token a secret!", - "debug": true, - "accessoryFilter": [ - "West Bedroom" - ], - "accessoryFilterInverse": true, - "betaServer": true, - "platform": "google-smarthome" - } - ], - "accessories": [] -} \ No newline at end of file diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts new file mode 100644 index 0000000..b080e12 --- /dev/null +++ b/test/plugin-config.test.ts @@ -0,0 +1,672 @@ +/* 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'; + +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.skip('Prepare Environment', () => { + test('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(); + 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://clone-gsh.homebridge.ca/link-account'); + }, 20000); + + // 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); + }); + + + }); + }); + }); + + 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}, UTC/); // ✅ 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(); + + // 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); + + + await driver.findElement(By.id('btnLogin')).click(); + + await driver.wait(until.titleIs('PayPal Checkout - 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 From e038b9526630bedb40b0d55cc4b58f19dd8ecbaa Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Mon, 19 May 2025 15:30:30 -0400 Subject: [PATCH 4/6] Subscriptions --- README.md | 6 ++++++ homebridge-ui/public/src/assets/markdown/NEWUSER.md | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0caeaa2..af2eb19 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ Control your supported [Homebridge](https://github.com/homebridge/homebridge) ac _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: diff --git a/homebridge-ui/public/src/assets/markdown/NEWUSER.md b/homebridge-ui/public/src/assets/markdown/NEWUSER.md index 65d92c1..17ef5f0 100644 --- a/homebridge-ui/public/src/assets/markdown/NEWUSER.md +++ b/homebridge-ui/public/src/assets/markdown/NEWUSER.md @@ -1,3 +1,9 @@ + +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. --- @@ -26,6 +32,6 @@ Follow these steps inside the Google Home app: 6. Sign in using the same account you linked above. 7. Your accessories will sync automatically. -> 📖 See the Wiki for more detailed instructions +> 📖 See the Wiki for more detailed installation instructions --- From 95174d516d69130ba0684d8ffa9d5736366fbf82 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:16:07 -0400 Subject: [PATCH 5/6] UI Improvements including Dark Mode Switching (#21) * Update package.json * Dark mode improvements * Add activeThermostatMode to thermostat query response (#17) * Fix for #19 * Update plugin-config.test.ts * Update config.schema.json * Update config.schema.json * Update config.schema.json * Lint fixes * Increased time for logging homebridge-gsh-discovery.json * Update plugin-config.test.ts * Linking Status * Still needs styling * Update app.component.html * Dark mode (#20) * WIP * Dark Mode * Update app.component.html * Update package.json * Update index.html * Promoted * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: Luke --- CHANGELOG.md | 14 +- config.schema.json | 22 +++ .../public/src/app/app.component.html | 103 ++++++++----- .../public/src/app/app.component.scss | 19 --- homebridge-ui/public/src/app/app.component.ts | 22 +-- .../public/src/app/user-data.component.html | 69 ++++----- .../public/src/app/user-data.component.scss | 135 ++---------------- .../public/src/app/user-data.component.ts | 13 +- .../public/src/assets/google-home.svg | 14 ++ homebridge-ui/public/src/index.html | 2 +- homebridge-ui/public/src/styles.scss | 51 ++++++- package.json | 6 +- src/hap.ts | 14 +- src/interfaces.ts | 2 + src/main.ts | 1 + src/types/thermostat.spec.ts | 44 ++++++ src/types/thermostat.ts | 3 + test/plugin-config.test.ts | 67 +++++++-- 18 files changed, 341 insertions(+), 260 deletions(-) create mode 100644 homebridge-ui/public/src/assets/google-home.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ba743..c661ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ 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.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 @@ -10,7 +23,6 @@ All notable changes to `homebridge-gsh` will be documented in this file. This pr - 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 -- Add activeThermostatMode to thermostat query response #17, tks @rukuh ### Fixes diff --git a/config.schema.json b/config.schema.json index 1ba8091..4f9bb46 100644 --- a/config.schema.json +++ b/config.schema.json @@ -82,6 +82,18 @@ "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 } } }, @@ -202,6 +214,16 @@ "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." diff --git a/homebridge-ui/public/src/app/app.component.html b/homebridge-ui/public/src/app/app.component.html index 45f0030..85a9c30 100644 --- a/homebridge-ui/public/src/app/app.component.html +++ b/homebridge-ui/public/src/app/app.component.html @@ -4,11 +4,8 @@

{{ 'accessories.message_please_see' | translate }} - + https://github.com/homebridge/homebridge-config-ui-x/wiki/Enabling-Accessory-Control {{ 'accessories.message_for_more_information' | translate }} @@ -17,54 +14,82 @@

-

-

+ +

+

-
+
- -
-
-

- Google Logo - GitHub Logo - {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' | translate }} -

-

- User ID: - - {{ user_id }}  - -

-
+
+
+

+ Google Logo + GitHub Logo + {{ linkType | titlecase }} {{ 'plugins.settings.custom.homebridge-gsh.label_account_linked' | + translate }} +

+
    + +
  • + + Google Home Icon + Google Home App: + + + {{ userData.gshNotLinked ? 'Not Linked' : 'Linked' }} + +
  • - - +
  • + User ID: + {{ user_id }} +
  • -
    -

    - - {{ 'plugins.settings.custom.homebridge-gsh.message_homebridge_restart_required' | translate }} - +

  • + Account Status: + {{ userData.accountStatus.text }} +
  • +
+
+
+ + -
-
- Search for - Homebridge in the Google Home app - and link the action to complete setup. + + + +
- + \ No newline at end of file diff --git a/homebridge-ui/public/src/app/app.component.scss b/homebridge-ui/public/src/app/app.component.scss index 52b1451..e69de29 100644 --- a/homebridge-ui/public/src/app/app.component.scss +++ b/homebridge-ui/public/src/app/app.component.scss @@ -1,19 +0,0 @@ -.copyable { - display: inline-flex; - align-items: center; - background-color: #f0f0f0; /* light gray background */ - padding: 2px 6px; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s; -} - -.copyable:hover { - background-color: #d6d6d6; /* darker gray on hover */ - text-decoration: none; -} - -.copyable i { - font-size: 0.9rem; /* slightly smaller icon */ - opacity: 0.7; -} \ 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 7affa24..04e4614 100644 --- a/homebridge-ui/public/src/app/app.component.ts +++ b/homebridge-ui/public/src/app/app.component.ts @@ -7,7 +7,7 @@ import { SERVER_ADDRESS } from '../../../../src/settings'; import { TranslateService } from './translate.service'; import { UserDataService } from './user-data.service'; -const jwtHelper = new JwtHelperService() +const jwtHelper = new JwtHelperService(); @Component({ selector: 'app-root', @@ -29,8 +29,7 @@ export class AppComponent implements OnInit, OnDestroy { public justLinked = false; public ready = false; - - // public userData: UserDataResponse; + public userData: any; constructor( public translateService: TranslateService, @@ -131,6 +130,10 @@ export class AppComponent implements OnInit, OnDestroy { } catch (e) { console.error(e); } + }; + + onUserDataChange(userData: any) { + this.userData = userData; } ngOnDestroy() { @@ -142,13 +145,14 @@ export class AppComponent implements OnInit, OnDestroy { } copyToClipboard(input: string): void { - navigator.clipboard.writeText(input) - .then(() => { + navigator.clipboard.writeText(input).then( + () => { window.homebridge.toast.success(`Copied ${input} to clipboard`); - }) - .catch(err => { + }, + (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/user-data.component.html b/homebridge-ui/public/src/app/user-data.component.html index 6ed2cf4..d49d154 100644 --- a/homebridge-ui/public/src/app/user-data.component.html +++ b/homebridge-ui/public/src/app/user-data.component.html @@ -1,36 +1,27 @@
-

- Account Status: {{ userData.accountStatus.text }} -

-
Create Subscription -
- -

- Subscription Start Date: {{ userData.startDate | dateToString }} +

+ Subscription Start Date: {{ userData.startDate | dateToString }}

-
-
{{ plan.name }}
-

{{ plan.description }}

- +

{{ plan.description }}

@@ -39,82 +30,80 @@
{{ plan.name }}
-
-
Subscription Details -
-
- -
-
-

Are you sure you want to cancel your subscription?

-
- -
-
- - + + - - + + - - + - - - + + +
Plan:{{ userData.paypalPlanName }}Plan:{{ userData.paypalPlanName }}
Description:{{ userData.paypalPlanDescription }}Description:{{ userData.paypalPlanDescription }}
Subscription ID: + Subscription ID: - {{ userData.paypalSubscriptionID }}  + {{ userData.paypalSubscriptionID }} 
Next Payment Date:{{ userData.expiryDate | dateToString }}
Next Billing Date:{{ userData.expiryDate | dateToString }}
-
-
- 📖 + 📖 Subscription Service Details diff --git a/homebridge-ui/public/src/app/user-data.component.scss b/homebridge-ui/public/src/app/user-data.component.scss index 35ed90a..c1f609c 100644 --- a/homebridge-ui/public/src/app/user-data.component.scss +++ b/homebridge-ui/public/src/app/user-data.component.scss @@ -1,21 +1,24 @@ /* Chevron before expandable / expanded legends */ -.expandable > legend::before, -.expanded > legend::before { - content: '\f054'; /* right-chevron for collapsed */ +.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 */ +.expanded>legend::before { + content: '\f078'; + /* down-chevron for expanded */ padding-right: 0.2em; } /* Fieldset and Legend Adjustments */ fieldset { padding: 0; - border: none; /* no border */ + border: none; + /* no border */ } legend { @@ -23,124 +26,4 @@ legend { cursor: pointer; user-select: none; margin-bottom: 0.5rem; -} - -/* Adjust Card Body */ -.card-body { - padding: 1rem; -} - -/* Subscription Button Styling */ -.paypal-button { - min-width: 150px; -} - -/* Fix Plan Cards */ -.paypal-plan-container .card { - width: 100%; -} - -/* Confirm Cancel Overlay */ -.confirm-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1050; -} - -.confirm-dialog { - background: white; - padding: 2rem; - border-radius: 0.5rem; - max-width: 400px; - width: 90%; -} - -/* Table inside Subscription Details */ -table { - margin-bottom: 0; -} - -th, -td { - vertical-align: top; -} - -.position-relative { - position: relative; -} - -.loading-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.6); // light semi-transparent background - z-index: 10; - display: flex; - align-items: center; - justify-content: center; - pointer-events: all; - cursor: wait; -} - -.loading-overlay i { - font-size: 2rem; -} - -.copyable { - display: inline-flex; - align-items: center; - background-color: #f0f0f0; /* light gray background */ - padding: 2px 6px; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s; -} - -.copyable:hover { - background-color: #d6d6d6; /* darker gray on hover */ - text-decoration: none; -} - -.copyable i { - font-size: 0.9rem; /* slightly smaller icon */ - opacity: 0.7; -} - -fieldset.expandable:not(.expanded) { - margin: 0; - padding: 0; - border: none; -} - -table.table-sm th, table.table-sm td { - vertical-align: middle; -} - -.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; /* Light values */ - line-height: 1; - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.inline-link { - white-space: nowrap; } \ 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 index 905c9cb..9d27c50 100644 --- a/homebridge-ui/public/src/app/user-data.component.ts +++ b/homebridge-ui/public/src/app/user-data.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +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'; @@ -14,6 +14,7 @@ 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; @@ -37,6 +38,7 @@ export class UserDataComponent implements OnInit { 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 => { @@ -68,7 +70,6 @@ export class UserDataComponent implements OnInit { subscriptionID: this.userData.paypalSubscriptionID }; - fetch(`${this.linkDomain}/userData/cancel`, { method: 'POST', headers: { @@ -87,10 +88,11 @@ export class UserDataComponent implements OnInit { .then(response => { if (response.success) { window.homebridge.toast.success('Subscription Cancelled', this.translateService.translations['toast.title_success']); - // ✅ Refresh UI after cancel + // 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 => { @@ -111,7 +113,6 @@ export class UserDataComponent implements OnInit { }); } - toggleCreateSubscriptionExpand(): void { this.createSubscriptionExpanded = !this.createSubscriptionExpanded; this.subscriptionDetailsExpanded = false; @@ -160,7 +161,6 @@ export class UserDataComponent implements OnInit { console.error('PayPal script failed to load', error); this.isLoadingPayPalButtons = false; }); - } async renderPayPalButton(plan: any, index: number): Promise { @@ -215,6 +215,7 @@ export class UserDataComponent implements OnInit { 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 => { @@ -237,4 +238,4 @@ export class UserDataComponent implements OnInit { }); }); } -} +} \ 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/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/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/package.json b/package.json index 71f43d2..336c9eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-gsh", "displayName": "Homebridge Google Smart Home", - "version": "4.0.0", + "version": "4.0.1", "description": "Google Smart Home", "homepage": "https://github.com/homebridge-plugins/homebridge-gsh/blob/latest/README.md", "license": "GPL-3.0", @@ -96,7 +96,7 @@ "**/*.spec.ts", "**/*.test.ts" ], - "exec": "rimraf dist && npm run testbuild:ui & tsc & wait && 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" @@ -126,4 +126,4 @@ "fs-extra", "rxjs" ] -} +} \ No newline at end of file diff --git a/src/hap.ts b/src/hap.ts index a0210eb..a55ddbc 100644 --- a/src/hap.ts +++ b/src/hap.ts @@ -37,6 +37,8 @@ export class Hap { private discoveryTimeout: NodeJS.Timeout; private syncTimeout: NodeJS.Timeout; private api: API; + private configDiscoveryTimeout: number; + private configDiscoveryWait: number; public ready: boolean; @@ -102,15 +104,19 @@ export class Hap { 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( @@ -160,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); }; /** @@ -307,7 +313,7 @@ export class Hap { */ public async loadAccessories(): Promise { return this.hapClient.getAllServices().then((services) => { - if (this.config.debug && process.uptime() < 300) { + 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'; diff --git a/src/interfaces.ts b/src/interfaces.ts index df322d9..ab9009b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -16,6 +16,8 @@ export interface PluginConfig extends PlatformConfig { forceFahrenheit?: boolean; betaServer?: boolean; showHeaterCoolerAsACUnit?: boolean; + discoveryTimeout?: number; + discoveryWait?: number; } export interface Instance { diff --git a/src/main.ts b/src/main.ts index e3312b4..99ce7bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,6 +47,7 @@ export class Plugin { if (this.config.betaServer) { this.log.warn(`Using beta server ${serverUrl}`); + options.rejectUnauthorized = false; } const socket = new WebSocket(`${serverUrl}?${querystring.stringify(qs)}`, { options: options }); diff --git a/src/types/thermostat.spec.ts b/src/types/thermostat.spec.ts index 533550d..4c78a91 100644 --- a/src/types/thermostat.spec.ts +++ b/src/types/thermostat.spec.ts @@ -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) @@ -457,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, @@ -617,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, diff --git a/src/types/thermostat.ts b/src/types/thermostat.ts index f567af1..30d2851 100644 --- a/src/types/thermostat.ts +++ b/src/types/thermostat.ts @@ -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, diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts index b080e12..48e6387 100644 --- a/test/plugin-config.test.ts +++ b/test/plugin-config.test.ts @@ -5,6 +5,12 @@ 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) => @@ -16,8 +22,8 @@ let cancelCreateTests = false; let cancelCancelTests = false; const trace = true; -describe.skip('Prepare Environment', () => { - test('should clear the Google Smart Home token in config.json', () => { +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')); @@ -122,8 +128,8 @@ describe('Plugin Config', () => { const body = await driver.findElement(By.css('body')); const text = await body.getText(); - 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'); + // 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(); }); @@ -207,9 +213,35 @@ describe('Plugin Config', () => { const url = await driver.getCurrentUrl(); console.log('Redirected URL after click:', url); - expect(url).toContain('https://clone-gsh.homebridge.ca/link-account'); + 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 () => { @@ -248,6 +280,10 @@ describe('Plugin Config', () => { }); + test('sleep 10 seconds to observe the popup', async () => { + console.log('Sleeping for 10 seconds to observe the popup...'); + await driver.sleep(120000); + }, 121000); }); }); @@ -274,7 +310,7 @@ describe('Plugin Config', () => { 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}, UTC/); // ✅ RegExp + expect(statusText).toMatch(/Account Status: Trial, Expiry: \d{1,2} \w{3} 20\d{2}/); // ✅ RegExp originalWindow = await driver.getWindowHandle(); await driver.switchTo().defaultContent(); }); @@ -349,6 +385,19 @@ describe('Plugin Config', () => { } //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()); @@ -361,11 +410,11 @@ describe('Plugin Config', () => { 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) { From 4ca999018f4bed9e278c78351255c0bcb2673624 Mon Sep 17 00:00:00 2001 From: Northern Man <19808920+NorthernMan54@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:01:29 -0400 Subject: [PATCH 6/6] 4.0.2 (#29) --- CHANGELOG.md | 6 ++++++ package-lock.json | 6 +++--- package.json | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c661ebf..111dfdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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 diff --git a/package-lock.json b/package-lock.json index 8269ea9..fe23878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-gsh", - "version": "4.0.0", + "version": "4.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-gsh", - "version": "4.0.0", + "version": "4.0.2", "bundleDependencies": [ "@homebridge/hap-client", "@homebridge/ws-connect", @@ -51,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": { diff --git a/package.json b/package.json index 336c9eb..cc6bdf5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-gsh", "displayName": "Homebridge Google Smart Home", - "version": "4.0.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", 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