diff --git a/.eslintrc.json b/.eslintrc.json
index 67ebf2e..2bce81d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -13,6 +13,7 @@
"sourceType": "module"
},
"plugins": [ "@stylistic" ],
+ "root": true,
"rules": {
"camelcase": [ "warn" ],
"curly": [ "warn", "all" ],
diff --git a/README.md b/README.md
index 47f165c..b06c657 100644
--- a/README.md
+++ b/README.md
@@ -31,17 +31,23 @@ In the interest of the community seeking a solution outside of myQ, I've develop
* Obstruction detection.
* Occupancy sensor support.
* Read-only garage door opener support.
- * Automation switch support.
+ * Automation switch and dimmer support, allowing you to set the garage door to any position.
* A rich webUI for configuration.
## Getting Started
To get started with `homebridge-ratgdo`:
* Install `homebridge-ratgdo` using the Homebridge webUI. Make sure you make `homebridge-ratgdo` a child bridge for the best performance.
+ * Install the [ESPHome Ratgdo firmware](https://ratgdo.github.io/esphome-ratgdo/). You'll need to use Chrome for this as Safari doesn't support installing firmware through a USB serial port.
+ * That's it. Ensure `homebridge-ratgdo` is running and it will autodiscover your Ratgdo devices and make them available in HomeKit.
+
+Deprecated instructions:
+ * Install the [MQTT Ratgdo firmware](https://paulwieland.github.io/ratgdo/flash.html). You'll need to use Chrome for this as Safari doesn't support installing firmware through a USB serial port.
* [Carefully](#known-caveats) edit the MQTT server and port on your Ratgdo device to the IP address of your Homebridge server, and port 18830 (unless you've changed the default port in `homebridge-ratgdo`).
+ * **Please note, MQTT Ratgdo firmware support in `homebridge-ratgdo` is now considered deprecated and will be removed in an upcoming release. I encourage everyone to upgrade to the [ESPHome Ratgdo firmware](https://ratgdo.github.io/esphome-ratgdo/) as soon as they can.
## Known Caveats
-Ratgdo is a terrific solution that solves a problem for many stranded former myQ users and others. There are some quirks and caveats to note, however. As of Ratgdo firmware v2.57:
+Ratgdo is a terrific solution that solves a problem for many stranded former myQ users and others. There are some quirks and caveats to note, however. As of MQTT Ratgdo firmware v2.57:
* Misconfiguring your MQTT server IP or port number in any way **will** lock up / brick the Ratgdo. The only fix for this I've discovered is to reflash the Ratgdo and don't misconfigure it the next time around.
* Ratgdo currently has no useful way to query it's state over MQTT. That means that on startup, the state of the garage door opener in Homebridge / HomeKit will be unknowable. Given that challenge, `homebridge-ratgdo` will assume the garage door opener is closed on startup. Once an action is taken, the state of the garage door opener will be accurately reflected in Homebridge / HomeKit. There is technically a *query* command available through the MQTT interface to Ratgdo, but all that currently does is to set the Ratgdo state information to an unknown state, awaiting the next state update from the garage door opener, rather than actually publish the current state, which is really what we need.
diff --git a/config.schema.json b/config.schema.json
index d56c0be..9f87d16 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -69,7 +69,7 @@
"layout": [
{
"type": "section",
- "title": "Settings",
+ "title": "Ratgdo Settings (MQTT Firmware)",
"expandable": true,
"expanded": false,
"items": [
diff --git a/docs/Changelog.md b/docs/Changelog.md
index 7f2047d..5d01806 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. This project uses [semantic versioning](https://semver.org/).
+## 1.2.0 (2024-04-14)
+ * New feature: ESPHome firmware support. homebridge-ratgdo now supports both the MQTT and ESPHome firmwares. **A future release of homebridge-ratgdo will remove support for using the Ratgdo MQTT firmware. I encourage everyone to upgrade to the ESPHome firmware sooner than later.** The ESPHome firmware appears to be far better maintained and issue-free than the MQTT firmware. It's been far more stable and reliable in my extensive testing. In addition, it enables some new functionality, particularly for certain automation scenarios. ESPHome firmware support "just works": homebridge-ratgdo will autodetect it's presence and configure it accordingly. There is nothing you need to do beyond installing the Ratgdo ESPHome firmware - the devices will be autodiscovered and autoconfigured by homebridge-ratgdo. You also do not need to remove your prior accessories. homebridge-ratgdo will gracefully handle things.
+ * New feature: automation dimmer support. You can now set automations to open and close yoru garage door opener to specific percentage levels, if you choose to do so. This feature is only available when using the ESPHome Ratgdo firmware.
+ * Improvement: enhanced MQTT support for opening and closing, allowing you to specify where you want the door to be opened to, if using the ESPHome Ratgdo firmware.
+ * Housekeeping.
+
## 1.1.0 (2024-02-29)
* Improvement: when tapping on the garage door opener while an open or close event is inflight and has not yet completed, the garage door opener will now stop.
* Fix: accessory names were being overwritten when the user has not requested them to be.
diff --git a/docs/FeatureOptions.md b/docs/FeatureOptions.md
index 9957ad0..2338faa 100644
--- a/docs/FeatureOptions.md
+++ b/docs/FeatureOptions.md
@@ -44,13 +44,14 @@ Feature options provide a rich mechanism for tailoring your `homebridge-ratgdo`
| Option | Description
|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| `Device` | Make this device available in HomeKit. **(default: enabled)**.
-| `Device.SyncName` | Synchronize the Ratgdo name of this device with HomeKit. Synchronization is one-way only, syncing the device name from Ratgdo to HomeKit. **(default: disabled)**.
+| `Device.SyncName` | Synchronize the Ratgdo name of this device with HomeKit. Synchronization is one-way only, syncing the device name from Ratgdo to HomeKit. This option is only available on Ratgdo devices running MQTT firmware versions. **(default: disabled)**.
#### Opener feature options.
| Option | Description
|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| `Opener.ReadOnly` | Make this opener read-only by ignoring open and close requests from HomeKit. **(default: disabled)**.
+| `Opener.Dimmer` | Add a dimmer accessory to control the opener. This can be useful in automation scenarios where you want to set the door to a specific percentage. This option is only available on Ratgdo devices running ESPHome firmware versions. **(default: disabled)**.
| `Opener.Switch` | Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers. **(default: disabled)**.
| `Opener.OccupancySensor` | Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time. **(default: disabled)**.
| `Opener.OccupancySensor.Duration.Value` | Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy. **(default: 300)**.
diff --git a/docs/MQTT.md b/docs/MQTT.md
index 4e6ca37..5a7abca 100644
--- a/docs/MQTT.md
+++ b/docs/MQTT.md
@@ -69,7 +69,7 @@ The topics that `homebridge-ratgdo` subscribes to are:
|-------------------------|----------------------------------
| `dooropenoccupancy/get` | `true` will trigger a publish event of the current garage door opener door open indicator occupancy status.
| `garagedoor/get` | `true` will trigger a publish event of the current garage door opener state.
-| `garagedoor/set` | One of `close` or `open`. This will send the respective command to the garage door opener.
+| `garagedoor/set` | One of `close` or `open`. This will send the respective command to the garage door opener. When using the ESPHome firmware, `open` takes an optional parameter between 0 and 100 to set the door position appropriately: `open 50` will set the garage door to the 50% open position.
| `light/get` | `true` will trigger a publish event of the current garage door opener light.
| `motion/get` | `true` will trigger a publish event of the current garage door opener motion sensor.
| `obstruction/get` | `true` will trigger a publish event of the current garage door opener obstruction sensor.
diff --git a/homebridge-ui/public/index.html b/homebridge-ui/public/index.html
index ef1d398..885d269 100644
--- a/homebridge-ui/public/index.html
+++ b/homebridge-ui/public/index.html
@@ -5,8 +5,7 @@
Welcome to homebridge-ratgdo. To get started:
- - Ensure you're running the latest Ratgdo MQTT firmware.
- - Edit the MQTT server and port configuration using the webUI on your Ratgdo devices to the IP address of this Homebridge server and port
.
+ - Ensure you're running the latest Ratgdo ESPHome firmware.
@@ -70,6 +69,7 @@ Introduction
Other plugins by HJD:
diff --git a/package-lock.json b/package-lock.json
index e8757d2..6b21592 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,29 +1,33 @@
{
"name": "homebridge-ratgdo",
- "version": "1.1.0",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebridge-ratgdo",
- "version": "1.1.0",
+ "version": "1.2.0",
"license": "ISC",
"dependencies": {
- "@homebridge/plugin-ui-utils": "^1.0.1",
+ "@adobe/fetch": "^4.1.2",
+ "@homebridge/plugin-ui-utils": "^1.0.3",
"aedes": "^0.51.0",
- "mqtt": "^5.3.6"
+ "bonjour-service": "^1.2.1",
+ "eventsource": "^2.0.2",
+ "mqtt": "^5.5.2"
},
"devDependencies": {
- "@stylistic/eslint-plugin": "^1.6.2",
- "@types/node": "^20.11.21",
- "@typescript-eslint/eslint-plugin": "^7.1.0",
- "@typescript-eslint/parser": "^7.1.0",
- "eslint": "^8.57.0",
+ "@stylistic/eslint-plugin": "^1.7.0",
+ "@types/eventsource": "^1.1.15",
+ "@types/node": "^20.12.7",
+ "@typescript-eslint/eslint-plugin": "^7.6.0",
+ "@typescript-eslint/parser": "^7.6.0",
+ "eslint": "^8.5.7",
"homebridge": "^1.7.0",
"nodemon": "^3.1.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
- "typescript": "^5.3.3"
+ "typescript": "^5.4.5"
},
"engines": {
"homebridge": ">1.2.0",
@@ -39,10 +43,31 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@adobe/fetch": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.2.tgz",
+ "integrity": "sha512-O7yA/NgQGkp/2SFOTrcSFOb91VMZH29QRE9dXev/K8xVJkIL1g1B8IqT2G8sRurgQw2CPBQ2H7GF/FHW/HBxKw==",
+ "dependencies": {
+ "debug": "4.3.4",
+ "http-cache-semantics": "4.1.1",
+ "lru-cache": "7.18.3"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/@adobe/fetch/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@babel/runtime": {
- "version": "7.23.9",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
- "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+ "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -161,9 +186,9 @@
"dev": true
},
"node_modules/@homebridge/plugin-ui-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-1.0.1.tgz",
- "integrity": "sha512-Qxpu+HTb5F3tz6iV+gls/snzKPcP/9lOHwoV8IpJlXOeVWj3QMeMuw19dHH8ggLPMm4GaKuObrWjU9yOMENKkw=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-1.0.3.tgz",
+ "integrity": "sha512-p2S/czGYNRnRtMICxBUk4Uar+KCezfyxjqfStfxKgykD2082SNayVDncYUK1xRai78EGHCbif9eoyrmDweh4tQ=="
},
"node_modules/@homebridge/put": {
"version": "0.0.8",
@@ -279,8 +304,7 @@
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
- "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
- "dev": true
+ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
@@ -328,15 +352,15 @@
}
},
"node_modules/@stylistic/eslint-plugin": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-1.6.2.tgz",
- "integrity": "sha512-EFnVcKOE5HTiMlVwisL9hHjz8a69yBbJRscWF/z+/vl6M4ew8NVrBlY8ea7KdV8QtyCY4Yapmsbg5ZDfhWlEgg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-1.7.0.tgz",
+ "integrity": "sha512-ThMUjGIi/jeWYNvOdjZkoLw1EOVs0tEuKXDgWvTn8uWaEz55HuPlajKxjKLpv19C+qRDbKczJfzUODfCdME53A==",
"dev": true,
"dependencies": {
- "@stylistic/eslint-plugin-js": "1.6.2",
- "@stylistic/eslint-plugin-jsx": "1.6.2",
- "@stylistic/eslint-plugin-plus": "1.6.2",
- "@stylistic/eslint-plugin-ts": "1.6.2",
+ "@stylistic/eslint-plugin-js": "1.7.0",
+ "@stylistic/eslint-plugin-jsx": "1.7.0",
+ "@stylistic/eslint-plugin-plus": "1.7.0",
+ "@stylistic/eslint-plugin-ts": "1.7.0",
"@types/eslint": "^8.56.2"
},
"engines": {
@@ -347,9 +371,9 @@
}
},
"node_modules/@stylistic/eslint-plugin-js": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.6.2.tgz",
- "integrity": "sha512-ndT6X2KgWGxv8101pdMOxL8pihlYIHcOv3ICd70cgaJ9exwkPn8hJj4YQwslxoAlre1TFHnXd/G1/hYXgDrjIA==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.0.tgz",
+ "integrity": "sha512-PN6On/+or63FGnhhMKSQfYcWutRlzOiYlVdLM6yN7lquoBTqUJHYnl4TA4MHwiAt46X5gRxDr1+xPZ1lOLcL+Q==",
"dev": true,
"dependencies": {
"@types/eslint": "^8.56.2",
@@ -366,12 +390,12 @@
}
},
"node_modules/@stylistic/eslint-plugin-jsx": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-1.6.2.tgz",
- "integrity": "sha512-hbbouazSJbHD/fshBIOLh9JgtSphKNoTCfHLSNBjAkXLK+GR4i2jhEZZF9P0mtXrNuy2WWInmpq/g0pfWBmSBA==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-1.7.0.tgz",
+ "integrity": "sha512-BACdBwXakQvjYIST5N2WWhRbvhRsIxa/F59BiZol+0IH4FSmDXhie7v/yaxDIIA9CbfElzOmIA5nWNYTVXcnwQ==",
"dev": true,
"dependencies": {
- "@stylistic/eslint-plugin-js": "^1.6.2",
+ "@stylistic/eslint-plugin-js": "^1.7.0",
"@types/eslint": "^8.56.2",
"estraverse": "^5.3.0",
"picomatch": "^4.0.1"
@@ -384,9 +408,9 @@
}
},
"node_modules/@stylistic/eslint-plugin-jsx/node_modules/picomatch": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz",
- "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"engines": {
"node": ">=12"
@@ -396,9 +420,9 @@
}
},
"node_modules/@stylistic/eslint-plugin-plus": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-1.6.2.tgz",
- "integrity": "sha512-EDMwa6gzKw4bXRqdIAUvZDfIgwotbjJs8o+vYE22chAYtVAnA0Pcq+cPx0Uk35t2gvJWb5OaLDjqA6oy1tD0jg==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-1.7.0.tgz",
+ "integrity": "sha512-AabDw8sXsc70Ydx3qnbeTlRHZnIwY6UKEenBPURPhY3bfYWX+/pDpZH40HkOu94v8D0DUrocPkeeEUxl4e0JDg==",
"dev": true,
"dependencies": {
"@types/eslint": "^8.56.2",
@@ -408,137 +432,13 @@
"eslint": "*"
}
},
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/scope-manager": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
- "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
- "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
- "dev": true,
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
- "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "9.0.3",
- "semver": "^7.5.4",
- "ts-api-utils": "^1.0.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/utils": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
- "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
- "dev": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@types/json-schema": "^7.0.12",
- "@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.21.0",
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/typescript-estree": "6.21.0",
- "semver": "^7.5.4"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
- "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "eslint-visitor-keys": "^3.4.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@stylistic/eslint-plugin-plus/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@stylistic/eslint-plugin-ts": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-1.6.2.tgz",
- "integrity": "sha512-FizV58em0OjO/xFHRIy/LJJVqzxCNmYC/xVtKDf8aGDRgZpLo+lkaBKfBrbMkAGzhBKbYj+iLEFI4WEl6aVZGQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-1.7.0.tgz",
+ "integrity": "sha512-QsHv98mmW1xaucVYQTyLDgEpybPJ/6jPPxVBrIchntWWwj74xCWKUiw79hu+TpYj/Pbhd9rkqJYLNq3pQGYuyA==",
"dev": true,
"dependencies": {
- "@stylistic/eslint-plugin-js": "1.6.2",
+ "@stylistic/eslint-plugin-js": "1.7.0",
"@types/eslint": "^8.56.2",
"@typescript-eslint/utils": "^6.21.0"
},
@@ -549,130 +449,6 @@
"eslint": ">=8.40.0"
}
},
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/scope-manager": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
- "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
- "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
- "dev": true,
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
- "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0",
- "debug": "^4.3.4",
- "globby": "^11.1.0",
- "is-glob": "^4.0.3",
- "minimatch": "9.0.3",
- "semver": "^7.5.4",
- "ts-api-utils": "^1.0.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/utils": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
- "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
- "dev": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@types/json-schema": "^7.0.12",
- "@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.21.0",
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/typescript-estree": "6.21.0",
- "semver": "^7.5.4"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
- "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
- "dev": true,
- "dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "eslint-visitor-keys": "^3.4.1"
- },
- "engines": {
- "node": "^16.0.0 || >=18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@stylistic/eslint-plugin-ts/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -698,9 +474,9 @@
"dev": true
},
"node_modules/@types/eslint": {
- "version": "8.56.3",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.3.tgz",
- "integrity": "sha512-PvSf1wfv2wJpVIFUMSb+i4PvqNYkB9Rkp9ZDO3oaWzq4SKhsQk4mrMBr3ZH06I0hKrVGLBacmgl8JM4WVjb9dg==",
+ "version": "8.56.9",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz",
+ "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==",
"dev": true,
"dependencies": {
"@types/estree": "*",
@@ -713,6 +489,12 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
+ "node_modules/@types/eventsource": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz",
+ "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -720,9 +502,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.11.21",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.21.tgz",
- "integrity": "sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==",
+ "version": "20.12.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
+ "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -756,25 +538,25 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
- "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz",
+ "integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==",
"dev": true,
"dependencies": {
- "@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "7.1.0",
- "@typescript-eslint/type-utils": "7.1.0",
- "@typescript-eslint/utils": "7.1.0",
- "@typescript-eslint/visitor-keys": "7.1.0",
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.6.0",
+ "@typescript-eslint/type-utils": "7.6.0",
+ "@typescript-eslint/utils": "7.6.0",
+ "@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
- "ignore": "^5.2.4",
+ "ignore": "^5.3.1",
"natural-compare": "^1.4.0",
- "semver": "^7.5.4",
- "ts-api-utils": "^1.0.1"
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -790,20 +572,45 @@
}
}
},
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
+ "integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.15",
+ "@types/semver": "^7.5.8",
+ "@typescript-eslint/scope-manager": "7.6.0",
+ "@typescript-eslint/types": "7.6.0",
+ "@typescript-eslint/typescript-estree": "7.6.0",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
- "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz",
+ "integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "7.1.0",
- "@typescript-eslint/types": "7.1.0",
- "@typescript-eslint/typescript-estree": "7.1.0",
- "@typescript-eslint/visitor-keys": "7.1.0",
+ "@typescript-eslint/scope-manager": "7.6.0",
+ "@typescript-eslint/types": "7.6.0",
+ "@typescript-eslint/typescript-estree": "7.6.0",
+ "@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -819,16 +626,16 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
- "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz",
+ "integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.1.0",
- "@typescript-eslint/visitor-keys": "7.1.0"
+ "@typescript-eslint/types": "7.6.0",
+ "@typescript-eslint/visitor-keys": "7.6.0"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -836,18 +643,18 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
- "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz",
+ "integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.1.0",
- "@typescript-eslint/utils": "7.1.0",
+ "@typescript-eslint/typescript-estree": "7.6.0",
+ "@typescript-eslint/utils": "7.6.0",
"debug": "^4.3.4",
- "ts-api-utils": "^1.0.1"
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -862,13 +669,38 @@
}
}
},
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
+ "integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.15",
+ "@types/semver": "^7.5.8",
+ "@typescript-eslint/scope-manager": "7.6.0",
+ "@typescript-eslint/types": "7.6.0",
+ "@typescript-eslint/typescript-estree": "7.6.0",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
"node_modules/@typescript-eslint/types": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
- "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz",
+ "integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==",
"dev": true,
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -876,22 +708,22 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
- "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz",
+ "integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.1.0",
- "@typescript-eslint/visitor-keys": "7.1.0",
+ "@typescript-eslint/types": "7.6.0",
+ "@typescript-eslint/visitor-keys": "7.6.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
- "minimatch": "9.0.3",
- "semver": "^7.5.4",
- "ts-api-utils": "^1.0.1"
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -913,9 +745,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+ "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -928,17 +760,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
- "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "7.1.0",
- "@typescript-eslint/types": "7.1.0",
- "@typescript-eslint/typescript-estree": "7.1.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
"semver": "^7.5.4"
},
"engines": {
@@ -949,16 +781,74 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.56.0"
+ "eslint": "^7.0.0 || ^8.0.0"
}
},
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
- "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.1.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -969,6 +859,47 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz",
+ "integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "7.6.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1287,6 +1218,15 @@
"multicast-dns-service-types": "^1.1.0"
}
},
+ "node_modules/bonjour-service": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz",
+ "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1582,7 +1522,6 @@
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
- "dev": true,
"dependencies": {
"@leichtgewicht/ip-codec": "^2.0.1"
},
@@ -1845,11 +1784,18 @@
"node": ">=0.8.x"
}
},
+ "node_modules/eventsource": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
+ "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
@@ -1889,15 +1835,15 @@
}
},
"node_modules/fast-unique-numbers": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.0.tgz",
- "integrity": "sha512-lgIjiflW23W7qgagregmo5FFzM+m4/dWaDUVneRi2AV7o2k5npggeEX7srSKlYfJU9fKXvQV2Gzk3272fJT65w==",
+ "version": "8.0.13",
+ "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz",
+ "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==",
"dependencies": {
- "@babel/runtime": "^7.23.9",
+ "@babel/runtime": "^7.23.8",
"tslib": "^2.6.2"
},
"engines": {
- "node": ">=18.2.0"
+ "node": ">=16.1.0"
}
},
"node_modules/fastfall": {
@@ -2390,6 +2336,11 @@
"node": "^18.15.0 || ^20.7.0"
}
},
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+ },
"node_modules/hyperid": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.1.1.tgz",
@@ -2998,9 +2949,9 @@
}
},
"node_modules/mqtt": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.3.6.tgz",
- "integrity": "sha512-3XeyCdHRFf3zZdUUBt/pqprKPtUABc8O4ZGPGs2QPO4sPNTnJels8U2UtBtMt09QCgpUmw8gLTLy2R7verR7kQ==",
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.2.tgz",
+ "integrity": "sha512-dlKxINBrrorgMp1A5UHQVf5GAkn1m/dY12W2Sp6LAY794RxQ0OPo0Q9N2S3qrNRjjC1WETA/9oYR6yadhR3siw==",
"dependencies": {
"@types/readable-stream": "^4.0.5",
"@types/ws": "^8.5.9",
@@ -3017,7 +2968,7 @@
"reinterval": "^1.1.0",
"rfdc": "^1.3.0",
"split2": "^4.2.0",
- "worker-timers": "^7.0.78",
+ "worker-timers": "^7.1.4",
"ws": "^8.14.2"
},
"bin": {
@@ -3071,7 +3022,6 @@
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
"integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
- "dev": true,
"dependencies": {
"dns-packet": "^5.2.2",
"thunky": "^1.0.2"
@@ -3616,9 +3566,9 @@
"dev": true
},
"node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -3925,8 +3875,7 @@
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
- "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
- "dev": true
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
"node_modules/to-regex-range": {
"version": "5.0.1",
@@ -3953,9 +3902,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
- "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true,
"engines": {
"node": ">=16"
@@ -4048,9 +3997,9 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/typescript": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
- "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -4183,33 +4132,33 @@
}
},
"node_modules/worker-timers": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.2.tgz",
- "integrity": "sha512-iqhXt5+Mc3u2nHj3G/w/E9pXqhlueniA2NlyelB/MQSHQuuW2fmmZGkveAv6yi4SSZvrpbveBBlqPSZ0MDCLww==",
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.7.tgz",
+ "integrity": "sha512-Dr4La61d94SjOA8P57h2LN8W3MXOVe/m1P7jER8cmuIy+JaDMqPttSwo6QRJFSK6YnG9cD6SU7J8m7CVlu8jlw==",
"dependencies": {
- "@babel/runtime": "^7.23.9",
+ "@babel/runtime": "^7.24.4",
"tslib": "^2.6.2",
- "worker-timers-broker": "^6.1.2",
- "worker-timers-worker": "^7.0.66"
+ "worker-timers-broker": "^6.1.7",
+ "worker-timers-worker": "^7.0.70"
}
},
"node_modules/worker-timers-broker": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.2.tgz",
- "integrity": "sha512-slFupigW5vtkGJ1VBCxYPwXFFRmvfioh02bCltBhbMkt3fFnkAbKBCg61pNTetlD0RAsP09mqx/FB0f4UMoHNw==",
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.7.tgz",
+ "integrity": "sha512-8hb4lSMAijDY/Dp/MOw9Hc2x6uU59XWFYjcWQgC4bai+sxcLXjeexd9aYKdYMFZPiPoieGzMYIs9WGpv2Co3eA==",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "fast-unique-numbers": "^9.0.0",
+ "@babel/runtime": "^7.24.4",
+ "fast-unique-numbers": "^8.0.13",
"tslib": "^2.6.2",
- "worker-timers-worker": "^7.0.66"
+ "worker-timers-worker": "^7.0.70"
}
},
"node_modules/worker-timers-worker": {
- "version": "7.0.66",
- "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.66.tgz",
- "integrity": "sha512-VCLa0H5K9fE2DVI/9r5zDuFrMQIpNL3UD/h4Ui49fIiRBTgv1Sqe0RM12brr83anBsm103aUQkvKvCBL+KpNtg==",
+ "version": "7.0.70",
+ "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.70.tgz",
+ "integrity": "sha512-lemWEME0RHB78hzGkkQcKfF6L82gqVhV3T9iY14jHBhbLxLq9t1RRCLmPDBZV7sdnUoW6Khkfn6coqPjgEK6cw==",
"dependencies": {
- "@babel/runtime": "^7.23.9",
+ "@babel/runtime": "^7.24.4",
"tslib": "^2.6.2"
}
},
diff --git a/package.json b/package.json
index 9405654..aa4490c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "homebridge-ratgdo",
"displayName": "Homebridge Ratgdo",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": "HomeKit integration for LiftMaster and Chamberlain garage door openers, without requiring myQ.",
"license": "ISC",
"repository": {
@@ -27,20 +27,24 @@
"homebridge-plugin"
],
"devDependencies": {
- "@stylistic/eslint-plugin": "^1.6.2",
- "@types/node": "^20.11.21",
- "@typescript-eslint/eslint-plugin": "^7.1.0",
- "@typescript-eslint/parser": "^7.1.0",
- "eslint": "^8.57.0",
+ "@stylistic/eslint-plugin": "^1.7.0",
+ "@types/eventsource": "^1.1.15",
+ "@types/node": "^20.12.7",
+ "@typescript-eslint/eslint-plugin": "^7.6.0",
+ "@typescript-eslint/parser": "^7.6.0",
+ "eslint": "^8.5.7",
"homebridge": "^1.7.0",
"nodemon": "^3.1.0",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
- "typescript": "^5.3.3"
+ "typescript": "^5.4.5"
},
"dependencies": {
- "@homebridge/plugin-ui-utils": "^1.0.1",
+ "@adobe/fetch": "^4.1.2",
+ "@homebridge/plugin-ui-utils": "^1.0.3",
"aedes": "^0.51.0",
- "mqtt": "^5.3.6"
+ "bonjour-service": "^1.2.1",
+ "eventsource": "^2.0.2",
+ "mqtt": "^5.5.2"
}
}
diff --git a/src/ratgdo-device.ts b/src/ratgdo-device.ts
index 71f77db..9794ee7 100644
--- a/src/ratgdo-device.ts
+++ b/src/ratgdo-device.ts
@@ -3,8 +3,9 @@
* ratgdo-device.ts: Base class for all Ratgdo devices.
*/
import { API, CharacteristicValue, HAP, PlatformAccessory, Service } from "homebridge";
-import { RATGDO_MOTION_DURATION, RATGDO_OCCUPANCY_DURATION, RATGDO_TRANSITION_DURATION } from "./settings.js";
-import { RatgdoDevice, RatgdoLogging, RatgdoReservedNames } from "./ratgdo-types.js";
+import { FetchError, fetch } from "@adobe/fetch";
+import { Firmware, RatgdoDevice, RatgdoLogging, RatgdoReservedNames } from "./ratgdo-types.js";
+import { RATGDO_MOTION_DURATION, RATGDO_OCCUPANCY_DURATION } from "./settings.js";
import { RatgdoOptions, getOptionFloat, getOptionNumber, getOptionValue, isOptionEnabled } from "./ratgdo-options.js";
import { RatgdoPlatform } from "./ratgdo-platform.js";
import util from "node:util";
@@ -12,6 +13,7 @@ import util from "node:util";
// Device-specific options and settings.
interface RatgdoHints {
+ automationDimmer: boolean,
automationSwitch: boolean,
doorOpenOccupancyDuration: number,
doorOpenOccupancySensor: boolean,
@@ -29,6 +31,7 @@ interface RatgdoStatus {
availability: boolean,
door: CharacteristicValue,
+ doorPosition: number,
light: boolean,
lock: CharacteristicValue,
motion: boolean,
@@ -42,7 +45,6 @@ export class RatgdoAccessory {
private readonly config: RatgdoOptions;
public readonly device: RatgdoDevice;
private doorOccupancyTimer: NodeJS.Timeout | null;
- private doorTimer: NodeJS.Timeout | null;
private readonly hap: HAP;
private readonly hints: RatgdoHints;
public readonly log: RatgdoLogging;
@@ -59,7 +61,6 @@ export class RatgdoAccessory {
this.api = platform.api;
this.status = {} as RatgdoStatus;
this.config = platform.config;
- this.doorTimer = null;
this.hap = this.api.hap;
this.hints = {} as RatgdoHints;
this.device = device;
@@ -76,6 +77,7 @@ export class RatgdoAccessory {
// Initialize our internal state.
this.status.availability = false;
this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
+ this.status.doorPosition = 0;
this.status.light = false;
this.status.lock = this.hap.Characteristic.LockCurrentState.UNSECURED;
this.status.motion = false;
@@ -99,6 +101,7 @@ export class RatgdoAccessory {
this.configureInfo();
this.configureGarageDoor();
this.configureMqtt();
+ this.configureAutomationDimmer();
this.configureAutomationSwitch();
this.configureDoorOpenOccupancySensor();
this.configureLight();
@@ -109,6 +112,7 @@ export class RatgdoAccessory {
// Configure device-specific settings.
private configureHints(): boolean {
+ this.hints.automationDimmer = this.hasFeature("Opener.Dimmer");
this.hints.automationSwitch = this.hasFeature("Opener.Switch");
this.hints.doorOpenOccupancySensor = this.hasFeature("Opener.OccupancySensor");
this.hints.doorOpenOccupancyDuration = this.getFeatureNumber("Opener.OccupancySensor.Duration") ?? RATGDO_OCCUPANCY_DURATION;
@@ -119,6 +123,12 @@ export class RatgdoAccessory {
this.hints.readOnly = this.hasFeature("Opener.ReadOnly");
this.hints.syncName = this.hasFeature("Device.SyncName");
+ if(this.hints.automationDimmer && (this.device.type !== Firmware.ESPHOME)) {
+
+ this.hints.automationDimmer = false;
+ this.log.info("Automation dimmer support is only available on Ratgdo devices running on ESPHome firmware versions.");
+ }
+
if(this.hints.readOnly) {
this.log.info("Garage door opener is read-only. The opener will not respond to open and close requests from HomeKit.");
@@ -126,7 +136,14 @@ export class RatgdoAccessory {
if(this.hints.syncName) {
- this.log.info("Syncing Ratgdo device name to HomeKit.");
+ if(this.device.type !== Firmware.MQTT) {
+
+ this.hints.syncName = false;
+ this.log.info("Syncing names is only available on Ratgdo devices running on MQTT firmware versions.");
+ } else {
+
+ this.log.info("Syncing Ratgdo device name to HomeKit.");
+ }
}
return true;
@@ -165,8 +182,11 @@ export class RatgdoAccessory {
this.platform.mqtt?.subscribeSet(this, "garagedoor", "Garage Door", (value: string) => {
let command;
+ let position;
+
+ const action = value.split(" ");
- switch(value) {
+ switch(action[0]) {
case "close":
@@ -176,6 +196,18 @@ export class RatgdoAccessory {
case "open":
command = this.hap.Characteristic.TargetDoorState.OPEN;
+
+ // Parse the position information, if set.
+ if(this.device.type === Firmware.ESPHOME) {
+
+ position = parseFloat(action[1]);
+
+ if(isNaN(position) || (position < 0) || (position > 100)) {
+
+ position = undefined;
+ }
+ }
+
break;
default:
@@ -186,7 +218,7 @@ export class RatgdoAccessory {
}
// Set our door state accordingly.
- this.setDoorState(command);
+ this.setDoorState(command, position);
});
// Return our obstruction state.
@@ -356,7 +388,7 @@ export class RatgdoAccessory {
lightService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.light);
lightService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value: CharacteristicValue) => {
- this.command("light", value === true ? "on" : "off");
+ void this.command("light", value === true ? "on" : "off");
});
return true;
@@ -408,6 +440,94 @@ export class RatgdoAccessory {
return true;
}
+ // Configure a dimmer to automate open and close events in HomeKit beyond what HomeKit might allow for a garage opener service that gets treated as a secure service.
+ private configureAutomationDimmer(): boolean {
+
+ // Find the dimmer service, if it exists.
+ let dimmerService = this.accessory.getServiceById(this.hap.Service.Lightbulb, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
+
+ // The switch is disabled by default and primarily exists for automation purposes.
+ if(!this.hints.automationDimmer) {
+
+ if(dimmerService) {
+
+ this.accessory.removeService(dimmerService);
+ this.log.info("Disabling automation dimmer.");
+ }
+
+ return false;
+ }
+
+ // Add the dimmer to the opener, if needed.
+ if(!dimmerService) {
+
+ dimmerService = new this.hap.Service.Lightbulb(this.name + " Automation Dimmer", RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
+
+ if(!dimmerService) {
+
+ this.log.error("Unable to add automation dimmer.");
+ return false;
+ }
+
+ dimmerService.displayName = this.name + " Automation Dimmer";
+ dimmerService.updateCharacteristic(this.hap.Characteristic.Name, this.name + " Automation Dimmer");
+ this.accessory.addService(dimmerService);
+ }
+
+ // Return the current state of the opener.
+ dimmerService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
+
+ // We're on if we are in any state other than closed (specifically open or stopped).
+ return this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED;
+ });
+
+ // Close the opener. Opening is really handled in the brightness event.
+ dimmerService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value: CharacteristicValue) => {
+
+ // We really only want to act when the opener is open. Otherwise, it's handled by the brightness event.
+ if(value) {
+
+ return;
+ }
+
+ // Inform the user.
+ this.log.info("Automation dimmer: closing.");
+
+ // Send the command.
+ if(!this.setDoorState(this.hap.Characteristic.TargetDoorState.CLOSED)) {
+
+ // Something went wrong. Let's make sure we revert the dimmer to it's prior state.
+ setTimeout(() => {
+
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.On, !value);
+ }, 50);
+ }
+ });
+
+ // Return the door position of the opener.
+ dimmerService.getCharacteristic(this.hap.Characteristic.Brightness)?.onGet(() => {
+
+ return this.status.doorPosition;
+ });
+
+ // Adjust the door position of the opener by adjusting brightness of the light.
+ dimmerService.getCharacteristic(this.hap.Characteristic.Brightness)?.onSet((value: CharacteristicValue) => {
+
+ this.log.info("Automation dimmer: moving opener to %s%.", (value as number).toFixed(0));
+
+ this.setDoorState((value as number) > 0 ?
+ this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED, value as number);
+ });
+
+ // Initialize the switch.
+ dimmerService.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
+ dimmerService.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition);
+
+ this.log.info("Enabling automation dimmer.");
+
+ return true;
+ }
+
// Configure a switch to automate open and close events in HomeKit beyond what HomeKit might allow for a garage opener service that gets treated as a secure service.
private configureAutomationSwitch(): boolean {
@@ -574,14 +694,24 @@ export class RatgdoAccessory {
}
// Open or close the garage door.
- private setDoorState(value: CharacteristicValue): boolean {
+ private setDoorState(value: CharacteristicValue, position?: number): boolean {
+
+ // Understand what we're targeting.
+ const targetAction = (position !== undefined) ? "set" : this.translateTargetDoorState(value);
+
+ // If we have an invalid target state, we're done.
+ if(targetAction === "unknown") {
- const actionAttempt = value === this.hap.Characteristic.TargetDoorState.CLOSED ? "close" : "open";
+ // HomeKit has told us something that we don't know how to handle.
+ this.log.error("Unknown HomeKit set event received: %s.", value);
+
+ return false;
+ }
// If this garage door is read-only, we won't process any requests to set state.
if(this.hints.readOnly) {
- this.log.info("Unable to %s door. The door has been configured to be read only.", actionAttempt);
+ this.log.info("Unable to %s garage door: read-only mode enabled.", targetAction);
// Tell HomeKit that we haven't in fact changed our state so we don't end up in an inadvertent opening or closing state.
setImmediate(() => {
@@ -596,49 +726,31 @@ export class RatgdoAccessory {
// If we are already opening or closing the garage door, we assume the user wants to stop the garage door opener at it's current location.
if((this.status.door === this.hap.Characteristic.CurrentDoorState.OPENING) || (this.status.door === this.hap.Characteristic.CurrentDoorState.CLOSING)) {
- this.log.debug("Stop requested from user while transitioning between open and close states.");
+ this.log.debug("User-initiated stop requested while transitioning between open and close states.");
// Execute the stop command.
- this.command("door", "stop");
- return true;
- }
-
- // Close the garage door.
- if(value === this.hap.Characteristic.TargetDoorState.CLOSED) {
-
- // HomeKit is asking us to close the garage door, but let's make sure it's not already closed first.
- if(this.status.door !== this.hap.Characteristic.CurrentDoorState.CLOSED) {
-
- // Execute the command.
- this.command("door", "close");
- }
+ void this.command("door", "stop");
return true;
}
- // Open the garage door.
- if(value === this.hap.Characteristic.TargetDoorState.OPEN) {
+ // Set the door state, assuming we're not already there.
+ if(this.status.door !== value) {
- // HomeKit is informing us to open the door, but we don't want to act if it's already open.
- if(this.status.door !== this.hap.Characteristic.CurrentDoorState.OPEN) {
+ this.log.debug("User-initiated door state change: %s%s.", this.translateTargetDoorState(value), (position !== undefined) ? " (" + position.toString() + "%)" : "");
- // Execute the command.
- this.command("door", "open");
- }
-
- return true;
+ // Execute the command.
+ void this.command("door", targetAction, position);
}
- // HomeKit has told us something that we don't know how to handle.
- this.log.error("Unknown HomeKit set event received: %s.", value);
-
- return false;
+ return true;
}
// Update the state of the accessory.
- public updateState(event: string, payload: string): void {
+ public updateState(event: string, payload: string, position?: number): void {
const camelCase = (text: string): string => text.charAt(0).toUpperCase() + text.slice(1);
+ const dimmerService = this.accessory.getServiceById(this.hap.Service.Lightbulb, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
const doorOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN);
const garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);
const lightBulbService = this.accessory.getService(this.hap.Service.Lightbulb);
@@ -662,22 +774,26 @@ export class RatgdoAccessory {
motionService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
// Inform the user:
- this.log.info("Device %s.", this.status.availability ? "connected" : "disconnected");
+ this.log.info("Device %s (%s v%s).", this.status.availability ? "connected" : "disconnected", this.device.type === Firmware.MQTT ? "MQTT" : "ESPHome",
+ this.device.firmwareVersion);
break;
case "door":
- // If we're already in the state we're updating to, we're done.
- if(this.translateCurrentDoorState(this.status.door) === payload) {
+ // Update our door position automation dimmer.
+ if(position !== undefined) {
- break;
+ this.status.doorPosition = position;
+
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition);
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.On, this.status.doorPosition > 0);
+ this.log.debug("Door state: %s% open.", this.status.doorPosition.toFixed(0));
}
- // Clear out our door transition timer, if we have one.
- if(this.doorTimer) {
+ // If we're already in the state we're updating to, we're done.
+ if(this.translateCurrentDoorState(this.status.door) === payload) {
- clearTimeout(this.doorTimer);
- this.doorTimer = null;
+ break;
}
switch(payload) {
@@ -685,22 +801,13 @@ export class RatgdoAccessory {
case "closed":
this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
+
break;
case "closing":
this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSING;
- // As a safety measure for occasionally unreliable MQTT message delivery, let's ensure we generate a closed state after a reasonable transition period. If we
- // receive an actual state update before this, this safety measure won't be triggered.
- this.doorTimer = setTimeout(() => {
-
- // Mark the door as closed.
- this.log.debug("Generating a close event to complete the state transition.");
- this.updateState("door", "closed");
- this.doorTimer = null;
- }, RATGDO_TRANSITION_DURATION * 1000);
-
break;
case "open":
@@ -726,21 +833,12 @@ export class RatgdoAccessory {
this.status.door = this.hap.Characteristic.CurrentDoorState.OPENING;
- // As a safety measure for occasionally unreliable MQTT message delivery, let's ensure we generate an open state after a reasonable transition period. If we
- // receive an actual state update before this, this safety measure won't be triggered.
- this.doorTimer = setTimeout(() => {
-
- // Mark the door as open.
- this.log.debug("Generating an open event to complete the state transition.");
- this.updateState("door", "open");
- this.doorTimer = null;
- }, RATGDO_TRANSITION_DURATION * 1000);
-
break;
case "stopped":
this.status.door = this.hap.Characteristic.CurrentDoorState.STOPPED;
+
break;
default:
@@ -828,17 +926,14 @@ export class RatgdoAccessory {
case "motion":
- this.status.motion = payload === "detected";
-
- // Motion no longer detected, clear out the motion sensor timer, and we're done.
- if(!this.status.motion && this.motionTimer) {
-
- clearTimeout(this.motionTimer);
- this.motionTimer = null;
+ // We only want motion detected events. We timeout the motion event on our own to allow for automations and a more holistic user experience.
+ if(payload !== "detected") {
break;
}
+ this.status.motion = true;
+
// Update the motion sensor state.
motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.status.motion);
@@ -926,6 +1021,115 @@ export class RatgdoAccessory {
}
}
+ // Utility function to transmit a command to Ratgdo.
+ private async command(topic: string, payload: string, position?: number): Promise {
+
+ if(this.device.type === Firmware.MQTT) {
+
+ this.platform.broker.publish({ cmd: "publish", dup: false, payload: payload, qos: 2, retain: false, topic: this.device.name + "/command/" + topic },
+ (error?: Error) => {
+
+ if(error) {
+
+ this.log.error("Publish error:");
+ this.log.error(util.inspect(error), { colors: true, depth: null, sorted: true });
+ }
+ }
+ );
+
+ return;
+ }
+
+ // Now we handle ESPHome firmware commands.
+ let endpoint;
+ let action;
+
+ switch(topic) {
+
+ case "door":
+
+ endpoint = "cover/door";
+
+ switch(payload) {
+
+ case "closed":
+
+ action = "close";
+ break;
+
+ case "open":
+ case "stop":
+
+ action = payload;
+ break;
+
+ case "set":
+
+ if(position === undefined) {
+
+ this.log.error("Invalid door set command received: no position specified.");
+ return;
+ }
+
+ action = "set?position=" + (position / 100).toString();
+ break;
+
+ default:
+
+ this.log.error("Unknown door command received: %s.", payload);
+ return;
+ break;
+ }
+
+ break;
+
+ case "light":
+
+ endpoint = "light/light";
+ action = (payload === "on") ? "turn_on" : "turn_off";
+ break;
+
+ default:
+
+ this.log.error("Unknown command received: %s - %s.", topic, payload);
+ return;
+ break;
+ }
+
+ try {
+
+ // Execute the action.
+ const response = await fetch("http://" + this.device.address + "/" + endpoint + "/" + action, { body: JSON.stringify({}), method: "POST"});
+
+ if(!response?.ok) {
+
+ this.log.error("Unable to execute command: %s - %s.", event, payload);
+ return;
+ }
+ } catch(error) {
+
+ if(error instanceof FetchError) {
+
+ switch(error.code) {
+
+ case "ECONNRESET":
+
+ this.log.error("Connection to the Ratgdo controller has been reset.");
+ break;
+
+ default:
+
+ this.log.error("Error sending command: %s %s.", error.code, error.message);
+ break;
+ }
+
+ return;
+ }
+
+ this.log.error("Error sending command: %s", error);
+ }
+ }
+
// Utility function to translate HomeKit's current door state values into human-readable form.
private translateCurrentDoorState(value: CharacteristicValue): string {
@@ -965,6 +1169,30 @@ export class RatgdoAccessory {
return "unknown";
}
+ // Utility function to translate HomeKit's target door state values into human-readable form.
+ private translateTargetDoorState(value: CharacteristicValue): string {
+
+ // HomeKit state decoder ring.
+ switch(value) {
+
+ case this.hap.Characteristic.TargetDoorState.CLOSED:
+
+ return "closed";
+ break;
+
+ case this.hap.Characteristic.TargetDoorState.OPEN:
+
+ return "open";
+ break;
+
+ default:
+
+ break;
+ }
+
+ return "unknown";
+ }
+
// Utility function to return our bias for what the current door state should be. This is primarily used for our initial bias on startup.
private doorCurrentStateBias(state: CharacteristicValue): CharacteristicValue {
@@ -1046,21 +1274,6 @@ export class RatgdoAccessory {
}
}
- // Utility function to transmit a command to Ratgdo.
- private command(topic: string, payload: string): void {
-
- this.platform.broker.publish({ cmd: "publish", dup: false, payload: payload, qos: 2, retain: false, topic: this.device.name + "/command/" + topic },
- (error?: Error) => {
-
- if(error) {
-
- this.log.error("Publish error:");
- this.log.error(util.inspect(error), { colors: true, depth: null, sorted: true });
- }
- }
- );
- }
-
// Utility function to return a floating point configuration parameter on a device.
private getFeatureFloat(option: string): number | undefined {
diff --git a/src/ratgdo-options.ts b/src/ratgdo-options.ts
index 6cdadd5..ca91457 100644
--- a/src/ratgdo-options.ts
+++ b/src/ratgdo-options.ts
@@ -32,7 +32,7 @@ export const featureOptions: { [index: string]: FeatureOption[] } = {
"Device": [
{ default: true, description: "Make this device available in HomeKit.", name: "" },
- { default: false, description: "Synchronize the Ratgdo name of this device with HomeKit. Synchronization is one-way only, syncing the device name from Ratgdo to HomeKit.", name: "SyncName" }
+ { default: false, description: "Synchronize the Ratgdo name of this device with HomeKit. Synchronization is one-way only, syncing the device name from Ratgdo to HomeKit. This option is only available on Ratgdo devices running MQTT firmware versions.", name: "SyncName" }
],
// Light options.
@@ -53,6 +53,7 @@ export const featureOptions: { [index: string]: FeatureOption[] } = {
"Opener": [
{ default: false, description: "Make this opener read-only by ignoring open and close requests from HomeKit.", name: "ReadOnly" },
+ { default: false, description: "Add a dimmer accessory to control the opener. This can be useful in automation scenarios where you want to set the door to a specific percentage. This option is only available on Ratgdo devices running ESPHome firmware versions.", name: "Dimmer" },
{ default: false, description: "Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers.", name: "Switch" },
{ default: false, description: "Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time.", name: "OccupancySensor" },
{ default: false, defaultValue: RATGDO_OCCUPANCY_DURATION, description: "Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy.", group: "OccupancySensor", name: "OccupancySensor.Duration" }
diff --git a/src/ratgdo-platform.ts b/src/ratgdo-platform.ts
index 577e6b3..0efc2fe 100644
--- a/src/ratgdo-platform.ts
+++ b/src/ratgdo-platform.ts
@@ -3,13 +3,17 @@
* ratgdo-platform.ts: homebridge-ratgdo platform class.
*/
import { API, APIEvent, DynamicPlatformPlugin, HAP, Logging, PlatformAccessory, PlatformConfig } from "homebridge";
+import { Bonjour, Service } from "bonjour-service";
import { PLATFORM_NAME, PLUGIN_NAME, RATGDO_API_PORT, RATGDO_MQTT_TOPIC } from "./settings.js";
import { RatgdoOptions, featureOptionCategories, featureOptions, isOptionEnabled } from "./ratgdo-options.js";
import { Server, createServer } from "node:net";
import Aedes from "aedes";
+import EventSource from "eventsource";
+import { Firmware } from "./ratgdo-types.js";
import { RatgdoAccessory } from "./ratgdo-device.js";
import { RatgdoMqtt } from "./ratgdo-mqtt.js";
import { URL } from "node:url";
+import net from "node:net";
import util from "node:util";
interface haConfigJson {
@@ -33,7 +37,8 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
private readonly accessories: PlatformAccessory[];
public readonly api: API;
public broker: Aedes;
- private deviceMac: { [index: string]: string };
+ private mqttNameMac: { [index: string]: string };
+ private espHomeEvents: { [index: string]: EventSource };
private featureOptionDefaults: { [index: string]: boolean };
public config!: RatgdoOptions;
public readonly configOptions: string[];
@@ -51,7 +56,8 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
this.broker = new Aedes();
this.configOptions = [];
this.configuredDevices = {};
- this.deviceMac = {};
+ this.mqttNameMac = {};
+ this.espHomeEvents = {};
this.featureOptionDefaults = {};
this.hap = api.hap;
this.log = log;
@@ -115,7 +121,7 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
this.log.debug("Debug logging on. Expect a lot of data.");
// Fire up the Ratgdo API once Homebridge has loaded all the cached accessories it knows about and called configureAccessory() on each.
- api.on(APIEvent.DID_FINISH_LAUNCHING, this.configureBroker.bind(this));
+ api.on(APIEvent.DID_FINISH_LAUNCHING, this.configureRatgdo.bind(this));
}
// This gets called when homebridge restores cached accessories at startup. We intentionally avoid doing anything significant here, and save all that logic for broker
@@ -126,9 +132,25 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
this.accessories.push(accessory);
}
+ // Connect to the Ratgdo device types we know about.
+ private configureRatgdo(): void {
+
+ this.configureBroker();
+ this.configureEspHome();
+ }
+
// Configure and start our MQTT broker.
private configureBroker(): void {
+ // Lockdown any attempts to send commands. We reserve those privileges for ourselves.
+ this.broker.authorizePublish = (client, packet, callback): void => {
+
+ // Match any strings that end in a command.
+ const commandRegex = new RegExp("/command/[^/]+/?$", "gi");
+
+ return callback(commandRegex.test(packet.topic) ? new Error("Command topics are reserved.") : null);
+ };
+
// Capture any publish events to our MQTT broker for processing.
this.broker.on("publish", (packet): void => {
@@ -150,7 +172,13 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
// Let's see if we have a new garage door opener.
if(discoveryRegex.test(packet.topic)) {
- this.configureGdo(JSON.parse(payload) as haConfigJson);
+ const deviceInfo = JSON.parse(payload) as haConfigJson;
+ const mac = deviceInfo.unique_id.split("_")[1].toUpperCase();
+
+ // Map the device name to the MAC address for future reference.
+ this.mqttNameMac[deviceInfo["~"]] = mac;
+
+ this.configureGdo(new URL(deviceInfo.device.configuration_url)?.hostname ?? "unknown", mac, deviceInfo["~"], deviceInfo.device.sw_version, Firmware.MQTT);
return;
}
@@ -159,7 +187,7 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
if(topicMatch) {
- const mac = this.deviceMac[topicMatch[1]];
+ const mac = this.mqttNameMac[topicMatch[1]];
if(!mac) {
@@ -175,7 +203,7 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
return;
}
- this.log.debug("Status update detected: %s (%s): %s - %s", topicMatch[1], this.deviceMac[topicMatch[1]], topicMatch[2], payload);
+ this.log.debug("Status update detected: %s (%s): %s - %s", topicMatch[1], this.mqttNameMac[topicMatch[1]], topicMatch[2], payload);
// Update our state, based on the update we've received.
garageDoor.updateState(topicMatch[2], payload);
@@ -226,14 +254,229 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
}
}
- // Configure a discovered garage door opener.
- private configureGdo(deviceInfo: haConfigJson): void {
+ // Configure and connect to Ratgdo ESPHome clients.
+ private configureEspHome(): void {
+
+ // Define the EventSource error message type.
+ interface ESError {
+
+ message?: string,
+ status?: number,
+ type: string
+ }
+
+ // Define the EventSource state message type.
+ interface ESMessage {
+
+ current_operation?: string,
+ id: string,
+ name: string,
+ position?: number,
+ state: string,
+ value?: string
+ }
+
+ // Instantiate our mDNS stack.
+ const mdns = new Bonjour();
+
+ // Make sure we cleanup our mDNS client on shutdown.
+ this.api.on(APIEvent.SHUTDOWN, () => {
+
+ mdns.destroy();
+ });
+
+ // ESPHome device discovery.
+ const mdnsBrowser = mdns.find({type: "esphomelib"});
+
+ // Add an ESPHome device.
+ mdnsBrowser.on("up", (service: Service) => {
+
+ // No addresses found for this entry. We're done.
+ if(!service.addresses) {
+
+ return;
+ }
+
+ // We grab the first address provided for the ESPHome device.
+ const address = service.addresses[0];
+
+ // Grab the MAC address. We uppercase it and put it in the familiar colon notation first.
+ const mac = (service.txt as Record).mac.toUpperCase().replace(/(.{2})(?=.)/g, "$1:");
+
+ // Configure the device.
+ const ratgdoAccessory = this.configureGdo(address, mac, (service.txt as Record).friendly_name, (service.txt as Record).version,
+ Firmware.ESPHOME);
+
+ // If we've already configured this one, we're done.
+ if(!ratgdoAccessory) {
+
+ return;
+ }
+
+ try {
+
+ // Connect to the Ratgdo ESPHome events API.
+ this.espHomeEvents[mac] = new EventSource("http://" + address + "/events");
+
+ // Handle errors in the events API.
+ this.espHomeEvents[mac].addEventListener("error", (payload: ESError) => {
+
+ let errorMessage;
+
+ switch(payload.message) {
+
+ case "read ECONNRESET":
+
+ errorMessage = "Connection to the Ratgdo controller has been reset";
+
+ break;
+
+ case "unknown error.":
+
+ errorMessage = "An unknown error on the Ratgdo controller has occurred. This will happen occasionally and can generally be ignored.";
+
+ break;
+
+ default:
+
+ errorMessage = payload;
+
+ break;
+ }
+
+ ratgdoAccessory.log.error("Ratgdo ESPHome: %s", errorMessage);
+ });
+
+ // Inform the user when we've successfully connected.
+ this.espHomeEvents[mac].addEventListener("open", () => {
+
+ ratgdoAccessory.updateState("availability", "online");
+ });
+
+ // Capture state updates from the controller.
+ this.espHomeEvents[mac].addEventListener("state", (message: MessageEvent) => {
+
+ let event;
+
+ ratgdoAccessory.log.debug(message.data);
+
+ try {
+
+ event = JSON.parse(message.data) as ESMessage;
+ } catch(error) {
- // Retrieve the MAC address from the unique identifier generated by Ratgdo.
- const mac = deviceInfo.unique_id.split("_")[1];
+ ratgdoAccessory.log.error("Unable to parse state message: \"%s\". Invalid JSON.", message.data);
+ return;
+ }
- // Map the device name to the MAC address for future reference.
- this.deviceMac[deviceInfo["~"]] = mac;
+ let state;
+
+ switch(event.id) {
+
+ case "binary_sensor-motion":
+
+ ratgdoAccessory.updateState("motion", (event.state === "OFF") ? "clear" : "detected");
+ break;
+
+ case "binary_sensor-obstruction":
+
+ ratgdoAccessory.updateState("obstruction", (event.state === "OFF") ? "clear" : "obstructed");
+ break;
+
+ case "cover-door":
+
+ switch(event.current_operation) {
+
+ case "CLOSING":
+ case "OPENING":
+
+ state = event.current_operation.toLowerCase();
+
+ break;
+
+ case "IDLE":
+
+ // We're stopped, rather than open, if the door is in a position greater than 0.
+ state = ((event.state === "OPEN") && (event.position !== undefined) && (event.position > 0) && (event.position < 1)) ? "stopped" :
+ event.state.toLowerCase();
+
+ break;
+
+ default:
+
+ ratgdoAccessory.log.error("Unknown door operation detected: %s.", event.current_operation);
+ return;
+
+ break;
+ }
+
+ ratgdoAccessory.updateState("door", state, (event.position !== undefined) ? event.position * 100 : undefined);
+
+ break;
+
+ case "light-light":
+
+ ratgdoAccessory.updateState("light", event.state === "OFF" ? "off" : "on");
+
+ break;
+
+ case "lock-lock_remotes":
+
+ ratgdoAccessory.updateState("lock", event.state === "LOCKED" ? "locked" : "unlocked");
+
+ break;
+
+ default:
+
+ break;
+ }
+ });
+
+ // Heartbeat the Ratgdo controller at regular intervals. We need to do this because the ESPHome firmware for Ratgdo has a failsafe that will autoreboot the
+ // Ratgdo every 15 minutes if it doesn't receive a native API connection. Fortunately, the failsafe only looks for an open connection to the API, allowing us the
+ // opportunity to heartbeat it with a connection we periodically reopen.
+ const heartbeat = (): void => {
+
+ // Connect to the Ratgdo.
+ const socket = net.createConnection({ host: address, port: 6053 }, () => {
+
+ // Close the heartbeat at regular intervals.
+ setTimeout(() => socket.destroy(), 120000);
+ });
+
+ // Handle heartbeat errors.
+ socket.on("error", (err) => {
+
+ ratgdoAccessory.log.error("ESPHome API heartbeat error: %s", err.message);
+ setTimeout(() => heartbeat(), 60000);
+ });
+
+ // Perpetually restart our heartbeat when it ends.
+ socket.on("close", () => {
+
+ setTimeout(() => heartbeat(), 300000);
+ });
+ };
+
+ heartbeat();
+ } catch(error) {
+
+ if(error instanceof Error) {
+
+ ratgdoAccessory.log.error("Ratgdo ESPHome: Error: %s", error.message);
+ }
+ }
+ });
+
+ // Refresh device detection, just in case we missed it in setting up the listener.
+ mdnsBrowser.update();
+
+ // Refresh device discovery regular intervals.
+ setInterval(() => mdnsBrowser.update(), 60000);
+ }
+
+ // Configure a discovered garage door opener.
+ private configureGdo(address: string, mac: string, name: string, firmwareVersion: string, type: Firmware): RatgdoAccessory | null {
// Generate this device's unique identifier.
const uuid = this.hap.uuid.generate(mac);
@@ -244,15 +487,13 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
// Our device details.
const device = {
- address: new URL(deviceInfo.device.configuration_url)?.hostname ?? "unknown",
- firmwareVersion: deviceInfo.device.sw_version,
+ address: address,
+ firmwareVersion: firmwareVersion,
mac: mac.replace(/:/g, ""),
- name: deviceInfo["~"]
+ name: name,
+ type: type
};
- // Inform the user.
- this.log.info("Discovered: %s (address: %s mac: %s firmware: v%s).", device.name, device.address, device.mac, device.firmwareVersion);
-
// Check to see if the user has disabled the device.
if(!isOptionEnabled(this.configOptions, device, "Device", this.featureOptionDefault("Device"))) {
@@ -269,9 +510,18 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
}
// We're done.
- return;
+ return null;
}
+ // If we've already configured this device before, we're done.
+ if(this.configuredDevices[uuid]) {
+
+ return null;
+ }
+
+ // Inform the user.
+ this.log.info("Discovered: %s (address: %s mac: %s firmware: v%s).", device.name, device.address, device.mac, device.firmwareVersion);
+
// It's a new device - let's add it to HomeKit.
if(!accessory) {
@@ -283,10 +533,12 @@ export class RatgdoPlatform implements DynamicPlatformPlugin {
}
// Add it to our list of configured devices.
- this.configuredDevices[accessory.UUID] = new RatgdoAccessory(this, accessory, device);
+ this.configuredDevices[uuid] = new RatgdoAccessory(this, accessory, device);
// Refresh the accessory cache.
this.api.updatePlatformAccessories([accessory]);
+
+ return this.configuredDevices[uuid];
}
// Utility to return the default value for a feature option.
diff --git a/src/ratgdo-types.ts b/src/ratgdo-types.ts
index 95ff48f..fae9975 100644
--- a/src/ratgdo-types.ts
+++ b/src/ratgdo-types.ts
@@ -3,13 +3,20 @@
* ratgdo-types.ts: Interface and type definitions for Ratgdo.
*/
+export enum Firmware {
+
+ ESPHOME = 1,
+ MQTT
+}
+
// Ratgdo device settings.
export interface RatgdoDevice {
address: string,
firmwareVersion: string,
mac: string,
- name: string
+ name: string,
+ type: Firmware
}
// Define Ratgdo logging conventions.
@@ -24,6 +31,9 @@ export interface RatgdoLogging {
// Ratgdo reserved names.
export enum RatgdoReservedNames {
+ // Manage our dimmer types.
+ DIMMER_OPENER_AUTOMATION = "Dimmer.Opener.Automation",
+
// Manage our occupancy sensor types.
OCCUPANCY_SENSOR_DOOR_OPEN = "OccupancySensor.DoorOpen",
OCCUPANCY_SENSOR_MOTION = "OccupancySensor.Motion",