homebridge-ratgdo 1.1.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,11 +75,21 @@ 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
+ }
76
82
  if (this.hints.readOnly) {
77
83
  this.log.info("Garage door opener is read-only. The opener will not respond to open and close requests from HomeKit.");
78
84
  }
79
85
  if (this.hints.syncName) {
80
- this.log.info("Syncing Ratgdo device name to HomeKit.");
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
+ }
81
93
  }
82
94
  return true;
83
95
  }
@@ -103,12 +115,21 @@ export class RatgdoAccessory {
103
115
  // Set our garage door state.
104
116
  this.platform.mqtt?.subscribeSet(this, "garagedoor", "Garage Door", (value) => {
105
117
  let command;
106
- switch (value) {
118
+ let position;
119
+ const action = value.split(" ");
120
+ switch (action[0]) {
107
121
  case "close":
108
122
  command = this.hap.Characteristic.TargetDoorState.CLOSED;
109
123
  break;
110
124
  case "open":
111
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
+ }
112
133
  break;
113
134
  default:
114
135
  this.log.error("Invalid command.");
@@ -116,7 +137,7 @@ export class RatgdoAccessory {
116
137
  break;
117
138
  }
118
139
  // Set our door state accordingly.
119
- this.setDoorState(command);
140
+ this.setDoorState(command, position);
120
141
  });
121
142
  // Return our obstruction state.
122
143
  this.platform.mqtt?.subscribeGet(this, "obstruction", "Obstruction", () => {
@@ -230,7 +251,7 @@ export class RatgdoAccessory {
230
251
  // Turn the light on or off.
231
252
  lightService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.light);
232
253
  lightService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
233
- this.command("light", value === true ? "on" : "off");
254
+ void this.command("light", value === true ? "on" : "off");
234
255
  });
235
256
  return true;
236
257
  }
@@ -266,6 +287,66 @@ export class RatgdoAccessory {
266
287
  motionService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
267
288
  return true;
268
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
+ }
269
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.
270
351
  configureAutomationSwitch() {
271
352
  // Find the switch service, if it exists.
@@ -378,11 +459,18 @@ export class RatgdoAccessory {
378
459
  return true;
379
460
  }
380
461
  // Open or close the garage door.
381
- setDoorState(value) {
382
- 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
+ }
383
471
  // If this garage door is read-only, we won't process any requests to set state.
384
472
  if (this.hints.readOnly) {
385
- 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);
386
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.
387
475
  setImmediate(() => {
388
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);
@@ -391,36 +479,23 @@ export class RatgdoAccessory {
391
479
  }
392
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.debug("Stop requested from user while transitioning between open and close states.");
482
+ this.log.debug("User-initiated stop requested while transitioning between open and close states.");
395
483
  // Execute the stop command.
396
- this.command("door", "stop");
484
+ void this.command("door", "stop");
397
485
  return true;
398
486
  }
399
- // Close the garage door.
400
- if (value === this.hap.Characteristic.TargetDoorState.CLOSED) {
401
- // HomeKit is asking us to close the garage door, but let's make sure it's not already closed first.
402
- if (this.status.door !== this.hap.Characteristic.CurrentDoorState.CLOSED) {
403
- // Execute the command.
404
- this.command("door", "close");
405
- }
406
- 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);
407
492
  }
408
- // Open the garage door.
409
- if (value === this.hap.Characteristic.TargetDoorState.OPEN) {
410
- // HomeKit is informing us to open the door, but we don't want to act if it's already open.
411
- if (this.status.door !== this.hap.Characteristic.CurrentDoorState.OPEN) {
412
- // Execute the command.
413
- this.command("door", "open");
414
- }
415
- return true;
416
- }
417
- // HomeKit has told us something that we don't know how to handle.
418
- this.log.error("Unknown HomeKit set event received: %s.", value);
419
- return false;
493
+ return true;
420
494
  }
421
495
  // Update the state of the accessory.
422
- updateState(event, payload) {
496
+ updateState(event, payload, position) {
423
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);
424
499
  const doorOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN);
425
500
  const garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);
