homebridge-ratgdo 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  <SPAN ALIGN="CENTER" STYLE="text-align:center">
2
2
  <DIV ALIGN="CENTER" STYLE="text-align:center">
3
3
 
4
+ [![homebridge-ratgdo: Native HomeKit support for Ratgdo](https://raw.githubusercontent.com/hjdhjd/homebridge-ratgdo/main/images/homebridge-ratgdo.svg)](https://github.com/hjdhjd/homebridge-ratgdo)
5
+
4
6
  # Homebridge Ratgdo
5
7
 
6
- [![Downloads](https://img.shields.io/npm/dt/homebridge-ratgdo?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-ratgdo)
7
- [![Version](https://img.shields.io/npm/v/homebridge-ratgdo?color=%235EB5E5&label=Homebridge%20Ratgdo&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-ratgdo)
8
- [![Ratgdo@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%235EB5E5&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
8
+ [![Downloads](https://img.shields.io/npm/dt/homebridge-ratgdo?color=%23000000&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-ratgdo)
9
+ [![Version](https://img.shields.io/npm/v/homebridge-ratgdo?color=%23000000&label=Homebridge%20Ratgdo&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-ratgdo)
10
+ [![Ratgdo@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%23000000&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
9
11
  [![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%2357277C&style=for-the-badge&logoColor=%23FFFFFF&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5OTIuMDkiIGhlaWdodD0iMTAwMCIgdmlld0JveD0iMCAwIDk5Mi4wOSAxMDAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYSIgZD0iTTk1MC4xOSw1MDguMDZhNDEuOTEsNDEuOTEsMCwwLDEtNDItNDEuOWMwLS40OC4zLS45MS4zLTEuNDJMODI1Ljg2LDM4Mi4xYTc0LjI2LDc0LjI2LDAsMCwxLTIxLjUxLTUyVjEzOC4yMmExNi4xMywxNi4xMywwLDAsMC0xNi4wOS0xNkg3MzYuNGExNi4xLDE2LjEsMCwwLDAtMTYsMTZWMjc0Ljg4bC0yMjAuMDktMjEzYTE2LjA4LDE2LjA4LDAsMCwwLTIyLjY0LjE5TDYyLjM0LDQ3Ny4zNGExNiwxNiwwLDAsMCwwLDIyLjY1bDM5LjM5LDM5LjQ5YTE2LjE4LDE2LjE4LDAsMCwwLDIyLjY0LDBMNDQzLjUyLDIyNS4wOWE3My43Miw3My43MiwwLDAsMSwxMDMuNjIuNDVMODYwLDUzOC4zOGE3My42MSw3My42MSwwLDAsMSwwLDEwNGwtMzguNDYsMzguNDdhNzMuODcsNzMuODcsMCwwLDEtMTAzLjIyLjc1TDQ5OC43OSw0NjguMjhhMTYuMDUsMTYuMDUsMCwwLDAtMjIuNjUuMjJMMjY1LjMsNjgwLjI5YTE2LjEzLDE2LjEzLDAsMCwwLDAsMjIuNjZsMzguOTIsMzlhMTYuMDYsMTYuMDYsMCwwLDAsMjIuNjUsMGwxMTQtMTEyLjM5YTczLjc1LDczLjc1LDAsMCwxLDEwMy4yMiwwbDExMywxMTEsLjQyLjQyYTczLjU0LDczLjU0LDAsMCwxLDAsMTA0TDU0NS4wOCw5NTcuMzV2LjcxYTQxLjk1LDQxLjk1LDAsMSwxLTQyLTQxLjk0Yy41MywwLC45NS4zLDEuNDQuM0w2MTYuNDMsODA0LjIzYTE2LjA5LDE2LjA5LDAsMCwwLDQuNzEtMTEuMzMsMTUuODUsMTUuODUsMCwwLDAtNC43OS0xMS4zMmwtMTEzLTExMWExNi4xMywxNi4xMywwLDAsMC0yMi42NiwwTDM2Ny4xNiw3ODIuNzlhNzMuNjYsNzMuNjYsMCwwLDEtMTAzLjY3LS4yN2wtMzktMzlhNzMuNjYsNzMuNjYsMCwwLDEsMC0xMDMuODZMNDM1LjE3LDQyNy44OGE3My43OSw3My43OSwwLDAsMSwxMDMuMzctLjlMNzU4LjEsNjM5Ljc1YTE2LjEzLDE2LjEzLDAsMCwwLDIyLjY2LDBsMzguNDMtMzguNDNhMTYuMTMsMTYuMTMsMCwwLDAsMC0yMi42Nkw1MDYuNSwyNjUuOTNhMTYuMTEsMTYuMTEsMCwwLDAtMjIuNjYsMEwxNjQuNjksNTgwLjQ0QTczLjY5LDczLjY5LDAsMCwxLDYxLjEsNTgwTDIxLjU3LDU0MC42OWwtLjExLS4xMmE3My40Niw3My40NiwwLDAsMSwuMTEtMTAzLjg4TDQzNi44NSwyMS40MUE3My44OSw3My44OSwwLDAsMSw1NDAsMjAuNTZMNjYyLjYzLDEzOS4zMnYtMS4xYTczLjYxLDczLjYxLDAsMCwxLDczLjU0LTczLjVINzg4YTczLjYxLDczLjYxLDAsMCwxLDczLjUsNzMuNVYzMjkuODFhMTYsMTYsMCwwLDAsNC43MSwxMS4zMmw4My4wNyw4My4wNWguNzlhNDEuOTQsNDEuOTQsMCwwLDEsLjA4LDgzLjg4WiIvPjwvc3ZnPg==)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
10
12
 
11
13
  ## Ratgdo-enabled garage door opener support for [Homebridge](https://homebridge.io).
@@ -29,17 +31,23 @@ In the interest of the community seeking a solution outside of myQ, I've develop
29
31
  * Obstruction detection.
30
32
  * Occupancy sensor support.
31
33
  * Read-only garage door opener support.
32
- * Automation switch support.
34
+ * Automation switch and dimmer support, allowing you to set the garage door to any position.
33
35
  * A rich webUI for configuration.
34
36
 
35
37
  ## Getting Started
36
38
  To get started with `homebridge-ratgdo`:
37
39
 
38
40
  * Install `homebridge-ratgdo` using the Homebridge webUI. Make sure you make `homebridge-ratgdo` a child bridge for the best performance.
41
+ * 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.
42
+ * That's it. Ensure `homebridge-ratgdo` is running and it will autodiscover your Ratgdo devices and make them available in HomeKit.
43
+
44
+ Deprecated instructions:
45
+ * 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.
39
46
  * [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`).
47
+ * **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.
40
48
 
41
49
  ## Known Caveats
42
- 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:
50
+ 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:
43
51
 
44
52
  * 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.
45
53
  * 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.
@@ -50,7 +58,8 @@ I hope these issues can be addressed in future Ratgdo releases.
50
58
  ## Plugin Development Dashboard
51
59
  This is mostly of interest to the true developer nerds amongst us.
52
60
 
53
- [![License](https://img.shields.io/npm/l/homebridge-ratgdo?color=%230559C9&logo=open%20source%20initiative&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/blob/main/LICENSE.md)
54
- [![Build Status](https://img.shields.io/github/actions/workflow/status/hjdhjd/homebridge-ratgdo/ci.yml?branch=main&color=%230559C9&logo=github-actions&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/actions?query=workflow%3A%22Continuous+Integration%22)
55
- [![Dependencies](https://img.shields.io/librariesio/release/npm/homebridge-ratgdo?color=%230559C9&logo=dependabot&style=for-the-badge)](https://libraries.io/npm/homebridge-ratgdo)
56
- [![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/hjdhjd/homebridge-ratgdo/latest?color=%230559C9&logo=github&sort=semver&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/commits/main)
61
+ [![License](https://img.shields.io/npm/l/homebridge-ratgdo?color=%23000000&logo=open%20source%20initiative&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/blob/main/LICENSE.md)
62
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/hjdhjd/homebridge-ratgdo/ci.yml?branch=main&color=%23000000&logo=github-actions&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/actions?query=workflow%3A%22Continuous+Integration%22)
63
+ [![Dependencies](https://img.shields.io/librariesio/release/npm/homebridge-ratgdo?color=%23000000&logo=dependabot&style=for-the-badge)](https://libraries.io/npm/homebridge-ratgdo)
64
+ [![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/hjdhjd/homebridge-ratgdo/latest?color=%23000000&logo=github&sort=semver&style=for-the-badge)](https://github.com/hjdhjd/homebridge-ratgdo/commits/main)
65
+
@@ -69,7 +69,7 @@
69
69
  "layout": [
70
70
  {
71
71
  "type": "section",
72
- "title": "Settings",
72
+ "title": "Ratgdo Settings (MQTT Firmware)",
73
73
  "expandable": true,
74
74
  "expanded": false,
75
75
  "items": [
@@ -7,7 +7,6 @@ export declare class RatgdoAccessory {
7
7
  private readonly config;
8
8
  readonly device: RatgdoDevice;
9
9
  private doorOccupancyTimer;
10
- private doorTimer;
11
10
  private readonly hap;
12
11
  private readonly hints;
13
12
  readonly log: RatgdoLogging;
@@ -24,16 +23,18 @@ export declare class RatgdoAccessory {
24
23
  private configureGarageDoor;
25
24
  private configureLight;
26
25
  private configureMotionSensor;
26
+ private configureAutomationDimmer;
27
27
  private configureAutomationSwitch;
28
28
  private configureDoorOpenOccupancySensor;
29
29
  private configureMotionOccupancySensor;
30
30
  private setDoorState;
31
- updateState(event: string, payload: string): void;
31
+ updateState(event: string, payload: string, position?: number): void;
32
+ private command;
32
33
  private translateCurrentDoorState;
34
+ private translateTargetDoorState;
33
35
  private doorCurrentStateBias;
34
36
  private doorTargetStateBias;
35
37
  private lockTargetStateBias;
36
- private command;
37
38
  private getFeatureFloat;
38
39
  private getFeatureNumber;
39
40
  private hasFeature;
@@ -1,5 +1,6 @@
1
- import { RATGDO_MOTION_DURATION, RATGDO_OCCUPANCY_DURATION, RATGDO_TRANSITION_DURATION } from "./settings.js";
2
- import { RatgdoReservedNames } from "./ratgdo-types.js";
1
+ import { FetchError, fetch } from "@adobe/fetch";
2
+ import { Firmware, RatgdoReservedNames } from "./ratgdo-types.js";
3
+ import { RATGDO_MOTION_DURATION, RATGDO_OCCUPANCY_DURATION } from "./settings.js";
3
4
  import { getOptionFloat, getOptionNumber, getOptionValue, isOptionEnabled } from "./ratgdo-options.js";
4
5
  import util from "node:util";
5
6
  export class RatgdoAccessory {
@@ -8,7 +9,6 @@ export class RatgdoAccessory {
8
9
  config;
9
10
  device;
10
11
  doorOccupancyTimer;
11
- doorTimer;
12
12
  hap;
13
13
  hints;
14
14
  log;
@@ -23,7 +23,6 @@ export class RatgdoAccessory {
23
23
  this.api = platform.api;
24
24
  this.status = {};
25
25
  this.config = platform.config;
26
- this.doorTimer = null;
27
26
  this.hap = this.api.hap;
28
27
  this.hints = {};
29
28
  this.device = device;
@@ -37,6 +36,7 @@ export class RatgdoAccessory {
37
36
  // Initialize our internal state.
38
37
  this.status.availability = false;
39
38
  this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
39
+ this.status.doorPosition = 0;
40
40
  this.status.light = false;
41
41
  this.status.lock = this.hap.Characteristic.LockCurrentState.UNSECURED;
42
42
  this.status.motion = false;
@@ -56,6 +56,7 @@ export class RatgdoAccessory {
56
56
  this.configureInfo();
57
57
  this.configureGarageDoor();
58
58
  this.configureMqtt();
59
+ this.configureAutomationDimmer();
59
60
  this.configureAutomationSwitch();
60
61
  this.configureDoorOpenOccupancySensor();
61
62
  this.configureLight();
@@ -64,6 +65,7 @@ export class RatgdoAccessory {
64
65
  }
65
66
  // Configure device-specific settings.
66
67
  configureHints() {
68
+ this.hints.automationDimmer = this.hasFeature("Opener.Dimmer");
67
69
  this.hints.automationSwitch = this.hasFeature("Opener.Switch");
68
70
  this.hints.doorOpenOccupancySensor = this.hasFeature("Opener.OccupancySensor");
69
71
  this.hints.doorOpenOccupancyDuration = this.getFeatureNumber("Opener.OccupancySensor.Duration") ?? RATGDO_OCCUPANCY_DURATION;
@@ -73,6 +75,22 @@ export class RatgdoAccessory {
73
75
  this.hints.motionSensor = this.hasFeature("Motion");
74
76
  this.hints.readOnly = this.hasFeature("Opener.ReadOnly");
75
77
  this.hints.syncName = this.hasFeature("Device.SyncName");
78
+ if (this.hints.automationDimmer && (this.device.type !== Firmware.ESPHOME)) {
79
+ this.hints.automationDimmer = false;
80
+ this.log.info("Automation dimmer support is only available on Ratgdo devices running on ESPHome firmware versions.");
81
+ }
82
+ if (this.hints.readOnly) {
83
+ this.log.info("Garage door opener is read-only. The opener will not respond to open and close requests from HomeKit.");
84
+ }
85
+ if (this.hints.syncName) {
86
+ if (this.device.type !== Firmware.MQTT) {
87
+ this.hints.syncName = false;
88
+ this.log.info("Syncing names is only available on Ratgdo devices running on MQTT firmware versions.");
89
+ }
90
+ else {
91
+ this.log.info("Syncing Ratgdo device name to HomeKit.");
92
+ }
93
+ }
76
94
  return true;
77
95
  }
78
96
  // Configure the device information for HomeKit.
@@ -97,12 +115,21 @@ export class RatgdoAccessory {
97
115
  // Set our garage door state.
98
116
  this.platform.mqtt?.subscribeSet(this, "garagedoor", "Garage Door", (value) => {
99
117
  let command;
100
- switch (value) {
118
+ let position;
119
+ const action = value.split(" ");
120
+ switch (action[0]) {
101
121
  case "close":
102
122
  command = this.hap.Characteristic.TargetDoorState.CLOSED;
103
123
  break;
104
124
  case "open":
105
125
  command = this.hap.Characteristic.TargetDoorState.OPEN;
126
+ // Parse the position information, if set.
127
+ if (this.device.type === Firmware.ESPHOME) {
128
+ position = parseFloat(action[1]);
129
+ if (isNaN(position) || (position < 0) || (position > 100)) {
130
+ position = undefined;
131
+ }
132
+ }
106
133
  break;
107
134
  default:
108
135
  this.log.error("Invalid command.");
@@ -110,7 +137,7 @@ export class RatgdoAccessory {
110
137
  break;
111
138
  }
112
139
  // Set our door state accordingly.
113
- this.setDoorState(command);
140
+ this.setDoorState(command, position);
114
141
  });
115
142
  // Return our obstruction state.
116
143
  this.platform.mqtt?.subscribeGet(this, "obstruction", "Obstruction", () => {
@@ -214,18 +241,17 @@ export class RatgdoAccessory {
214
241
  return false;
215
242
  }
216
243
  lightService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
244
+ lightService.updateCharacteristic(this.hap.Characteristic.Name, this.name);
245
+ this.setServiceName(lightService, this.name);
217
246
  this.accessory.addService(lightService);
218
247
  this.log.info("Enabling light.");
219
248
  }
220
249
  // Initialize the light.
221
- lightService.displayName = this.name;
222
- lightService.updateCharacteristic(this.hap.Characteristic.Name, this.name);
223
- lightService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name);
224
250
  lightService.updateCharacteristic(this.hap.Characteristic.On, this.status.light);
225
251
  // Turn the light on or off.
226
252
  lightService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.light);
227
253
  lightService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
228
- this.command("light", value === true ? "on" : "off");
254
+ void this.command("light", value === true ? "on" : "off");
229
255
  });
230
256
  return true;
231
257
  }
@@ -250,18 +276,77 @@ export class RatgdoAccessory {
250
276
  return false;
251
277
  }
252
278
  motionService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
279
+ motionService.updateCharacteristic(this.hap.Characteristic.Name, this.name);
280
+ this.setServiceName(motionService, this.name);
253
281
  this.accessory.addService(motionService);
254
282
  this.log.info("Enabling motion sensor.");
255
283
  }
256
284
  // Initialize the state of the motion sensor.
257
- motionService.displayName = this.name;
258
- motionService.updateCharacteristic(this.hap.Characteristic.Name, this.name);
259
- motionService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name);
260
285
  motionService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false);
261
286
  motionService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
262
287
  motionService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
263
288
  return true;
264
289
  }
290
+ // 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.
291
+ configureAutomationDimmer() {
292
+ // Find the dimmer service, if it exists.
293
+ let dimmerService = this.accessory.getServiceById(this.hap.Service.Lightbulb, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
294
+ // The switch is disabled by default and primarily exists for automation purposes.
295
+ if (!this.hints.automationDimmer) {
296
+ if (dimmerService) {
297
+ this.accessory.removeService(dimmerService);
298
+ this.log.info("Disabling automation dimmer.");
299
+ }
300
+ return false;
301
+ }
302
+ // Add the dimmer to the opener, if needed.
303
+ if (!dimmerService) {
304
+ dimmerService = new this.hap.Service.Lightbulb(this.name + " Automation Dimmer", RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
305
+ if (!dimmerService) {
306
+ this.log.error("Unable to add automation dimmer.");
307
+ return false;
308
+ }
309
+ dimmerService.displayName = this.name + " Automation Dimmer";
310
+ dimmerService.updateCharacteristic(this.hap.Characteristic.Name, this.name + " Automation Dimmer");
311
+ this.accessory.addService(dimmerService);
312
+ }
313
+ // Return the current state of the opener.
314
+ dimmerService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
315
+ // We're on if we are in any state other than closed (specifically open or stopped).
316
+ return this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED;
317
+ });
318
+ // Close the opener. Opening is really handled in the brightness event.
319
+ dimmerService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
320
+ // We really only want to act when the opener is open. Otherwise, it's handled by the brightness event.
321
+ if (value) {
322
+ return;
323
+ }
324
+ // Inform the user.
325
+ this.log.info("Automation dimmer: closing.");
326
+ // Send the command.
327
+ if (!this.setDoorState(this.hap.Characteristic.TargetDoorState.CLOSED)) {
328
+ // Something went wrong. Let's make sure we revert the dimmer to it's prior state.
329
+ setTimeout(() => {
330
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.On, !value);
331
+ }, 50);
332
+ }
333
+ });
334
+ // Return the door position of the opener.
335
+ dimmerService.getCharacteristic(this.hap.Characteristic.Brightness)?.onGet(() => {
336
+ return this.status.doorPosition;
337
+ });
338
+ // Adjust the door position of the opener by adjusting brightness of the light.
339
+ dimmerService.getCharacteristic(this.hap.Characteristic.Brightness)?.onSet((value) => {
340
+ this.log.info("Automation dimmer: moving opener to %s%.", value.toFixed(0));
341
+ this.setDoorState(value > 0 ?
342
+ this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED, value);
343
+ });
344
+ // Initialize the switch.
345
+ dimmerService.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
346
+ dimmerService.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition);
347
+ this.log.info("Enabling automation dimmer.");
348
+ return true;
349
+ }
265
350
  // 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.
266
351
  configureAutomationSwitch() {
267
352
  // Find the switch service, if it exists.
@@ -282,6 +367,7 @@ export class RatgdoAccessory {
282
367
  return false;
283
368
  }
284
369
  switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
370
+ this.setServiceName(switchService, this.name + " Automation Switch");
285
371
  this.accessory.addService(switchService);
286
372
  }
287
373
  // Return the current state of the opener.
@@ -302,7 +388,6 @@ export class RatgdoAccessory {
302
388
  }
303
389
  });
304
390
  // Initialize the switch.
305
- switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Automation Switch");
306
391
  switchService.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
307
392
  this.log.info("Enabling automation switch.");
308
393
  return true;
@@ -328,10 +413,9 @@ export class RatgdoAccessory {
328
413
  return false;
329
414
  }
330
415
  occupancyService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
416
+ this.setServiceName(occupancyService, this.name + " Open");
331
417
  this.accessory.addService(occupancyService);
332
418
  }
333
- // Ensure we can configure the name of the occupancy sensor.
334
- occupancyService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Open");
335
419
  // Initialize the state of the occupancy sensor.
336
420
  occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
337
421
  occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
@@ -362,10 +446,9 @@ export class RatgdoAccessory {
362
446
  return false;
363
447
  }
364
448
  occupancyService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
449
+ this.setServiceName(occupancyService, this.name);
365
450
  this.accessory.addService(occupancyService);
366
451
  }
367
- // Ensure we can configure the name of the occupancy sensor.
368
- occupancyService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name);
369
452
  // Initialize the state of the occupancy sensor.
370
453
  occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
371
454
  occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
@@ -376,55 +459,51 @@ export class RatgdoAccessory {
376
459
  return true;
377
460
  }
378
461
  // Open or close the garage door.
379
- setDoorState(value) {
380
- const actionExisting = this.status.door === this.hap.Characteristic.CurrentDoorState.OPENING ? "opening" : "closing";
381
- const actionAttempt = value === this.hap.Characteristic.TargetDoorState.CLOSED ? "close" : "open";
462
+ setDoorState(value, position) {
463
+ // Understand what we're targeting.
464
+ const targetAction = (position !== undefined) ? "set" : this.translateTargetDoorState(value);
465
+ // If we have an invalid target state, we're done.
466
+ if (targetAction === "unknown") {
467
+ // HomeKit has told us something that we don't know how to handle.
468
+ this.log.error("Unknown HomeKit set event received: %s.", value);
469
+ return false;
470
+ }
382
471
  // If this garage door is read-only, we won't process any requests to set state.
383
472
  if (this.hints.readOnly) {
384
- this.log.info("Unable to %s door. The door has been configured to be read only.", actionAttempt);
473
+ this.log.info("Unable to %s garage door: read-only mode enabled.", targetAction);
385
474
  // Tell HomeKit that we haven't in fact changed our state so we don't end up in an inadvertent opening or closing state.
386
475
  setImmediate(() => {
387
476
  this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, value === this.hap.Characteristic.TargetDoorState.CLOSED ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED);
388
477
  });
389
478
  return false;
390
479
  }
391
- // If we are already opening or closing the garage door, we error out. As a precaution, we ensure we complete the current action before allowing a new one.
392
- // This behavior may change in the future, but for now, we manage this edge case by eliminating the possibility of doing so.
480
+ // 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.
393
481
  if ((this.status.door === this.hap.Characteristic.CurrentDoorState.OPENING) || (this.status.door === this.hap.Characteristic.CurrentDoorState.CLOSING)) {
394
- this.log.error("Unable to %s door while currently attempting to complete %s. The existing action must be allowed to complete before attempting a new one.", actionAttempt, actionExisting);
395
- return false;
396
- }
397
- // Close the garage door.
398
- if (value === this.hap.Characteristic.TargetDoorState.CLOSED) {
399
- // HomeKit is asking us to close the garage door, but let's make sure it's not already closed first.
400
- if (this.status.door !== this.hap.Characteristic.CurrentDoorState.CLOSED) {
401
- // Execute the command.
402
- this.command("door", "close");
403
- }
482
+ this.log.debug("User-initiated stop requested while transitioning between open and close states.");
483
+ // Execute the stop command.
484
+ void this.command("door", "stop");
404
485
  return true;
405
486
  }
406
- // Open the garage door.
407
- if (value === this.hap.Characteristic.TargetDoorState.OPEN) {
408
- // HomeKit is informing us to open the door, but we don't want to act if it's already open.
409
- if (this.status.door !== this.hap.Characteristic.CurrentDoorState.OPEN) {
410
- // Execute the command.
411
- this.command("door", "open");
412
- }
413
- return true;
487
+ // Set the door state, assuming we're not already there.
488
+ if (this.status.door !== value) {
489
+ this.log.debug("User-initiated door state change: %s%s.", this.translateTargetDoorState(value), (position !== undefined) ? " (" + position.toString() + "%)" : "");
490
+ // Execute the command.
491
+ void this.command("door", targetAction, position);
414
492
  }
415
- // HomeKit has told us something that we don't know how to handle.
416
- this.log.error("Unknown HomeKit set event received: %s.", value);
417
- return false;
493
+ return true;
418
494
  }
419
495
  // Update the state of the accessory.
420
- updateState(event, payload) {
496
+ updateState(event, payload, position) {
421
497
  const camelCase = (text) => text.charAt(0).toUpperCase() + text.slice(1);
498
+ const dimmerService = this.accessory.getServiceById(this.hap.Service.Lightbulb, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
422
499
  const doorOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN);
423
500
  const garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);
424
501
  const lightBulbService = this.accessory.getService(this.hap.Service.Lightbulb);
425
502
  const motionOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION);
426
503
  const motionService = this.accessory.getService(this.hap.Service.MotionSensor);
427
504
  const switchService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_OPENER_AUTOMATION);
505
+ // We continuously rebroadcast our device information.
506
+ this.configureInfo();
428
507
  switch (event) {
429
508
  case "availability":
430
509
  this.status.availability = payload === "online";
@@ -434,32 +513,26 @@ export class RatgdoAccessory {
434
513
  motionOccupancyService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
435
514
  motionService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
436
515
  // Inform the user:
437
- this.log.info("Device %s.", this.status.availability ? "connected" : "disconnected");
516
+ this.log.info("Device %s (%s v%s).", this.status.availability ? "connected" : "disconnected", this.device.type === Firmware.MQTT ? "MQTT" : "ESPHome", this.device.firmwareVersion);
438
517
  break;
439
518
  case "door":
519
+ // Update our door position automation dimmer.
520
+ if (position !== undefined) {
521
+ this.status.doorPosition = position;
522
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition);
523
+ dimmerService?.updateCharacteristic(this.hap.Characteristic.On, this.status.doorPosition > 0);
524
+ this.log.debug("Door state: %s% open.", this.status.doorPosition.toFixed(0));
525
+ }
440
526
  // If we're already in the state we're updating to, we're done.
441
527
  if (this.translateCurrentDoorState(this.status.door) === payload) {
442
528
  break;
443
529
  }
444
- // Clear out our door transition timer, if we have one.
445
- if (this.doorTimer) {
446
- clearTimeout(this.doorTimer);
447
- this.doorTimer = null;
448
- }
449
530
  switch (payload) {
450
531
  case "closed":
451
532
  this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
452
533
  break;
453
534
  case "closing":
454
535
  this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSING;
455
- // 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
456
- // receive an actual state update before this, this safety measure won't be triggered.
457
- this.doorTimer = setTimeout(() => {
458
- // Mark the door as closed.
459
- this.log.debug("Generating a close event to complete the state transition.");
460
- this.updateState("door", "closed");
461
- this.doorTimer = null;
462
- }, RATGDO_TRANSITION_DURATION * 1000);
463
536
  break;
464
537
  case "open":
465
538
  this.status.door = this.hap.Characteristic.CurrentDoorState.OPEN;
@@ -475,14 +548,6 @@ export class RatgdoAccessory {
475
548
  break;
476
549
  case "opening":
477
550
  this.status.door = this.hap.Characteristic.CurrentDoorState.OPENING;
478
- // 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
479
- // receive an actual state update before this, this safety measure won't be triggered.
480
- this.doorTimer = setTimeout(() => {
481
- // Mark the door as open.
482
- this.log.debug("Generating an open event to complete the state transition.");
483
- this.updateState("door", "open");
484
- this.doorTimer = null;
485
- }, RATGDO_TRANSITION_DURATION * 1000);
486
551
  break;
487
552
  case "stopped":
488
553
  this.status.door = this.hap.Characteristic.CurrentDoorState.STOPPED;
@@ -543,13 +608,11 @@ export class RatgdoAccessory {
543
608
  this.platform.mqtt?.publish(this, "lock", this.status.lock.toString());
544
609
  break;
545
610
  case "motion":
546
- this.status.motion = payload === "detected";
547
- // Motion no longer detected, clear out the motion sensor timer, and we're done.
548
- if (!this.status.motion && this.motionTimer) {
549
- clearTimeout(this.motionTimer);
550
- this.motionTimer = null;
611
+ // We only want motion detected events. We timeout the motion event on our own to allow for automations and a more holistic user experience.
612
+ if (payload !== "detected") {
551
613
  break;
552
614
  }
615
+ this.status.motion = true;
553
616
  // Update the motion sensor state.
554
617
  motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.status.motion);
555
618
  // If we already have an inflight motion sensor timer, clear it out since we're restarting the timer. Also, if it's our first time detecting motion for this event
@@ -609,6 +672,76 @@ export class RatgdoAccessory {
609
672
  break;
610
673
  }
611
674
  }
675
+ // Utility function to transmit a command to Ratgdo.
676
+ async command(topic, payload, position) {
677
+ if (this.device.type === Firmware.MQTT) {
678
+ this.platform.broker.publish({ cmd: "publish", dup: false, payload: payload, qos: 2, retain: false, topic: this.device.name + "/command/" + topic }, (error) => {
679
+ if (error) {
680
+ this.log.error("Publish error:");
681
+ this.log.error(util.inspect(error), { colors: true, depth: null, sorted: true });
682
+ }
683
+ });
684
+ return;
685
+ }
686
+ // Now we handle ESPHome firmware commands.
687
+ let endpoint;
688
+ let action;
689
+ switch (topic) {
690
+ case "door":
691
+ endpoint = "cover/door";
692
+ switch (payload) {
693
+ case "closed":
694
+ action = "close";
695
+ break;
696
+ case "open":
697
+ case "stop":
698
+ action = payload;
699
+ break;
700
+ case "set":
701
+ if (position === undefined) {
702
+ this.log.error("Invalid door set command received: no position specified.");
703
+ return;
704
+ }
705
+ action = "set?position=" + (position / 100).toString();
706
+ break;
707
+ default:
708
+ this.log.error("Unknown door command received: %s.", payload);
709
+ return;
710
+ break;
711
+ }
712
+ break;
713
+ case "light":
714
+ endpoint = "light/light";
715
+ action = (payload === "on") ? "turn_on" : "turn_off";
716
+ break;
717
+ default:
718
+ this.log.error("Unknown command received: %s - %s.", topic, payload);
719
+ return;
720
+ break;
721
+ }
722
+ try {
723
+ // Execute the action.
724
+ const response = await fetch("http://" + this.device.address + "/" + endpoint + "/" + action, { body: JSON.stringify({}), method: "POST" });
725
+ if (!response?.ok) {
726
+ this.log.error("Unable to execute command: %s - %s.", event, payload);
727
+ return;
728
+ }
729
+ }
730
+ catch (error) {
731
+ if (error instanceof FetchError) {
732
+ switch (error.code) {
733
+ case "ECONNRESET":
734
+ this.log.error("Connection to the Ratgdo controller has been reset.");
735
+ break;
736
+ default:
737
+ this.log.error("Error sending command: %s %s.", error.code, error.message);
738
+ break;
739
+ }
740
+ return;
741
+ }
742
+ this.log.error("Error sending command: %s", error);
743
+ }
744
+ }
612
745
  // Utility function to translate HomeKit's current door state values into human-readable form.
613
746
  translateCurrentDoorState(value) {
614
747
  // HomeKit state decoder ring.
@@ -633,6 +766,21 @@ export class RatgdoAccessory {
633
766
  }
634
767
  return "unknown";
635
768
  }
769
+ // Utility function to translate HomeKit's target door state values into human-readable form.
770
+ translateTargetDoorState(value) {
771
+ // HomeKit state decoder ring.
772
+ switch (value) {
773
+ case this.hap.Characteristic.TargetDoorState.CLOSED:
774
+ return "closed";
775
+ break;
776
+ case this.hap.Characteristic.TargetDoorState.OPEN:
777
+ return "open";
778
+ break;
779
+ default:
780
+ break;
781
+ }
782
+ return "unknown";
783
+ }
636
784
  // Utility function to return our bias for what the current door state should be. This is primarily used for our initial bias on startup.
637
785
  doorCurrentStateBias(state) {
638
786
  // Our current door state reflects our opinion on what open or closed means in HomeKit terms. For the obvious states, this is easy. For some of the edge cases, it can
@@ -690,15 +838,6 @@ export class RatgdoAccessory {
690
838
  break;
691
839
  }
692
840
  }
693
- // Utility function to transmit a command to Ratgdo.
694
- command(topic, payload) {
695
- this.platform.broker.publish({ cmd: "publish", dup: false, payload: payload, qos: 2, retain: false, topic: this.device.name + "/command/" + topic }, (error) => {
696
- if (error) {
697
- this.log.error("Publish error:");
698
- this.log.error(util.inspect(error), { colors: true, depth: null, sorted: true });
699
- }
700
- });
701
- }
702
841
  // Utility function to return a floating point configuration parameter on a device.
703
842
  getFeatureFloat(option) {
704
843
  return getOptionFloat(getOptionValue(this.platform.configOptions, this.device, option));