From fd948995234c31f9fd4e271e838b06768cf2dacc Mon Sep 17 00:00:00 2001 From: HJD Date: Mon, 21 Aug 2023 09:27:20 -0500 Subject: [PATCH] New feature: automation switch support. Housekeeping. --- docs/FeatureOptions.md | 1 + src/myq-device.ts | 1 + src/myq-garagedoor.ts | 121 +++++++++++++++++++++++++++++++---------- src/myq-options.ts | 1 + 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/docs/FeatureOptions.md b/docs/FeatureOptions.md index e7adced..e9140cb 100644 --- a/docs/FeatureOptions.md +++ b/docs/FeatureOptions.md @@ -90,5 +90,6 @@ These option(s) apply to: myQ garage door and gate openers |--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | `Opener.ReadOnly` | Make this opener read-only by ignoring open and close requests from HomeKit. **(default: disabled)**. | `Opener.BatteryInfo` | Display battery status information for myQ door position sensors. You may want to disable this if the myQ status information is incorrectly resulting in a potential notification annoyance in the Home app. **(default: enabled)**.
*Supported on myQ devices that have a door position sensor.* +| `Opener.Switch` | Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers. **(default: disabled)**. | `Opener.OccupancySensor` | Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time. **(default: disabled)**. | `Opener.OccupancySensor.Duration.Value` | Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy. **(default: 300)**. diff --git a/src/myq-device.ts b/src/myq-device.ts index 4f00125..acdcf15 100644 --- a/src/myq-device.ts +++ b/src/myq-device.ts @@ -20,6 +20,7 @@ interface myQLogging { // Device-specific options and settings. interface myQHints { + automationSwitch: boolean, occupancyDuration: number, occupancySensor: boolean, readOnly: boolean, diff --git a/src/myq-garagedoor.ts b/src/myq-garagedoor.ts index 4d76df0..4459a98 100644 --- a/src/myq-garagedoor.ts +++ b/src/myq-garagedoor.ts @@ -33,6 +33,7 @@ export class myQGarageDoor extends myQAccessory { this.configureInfo(); this.configureGarageDoor(); this.configureBatteryInfo(); + this.configureSwitch(); this.configureOccupancySensor(); this.configureMqtt(); } @@ -44,6 +45,7 @@ export class myQGarageDoor extends myQAccessory { super.configureHints(); // Configure our device-class specific hints. + this.hints.automationSwitch = this.hasFeature("Opener.Switch"); this.hints.occupancySensor = this.hasFeature("Opener.OccupancySensor"); this.hints.occupancyDuration = this.getFeatureNumber("Opener.OccupancySensor.Duration") ?? MYQ_OCCUPANCY_DURATION; this.hints.readOnly = this.hasFeature("Opener.ReadOnly"); @@ -175,14 +177,80 @@ export class myQGarageDoor extends myQAccessory { return true; } + // Configure a switch to automate open and close events in HomeKit beyond what HomeKit might allow for a native garage opener service. + private configureSwitch(): boolean { + + // Find the switch service, if it exists. + let switchService = this.accessory.getService(this.hap.Service.Switch); + + // The switch is disabled by default and primarily exists for automation purposes. + if(!this.hints.automationSwitch) { + + if(switchService) { + + this.accessory.removeService(switchService); + this.log.info("Disabling automation switch."); + } + + return false; + } + + // Add the switch to the opener, if needed. + if(!switchService) { + + switchService = new this.hap.Service.Switch(this.name + " Automation Switch"); + + if(!switchService) { + + this.log.error("Unable to add automation switch."); + return false; + } + + this.accessory.addService(switchService); + } + + // Return the current state of the opener. + switchService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => { + + // We're on if we are in any state other than closed (specifically open or stopped). + return this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED; + }); + + // Open or close the opener. + switchService.getCharacteristic(this.hap.Characteristic.On)?.onSet((isOn: CharacteristicValue) => { + + // Inform the user. + this.log.info("Automation switch: %s.", isOn ? "open" : "close" ); + + // Send the command. + if(!this.setDoorState(isOn ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED)) { + + // Something went wrong. Let's make sure we revert the switch to it's prior state. + setTimeout(() => { + + switchService?.updateCharacteristic(this.hap.Characteristic.On, !isOn); + }, 50); + } + }); + + // Initialize the switch. + switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName); + switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Automation Switch"); + switchService.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED); + + this.log.info("Enabling automation switch."); + + return true; + } + // Configure the myQ open door occupancy sensor for HomeKit. - protected configureOccupancySensor(isEnabled = true, isInitialized = false): boolean { + protected configureOccupancySensor(): boolean { // Find the occupancy sensor service, if it exists. let occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor); // The occupancy sensor is disabled by default and primarily exists for automation purposes. - if(!isEnabled || !this.hints.occupancySensor) { + if(!this.hints.occupancySensor) { if(occupancyService) { @@ -208,26 +276,21 @@ export class myQGarageDoor extends myQAccessory { this.accessory.addService(occupancyService); } - // Have we previously initialized this sensor? We assume not by default, but this allows for scenarios where you may be dynamically reconfiguring a sensor at - // runtime. - if(!isInitialized) { - - // Ensure we can configure the name of the occupancy sensor. - occupancyService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName); - occupancyService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Open"); + // Ensure we can configure the name of the occupancy sensor. + occupancyService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName); + occupancyService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Open"); - // Initialize the state of the occupancy sensor. - occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false); - occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline); + // Initialize the state of the occupancy sensor. + occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false); + occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline); - occupancyService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => { + occupancyService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => { - return this.isOnline; - }); + return this.isOnline; + }); - this.log.info("Enabling the open indicator occupancy sensor. Occupancy will be triggered when the opener has been continuously open for more than %s seconds.", - this.hints.occupancyDuration); - } + this.log.info("Enabling the open indicator occupancy sensor. Occupancy will be triggered when the opener has been continuously open for more than %s seconds.", + this.hints.occupancyDuration); return true; } @@ -381,11 +444,8 @@ export class myQGarageDoor extends myQAccessory { // Update our HomeKit status. public updateState(): boolean { - // Update battery status only if it's supported by the device. - if(this.batteryDeviceSupport) { - - this.accessory.getService(this.hap.Service.Battery)?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.dpsBatteryStatus); - } + // Update our active status. + this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline); // Update our configured name, if requested. if(this.hints.syncNames) { @@ -398,8 +458,11 @@ export class myQGarageDoor extends myQAccessory { } } - // Update our active status. - this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline); + // Update battery status only if it's supported by the device. + if(this.batteryDeviceSupport) { + + this.accessory.getService(this.hap.Service.Battery)?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.dpsBatteryStatus); + } // Trigger our occupancy timer, if configured to do so. if(this.hints.occupancySensor) { @@ -449,8 +512,7 @@ export class myQGarageDoor extends myQAccessory { // in order to work around some notification quirks HomeKit occasionally has. if(this.status !== this.hap.Characteristic.CurrentDoorState.STOPPED) { - this.accessory.getService(this.hap.Service.GarageDoorOpener)?. - updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.doorTargetStateBias(this.status)); + this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.doorTargetStateBias(this.status)); } this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.status); @@ -459,13 +521,16 @@ export class myQGarageDoor extends myQAccessory { this.platform.pollOptions.count = 0; this.platform.poll(this.config.refreshInterval * -1); - // Inform the user of the state change. this.log.info("%s.", this.translateDoorState(this.status)); // Publish to MQTT, if the user has configured it. this.platform.mqtt?.publish(this.accessory, "garagedoor", this.translateDoorState(this.status).toLowerCase()); + // Update our automation switch, if it exists. + this.accessory.getService(this.hap.Service.Switch) + ?.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED); + return true; } diff --git a/src/myq-options.ts b/src/myq-options.ts index ea9c948..32525d6 100644 --- a/src/myq-options.ts +++ b/src/myq-options.ts @@ -44,6 +44,7 @@ export const featureOptions: { [index: string]: FeatureOption[] } = { { default: false, description: "Make this opener read-only by ignoring open and close requests from HomeKit.", name: "ReadOnly" }, { default: true, description: "Display battery status information for myQ door position sensors. You may want to disable this if the myQ status information is incorrectly resulting in a potential notification annoyance in the Home app.", hasProperty: [ "dps_low_battery_mode" ], name: "BatteryInfo" }, + { default: false, description: "Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers.", name: "Switch" }, { default: false, description: "Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time.", name: "OccupancySensor" }, { default: false, defaultValue: MYQ_OCCUPANCY_DURATION, description: "Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy.", group: "OccupancySensor", name: "OccupancySensor.Duration" } ]