426
501
  const lightBulbService = this.accessory.getService(this.hap.Service.Lightbulb);
@@ -438,32 +513,26 @@ export class RatgdoAccessory {
438
513
  motionOccupancyService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
439
514
  motionService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
440
515
  // Inform the user:
441
- 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);
442
517
  break;
443
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
+ }
444
526
  // If we're already in the state we're updating to, we're done.
445
527
  if (this.translateCurrentDoorState(this.status.door) === payload) {
446
528
  break;
447
529
  }
448
- // Clear out our door transition timer, if we have one.
449
- if (this.doorTimer) {
450
- clearTimeout(this.doorTimer);
451
- this.doorTimer = null;
452
- }
453
530
  switch (payload) {
454
531
  case "closed":
455
532
  this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
456
533
  break;
457
534
  case "closing":
458
535
  this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSING;
459
- // 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
460
- // receive an actual state update before this, this safety measure won't be triggered.
461
- this.doorTimer = setTimeout(() => {
462
- // Mark the door as closed.
463
- this.log.debug("Generating a close event to complete the state transition.");
464
- this.updateState("door", "closed");
465
- this.doorTimer = null;
466
- }, RATGDO_TRANSITION_DURATION * 1000);
467
536
  break;
468
537
  case "open":
469
538
  this.status.door = this.hap.Characteristic.CurrentDoorState.OPEN;
@@ -479,14 +548,6 @@ export class RatgdoAccessory {
479
548
  break;
480
549
  case "opening":
481
550
  this.status.door = this.hap.Characteristic.CurrentDoorState.OPENING;
482
- // 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
483
- // receive an actual state update before this, this safety measure won't be triggered.
484
- this.doorTimer = setTimeout(() => {
485
- // Mark the door as open.
486
- this.log.debug("Generating an open event to complete the state transition.");
487
- this.updateState("door", "open");
488
- this.doorTimer = null;
489
- }, RATGDO_TRANSITION_DURATION * 1000);
490
551
  break;
491
552
  case "stopped":
492
553
  this.status.door = this.hap.Characteristic.CurrentDoorState.STOPPED;
@@ -547,13 +608,11 @@ export class RatgdoAccessory {
547
608
  this.platform.mqtt?.publish(this, "lock", this.status.lock.toString());
548
609
  break;
549
610
  case "motion":
550
- this.status.motion = payload === "detected";
551
- // Motion no longer detected, clear out the motion sensor timer, and we're done.
552
- if (!this.status.motion && this.motionTimer) {
553
- clearTimeout(this.motionTimer);
554
- 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") {
555
613
  break;
556
614
  }
615
+ this.status.motion = true;
557
616
  // Update the motion sensor state.
558
617
  motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.status.motion);
559
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
@@ -613,6 +672,76 @@ export class RatgdoAccessory {
613
672
  break;
614
673
  }
615
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
+ }
616
745
  // Utility function to translate HomeKit's current door state values into human-readable form.
617
746
  translateCurrentDoorState(value) {
618
747
  // HomeKit state decoder ring.
@@ -637,6 +766,21 @@ export class RatgdoAccessory {
637
766
  }
638
767
  return "unknown";
639
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
+ }
640
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.
641
785
  doorCurrentStateBias(state) {
642
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
@@ -694,15 +838,6 @@ export class RatgdoAccessory {
694
838
  break;
695
839
  }
696
840
  }
697
- // Utility function to transmit a command to Ratgdo.
698
- command(topic, payload) {
699
- this.platform.broker.publish({ cmd: "publish", dup: false, payload: payload, qos: 2, retain: false, topic: this.device.name + "/command/" + topic }, (error) => {
700
- if (error) {
701
- this.log.error("Publish error:");
702
- this.log.error(util.inspect(error), { colors: true, depth: null, sorted: true });
703
- }
704
- });
705
- }
706
841
  // Utility function to return a floating point configuration parameter on a device.
707
842
  getFeatureFloat(option) {
708
843
  return getOptionFloat(getOptionValue(this.platform.configOptions, this.device, option));