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 +18 -9
- package/config.schema.json +1 -1
- package/dist/ratgdo-device.d.ts +4 -3
- package/dist/ratgdo-device.js +221 -82
- package/dist/ratgdo-device.js.map +1 -1
- package/dist/ratgdo-mqtt.d.ts +5 -5
- package/dist/ratgdo-mqtt.js +15 -15
- package/dist/ratgdo-mqtt.js.map +1 -1
- package/dist/ratgdo-options.js +2 -1
- package/dist/ratgdo-options.js.map +1 -1
- package/dist/ratgdo-platform.d.ts +4 -1
- package/dist/ratgdo-platform.js +169 -18
- package/dist/ratgdo-platform.js.map +1 -1
- package/dist/ratgdo-types.d.ts +6 -0
- package/dist/ratgdo-types.js +7 -0
- package/dist/ratgdo-types.js.map +1 -1
- package/homebridge-ui/public/index.html +2 -5
- package/package.json +13 -9
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
|
+
[](https://github.com/hjdhjd/homebridge-ratgdo)
|
|
5
|
+
|
|
4
6
|
# Homebridge Ratgdo
|
|
5
7
|
|
|
6
|
-
[](https://www.npmjs.com/package/homebridge-ratgdo)
|
|
9
|
+
[](https://www.npmjs.com/package/homebridge-ratgdo)
|
|
10
|
+
[](https://discord.gg/QXqfHEW)
|
|
9
11
|
[](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
|
-
[](https://img.shields.io/github/commits-since/hjdhjd/homebridge-ratgdo/latest?color=%
|
|
61
|
+
[](https://github.com/hjdhjd/homebridge-ratgdo/blob/main/LICENSE.md)
|
|
62
|
+
[](https://github.com/hjdhjd/homebridge-ratgdo/actions?query=workflow%3A%22Continuous+Integration%22)
|
|
63
|
+
[](https://libraries.io/npm/homebridge-ratgdo)
|
|
64
|
+
[](https://github.com/hjdhjd/homebridge-ratgdo/commits/main)
|
|
65
|
+
|
package/config.schema.json
CHANGED
package/dist/ratgdo-device.d.ts
CHANGED
|
@@ -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;
|
package/dist/ratgdo-device.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
const
|
|
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
|
|
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
|
|
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.
|
|
395
|
-
|
|
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
|
-
//
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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));
|