homebridge-ratgdo
Version:
HomeKit integration using Ratgdo and Konnected devices for LiftMaster and Chamberlain garage door openers, without requiring myQ.
848 lines • 71.2 kB
JavaScript
import { acquireService, validService, validateName } from "homebridge-plugin-utils";
import { RATGDO_MOTION_DURATION, RATGDO_OCCUPANCY_DURATION } from "./settings.js";
import { RatgdoReservedNames, RatgdoVariant } from "./ratgdo-types.js";
import util from "node:util";
export class RatgdoAccessory {
accessory;
api;
config;
device;
doorOccupancyTimer;
hap;
hints;
log;
motionOccupancyTimer;
motionTimer;
obstructionTimer;
platform;
status;
// The constructor initializes key variables and calls configureDevice().
constructor(platform, accessory, device) {
this.accessory = accessory;
this.api = platform.api;
this.status = {};
this.config = platform.config;
this.hap = this.api.hap;
this.hints = {};
this.device = device;
this.platform = platform;
this.log = {
debug: (message, ...parameters) => platform.debug(util.format(this.name + ": " + message, ...parameters)),
error: (message, ...parameters) => platform.log.error(util.format(this.name + ": " + message, ...parameters)),
info: (message, ...parameters) => platform.log.info(util.format(this.name + ": " + message, ...parameters)),
warn: (message, ...parameters) => platform.log.warn(util.format(this.name + ": " + message, ...parameters))
};
// Initialize our internal state.
this.status.availability = false;
this.status.discoLaser = false;
this.status.discoLed = false;
this.status.discoBatteryState = false;
this.status.discoVehicleArriving = false;
this.status.discoVehicleLeaving = false;
this.status.discoVehiclePresence = false;
this.status.door = this.hap.Characteristic.CurrentDoorState.CLOSED;
this.status.doorPosition = 0;
this.status.konnectedStrobe = false;
this.status.light = false;
this.status.lock = this.hap.Characteristic.LockCurrentState.UNSECURED;
this.status.motion = false;
this.status.obstruction = false;
this.doorOccupancyTimer = null;
this.motionOccupancyTimer = null;
this.motionTimer = null;
this.obstructionTimer = null;
this.configureDevice();
}
// Configure a garage door accessory for HomeKit.
configureDevice() {
// Clean out the context object.
this.accessory.context = {};
// Configure ourselves.
this.configureHints();
this.configureInfo();
this.configureGarageDoor();
this.configureMqtt();
this.configureAutomationDoorPositionDimmer();
this.configureAutomationDoorSwitch();
this.configureAutomationLockoutSwitch();
this.configureDoorOpenOccupancySensor();
this.configureLight();
this.configureMotionSensor();
this.configureMotionOccupancySensor();
// Configure Ratgdo (ESP32) Disco-specific features.
this.configureDiscoBattery();
this.configureDiscoLaserSwitch();
this.configureDiscoLedSwitch();
this.configureDiscoVehicleArrivingContactSensor();
this.configureDiscoVehicleLeavingContactSensor();
this.configureDiscoVehiclePresenceOccupancySensor();
// Configure Konnected-specific features.
this.configureKonnectedPcwSwitch();
this.configureKonnectedStrobeSwitch();
}
// Configure device-specific settings.
configureHints() {
this.hints.automationDimmer = this.hasFeature("Opener.Dimmer");
this.hints.automationSwitch = this.hasFeature("Opener.Switch");
this.hints.discoBattery = this.hasFeature("Disco.Battery");
this.hints.discoLaserSwitch = this.hasFeature("Disco.Switch.Laser");
this.hints.discoLedSwitch = this.hasFeature("Disco.Switch.Led");
this.hints.discoVehicleArriving = this.hasFeature("Disco.ContactSensor.Vehicle.Arriving");
this.hints.discoVehicleLeaving = this.hasFeature("Disco.ContactSensor.Vehicle.Leaving");
this.hints.discoVehiclePresence = this.hasFeature("Disco.OccupancySensor.Vehicle.Presence");
this.hints.doorOpenOccupancySensor = this.hasFeature("Opener.OccupancySensor");
this.hints.doorOpenOccupancyDuration = this.platform.featureOptions.getInteger("Opener.OccupancySensor.Duration", this.device.mac) ?? RATGDO_OCCUPANCY_DURATION;
this.hints.konnectedPcwSwitch = this.hasFeature("Konnected.Switch.Pcw");
this.hints.konnectedStrobeSwitch = this.hasFeature("Konnected.Switch.Strobe");
this.hints.light = this.hasFeature("Light");
this.hints.logLight = this.hasFeature("Log.Light");
this.hints.logMotion = this.hasFeature("Log.Motion");
this.hints.logObstruction = this.hasFeature("Log.Obstruction");
this.hints.logOpener = this.hasFeature("Log.Opener");
this.hints.logVehiclePresence = this.hasFeature("Log.VehiclePresence");
this.hints.lockoutSwitch = this.hasFeature("Opener.Switch.RemoteLockout");
this.hints.motionOccupancySensor = this.hasFeature("Motion.OccupancySensor");
this.hints.motionOccupancyDuration = this.platform.featureOptions.getInteger("Motion.OccupancySensor.Duration", this.device.mac) ?? RATGDO_OCCUPANCY_DURATION;
this.hints.motionSensor = this.hasFeature("Motion");
this.hints.readOnly = this.hasFeature("Opener.ReadOnly");
if (this.hints.readOnly) {
this.log.info("Garage door opener is read-only. The opener will not respond to open and close requests from HomeKit.");
}
return true;
}
// Configure the device information for HomeKit.
configureInfo() {
// Update the manufacturer information for this device.
this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Manufacturer, "github.com/hjdhjd");
// Update the model information for this device.
this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Model, (this.device.variant === RatgdoVariant.KONNECTED) ? "Konnected" : "Ratgdo");
// Update the serial number for this device.
this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.SerialNumber, this.device.mac);
// Update the firmware information for this device.
this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, this.device.firmwareVersion);
return true;
}
// Configure MQTT services.
configureMqtt() {
// Return our garage door state.
this.platform.mqtt?.subscribeGet(this.device.mac, "garagedoor", "Garage Door", () => {
// Return our current status using our HomeKit current state decoder ring.
return this.translateCurrentDoorState(this.status.door);
});
// Set our garage door state.
this.platform.mqtt?.subscribeSet(this.device.mac, "garagedoor", "Garage Door", (value) => {
const action = value.split(" ");
let command;
let position;
switch (action[0]) {
case "close":
command = this.hap.Characteristic.TargetDoorState.CLOSED;
break;
case "open":
command = this.hap.Characteristic.TargetDoorState.OPEN;
// Parse the position information.
position = parseFloat(action[1]);
if (isNaN(position) || (position < 0) || (position > 100)) {
position = undefined;
}
break;
default:
this.log.error("Invalid command.");
return;
}
// Set our door state accordingly.
this.setDoorState(command, position);
});
// Return our obstruction state.
this.platform.mqtt?.subscribeGet(this.device.mac, "obstruction", "Obstruction", () => {
return this.status.obstruction.toString();
});
// Return our door open occupancy state if configured to do so.
if (this.hints.doorOpenOccupancySensor) {
this.platform.mqtt?.subscribeGet(this.device.mac, "dooropenoccupancy", "Door Open Indicator Occupancy", () => {
return (this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN)
?.getCharacteristic(this.hap.Characteristic.OccupancyDetected).value ?? "false").toString();
});
}
// Return our light state if configured to do so.
if (this.hints.light) {
this.platform.mqtt?.subscribeGet(this.device.mac, "light", "Light", () => {
return this.status.light.toString();
});
}
// Return our motion occupancy state if configured to do so.
if (this.hints.motionOccupancySensor) {
this.platform.mqtt?.subscribeGet(this.device.mac, "occupancy", "Occupancy", () => {
return (this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION)
?.getCharacteristic(this.hap.Characteristic.OccupancyDetected).value ?? "false").toString();
});
}
// Return our motion state if configured to do so.
if (this.hints.motionSensor) {
this.platform.mqtt?.subscribeGet(this.device.mac, "motion", "Motion", () => {
return this.status.motion.toString();
});
}
return true;
}
// Configure the garage door service for HomeKit.
configureGarageDoor() {
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.GarageDoorOpener, this.name);
if (!service) {
this.log.error("Unable to add the garage door.");
return false;
}
// Set the initial current and target door states to closed since ratgdo doesn't tell us initial state over MQTT on startup.
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.status.door);
service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.doorTargetStateBias(this.status.door));
// Handle HomeKit open and close events.
service.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet((value) => {
this.setDoorState(value);
});
// Inform HomeKit of our current state.
service.getCharacteristic(this.hap.Characteristic.CurrentDoorState).onGet(() => this.status.door);
// Inform HomeKit of any obstructions.
service.getCharacteristic(this.hap.Characteristic.ObstructionDetected).onGet(() => this.status.obstruction === true);
// Configure the lock garage door lock current and target state characteristics.
service.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(async (value) => {
if (!(await this.command("lock", (value === this.hap.Characteristic.LockTargetState.SECURED) ? "lock" : "unlock"))) {
// Something went wrong. Let's make sure we revert the lock to it's prior state.
setTimeout(() => {
service?.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.lockTargetStateBias(this.status.lock));
service?.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.status.lock);
}, 50);
}
});
service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.status.lock);
service.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.lockTargetStateBias(this.status.lock));
// Let HomeKit know that this is the primary service on this accessory.
service.setPrimaryService(true);
return true;
}
// Configure the light for HomeKit.
configureLight() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Lightbulb, () => {
// Have we disabled the light?
if (!this.hints.light) {
this.log.info("Disabling the light.");
return false;
}
return true;
})) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Lightbulb, this.name, undefined, () => this.log.info("Enabling light."));
if (!service) {
this.log.error("Unable to add the light.");
return false;
}
// Initialize the light.
service.updateCharacteristic(this.hap.Characteristic.On, this.status.light);
// Turn the light on or off.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.light);
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => void this.command("light", value === true ? "on" : "off"));
return true;
}
// Configure the motion sensor for HomeKit.
configureMotionSensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.MotionSensor, () => {
// Have we disabled the motion sensor?
if (!this.hints.motionSensor) {
this.log.info("Disabling the motion sensor.");
return false;
}
return true;
})) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.MotionSensor, this.name, undefined, () => this.log.info("Enabling motion sensor."));
if (!service) {
this.log.error("Unable to add the motion sensor.");
return false;
}
// Initialize the state of the motion sensor.
service.updateCharacteristic(this.hap.Characteristic.MotionDetected, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
return true;
}
// Configure a dimmer to automate open and close events in HomeKit beyond what HomeKit might allow for a garage opener service that gets treated as a secure service.
configureAutomationDoorPositionDimmer() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Lightbulb, () => {
// The door position dimmer is disabled by default and primarily exists for automation purposes.
if (!this.hints.automationDimmer) {
return false;
}
return true;
}, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Lightbulb, this.name + " Automation Door Position", RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
if (!service) {
this.log.error("Unable to add the automation door position dimmer.");
return false;
}
// Return the current state of the opener. We're on if we are in any state other than closed (specifically open or stopped).
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
// Close the opener. Opening is really handled in the brightness event.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
// We really only want to act when the opener is open. Otherwise, it's handled by the brightness event.
if (value) {
return;
}
// Inform the user.
if (this.hints.logOpener) {
this.log.info("Automation door position dimmer: closing.");
}
// Send the command.
if (!this.setDoorState(this.hap.Characteristic.TargetDoorState.CLOSED)) {
// Something went wrong. Let's make sure we revert the dimmer to it's prior state.
setTimeout(() => service?.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
}
});
// Return the door position of the opener.
service.getCharacteristic(this.hap.Characteristic.Brightness)?.onGet(() => this.status.doorPosition);
// Adjust the door position of the opener by adjusting brightness of the light.
service.getCharacteristic(this.hap.Characteristic.Brightness)?.onSet((value) => {
if (this.hints.logOpener) {
this.log.info("Automation door position dimmer: moving opener to %s%.", value.toFixed(0));
}
this.setDoorState(value > 0 ?
this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED, value);
});
// Initialize the dimmer.
service.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
service.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition);
this.log.info("Enabling the automation door position dimmer.");
return true;
}
// Configure a switch to automate open and close events in HomeKit beyond what HomeKit might allow for a garage opener service that gets treated as a secure service.
configureAutomationDoorSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// Have we disabled the automation switch?
if (!this.hints.automationSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_OPENER_AUTOMATION)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " Automation Opener", RatgdoReservedNames.SWITCH_OPENER_AUTOMATION);
if (!service) {
this.log.error("Unable to add the automation door opener switch.");
return false;
}
// Return the current state of the opener. We're on if we are in any state other than closed (specifically open or stopped).
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
// Open or close the opener.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
// Inform the user.
if (this.hints.logOpener) {
this.log.info("Automation door opener switch: %s.", value ? "open" : "close");
}
// Send the command.
if (!this.setDoorState(value ? 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(() => service?.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
}
});
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status.door) !== this.hap.Characteristic.CurrentDoorState.CLOSED);
this.log.info("Enabling the automation door opener switch.");
return true;
}
// Configure the Ratgdo (ESP32) Disco-specific backup battery service.
configureDiscoBattery() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Battery, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoBattery) {
return false;
}
return true;
})) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Battery, this.name);
if (!service) {
this.log.error("Unable to add the Ratgdo (ESP32) Disco backup battery status.");
return false;
}
// Return the current state of the charging state.
service.getCharacteristic(this.hap.Characteristic.ChargingState)?.onGet(() => this.status.discoBatteryState);
// Initialize the charging state.
service.updateCharacteristic(this.hap.Characteristic.ChargingState, this.status.discoBatteryState);
this.log.info("Enabling the Ratgdo (ESP32) Disco backup battery status.");
return true;
}
// Configure the Ratgdo (ESP32) Disco-specific parking assistance laser switch.
configureDiscoLaserSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoLaserSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_DISCO_LASER)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " Laser", RatgdoReservedNames.SWITCH_DISCO_LASER);
if (!service) {
this.log.error("Unable to add the Ratgdo (ESP32) Disco laser switch.");
return false;
}
// Return the current state of the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.discoLaser);
// Open or close the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => void this.command("disco-laser", value === true ? "on" : "off"));
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, this.status.discoLaser);
this.log.info("Enabling the Ratgdo (ESP32) Disco parking assistance laser switch.");
return true;
}
// Configure the Ratgdo (ESP32) Disco-specific LED switch.
configureDiscoLedSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoLedSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_DISCO_LED)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " LED", RatgdoReservedNames.SWITCH_DISCO_LED);
if (!service) {
this.log.error("Unable to add the Ratgdo (ESP32) Disco LED switch.");
return false;
}
// Return the current state of the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.discoLed);
// Open or close the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => void this.command("disco-led", value === true ? "on" : "off"));
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, this.status.discoLed);
this.log.info("Enabling the Ratgdo (ESP32) Disco LED switch.");
return true;
}
// Configure the vehicle arriving contact sensor for HomeKit.
configureDiscoVehicleArrivingContactSensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.ContactSensor, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoVehicleArriving) {
return false;
}
return true;
}, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_ARRIVING)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.ContactSensor, this.name + " Vehicle Arriving", RatgdoReservedNames.CONTACT_DISCO_VEHICLE_ARRIVING);
if (!service) {
this.log.error("Unable to add the vehicle arriving contact sensor.");
return false;
}
// Initialize the occupancy sensor.
service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
this.log.info("Enabling the Ratgdo (ESP32) Disco vehicle arriving contact sensor.");
return true;
}
// Configure the vehicle leaving contact sensor for HomeKit.
configureDiscoVehicleLeavingContactSensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.ContactSensor, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoVehicleLeaving) {
return false;
}
return true;
}, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_LEAVING)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.ContactSensor, this.name + " Vehicle Leaving", RatgdoReservedNames.CONTACT_DISCO_VEHICLE_LEAVING);
if (!service) {
this.log.error("Unable to add the vehicle leaving contact sensor.");
return false;
}
// Initialize the occupancy sensor.
service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
this.log.info("Enabling the Ratgdo (ESP32) Disco vehicle leaving contact sensor.");
return true;
}
// Configure the vehicle presence occupancy sensor for HomeKit.
configureDiscoVehiclePresenceOccupancySensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.OccupancySensor, () => {
// We only enable this on Ratgdo devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.RATGDO) || !this.hints.discoVehiclePresence) {
return false;
}
return true;
}, RatgdoReservedNames.OCCUPANCY_DISCO_VEHICLE_PRESENCE)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.OccupancySensor, this.name + " Vehicle Presence", RatgdoReservedNames.OCCUPANCY_DISCO_VEHICLE_PRESENCE);
if (!service) {
this.log.error("Unable to add the vehicle presence occupancy sensor.");
return false;
}
// Initialize the occupancy sensor.
service.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
this.log.info("Enabling the Ratgdo (ESP32) Disco vehicle presence occupancy sensor.");
return true;
}
// Configure the Konnected-specific pre-close warning switch.
configureKonnectedPcwSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// We only enable this on Konnected devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.KONNECTED) || !this.hints.konnectedPcwSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_KONNECTED_PCW)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " Pre Close Warning", RatgdoReservedNames.SWITCH_KONNECTED_PCW);
if (!service) {
this.log.error("Unable to add the Konnected pre-close warning switch.");
return false;
}
// Return the current state of the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => false);
// Open or close the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
// Default to reseting our switch state after the pre-close warning has completed playing.
let resetTimer = 5000;
// Send the command.
if (!(await this.command("konnected-pcw"))) {
// Something went wrong. Let's make sure we revert the switch to it's prior state immediately.
resetTimer = 50;
}
// Reset the switch state.
setTimeout(() => service?.updateCharacteristic(this.hap.Characteristic.On, !value), resetTimer);
});
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, false);
this.log.info("Enabling the Konnected pre-close warning switch.");
return true;
}
// Configure the Konnected-specific strobe switch.
configureKonnectedStrobeSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// We only enable this on Konnected devices when the user has enabled this capability.
if ((this.device.variant !== RatgdoVariant.KONNECTED) || !this.hints.konnectedStrobeSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_KONNECTED_STROBE)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " Strobe", RatgdoReservedNames.SWITCH_KONNECTED_STROBE);
if (!service) {
this.log.error("Unable to add the Konnected strobe switch.");
return false;
}
// Return the current state of the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.konnectedStrobe);
// Open or close the switch.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => void this.command("konnected-strobe", value === true ? "on" : "off"));
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, this.status.konnectedStrobe);
this.log.info("Enabling the Konnected strobe switch.");
return true;
}
// Configure a switch to control the ability to lockout all wireless remotes for the garage door opener, if the feature exists.
configureAutomationLockoutSwitch() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, () => {
// The wireless lockout switch is disabled by default and primarily exists for automation purposes.
if (!this.hints.lockoutSwitch) {
return false;
}
return true;
}, RatgdoReservedNames.SWITCH_LOCKOUT)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.name + " Lockout", RatgdoReservedNames.SWITCH_LOCKOUT);
if (!service) {
this.log.error("Unable to add the automation wireless remote lockout switch.");
return false;
}
// Return the current state of the opener. We're on if we are in any state other than locked.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.status.lock === this.hap.Characteristic.LockCurrentState.SECURED);
// Lock or unlock the wireless remotes.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
// Inform the user.
this.log.info("Automation wireless remote lockout switch: remotes are %s.", value ? "locked out" : "permitted");
// Send the command.
if (!(await this.command("lock", value ? "lock" : "unlock"))) {
// Something went wrong. Let's make sure we revert the switch to it's prior state.
setTimeout(() => service?.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
}
});
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.On, this.status.lock === this.hap.Characteristic.LockCurrentState.SECURED);
this.log.info("Enabling the automation wireless remote lockout switch.");
return true;
}
// Configure the door open occupancy sensor for HomeKit.
configureDoorOpenOccupancySensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.OccupancySensor, () => {
// The occupancy sensor is disabled by default and primarily exists for automation purposes.
if (!this.hints.doorOpenOccupancySensor) {
return false;
}
return true;
}, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.OccupancySensor, this.name + " Open", RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN);
if (!service) {
this.log.error("Unable to add the door open occupancy sensor.");
return false;
}
// Initialize the occupancy sensor.
service.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
this.log.info("Enabling the door open indicator occupancy sensor. Occupancy will be triggered when the opener has been continuously open for more than %s seconds.", this.hints.doorOpenOccupancyDuration);
return true;
}
// Configure the motion occupancy sensor for HomeKit.
configureMotionOccupancySensor() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.OccupancySensor, () => {
// The occupancy sensor is disabled by default and primarily exists for automation purposes.
if (!this.hints.motionOccupancySensor) {
return false;
}
return true;
}, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION)) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.OccupancySensor, this.name, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION);
if (!service) {
this.log.error("Unable to add the occupancy sensor.");
return false;
}
// Initialize the occupancy sensor.
service.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.status.availability);
service.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => this.status.availability);
this.log.info("Enabling the occupancy sensor. Occupancy event duration set to %s seconds.", this.hints.motionOccupancyDuration);
return true;
}
// Open or close the garage door.
setDoorState(value, position) {
// Understand what we're targeting.
const targetAction = (position !== undefined) ? "set" : this.translateTargetDoorState(value);
// If we have an invalid target state, we're done.
if (targetAction === "unknown") {
// HomeKit has told us something that we don't know how to handle.
this.log.error("Unknown HomeKit set event received: %s.", value);
return false;
}
// If this garage door is read-only, we won't process any requests to set state.
if (this.hints.readOnly) {
this.log.info("Unable to %s garage door: read-only mode enabled.", targetAction);
// Tell HomeKit that we haven't in fact changed our state so we don't end up in an inadvertent opening or closing state.
setImmediate(() => {
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);
});
return false;
}
// If we are already opening or closing the garage door, we assume the user wants to stop the garage door opener at it's current location.
if ((this.status.door === this.hap.Characteristic.CurrentDoorState.OPENING) || (this.status.door === this.hap.Characteristic.CurrentDoorState.CLOSING)) {
this.log.debug("User-initiated stop requested while transitioning between open and close states.");
// Execute the stop command.
void this.command("door", "stop");
return true;
}
// Set the door state, assuming we're not already there.
if (this.status.door !== value) {
this.log.debug("User-initiated door state change: %s%s.", this.translateTargetDoorState(value), (position !== undefined) ? " (" + position.toString() + "%)" : "");
// Execute the command.
void this.command("door", targetAction, position);
}
return true;
}
// Update the state of the accessory.
updateState(event) {
const camelCase = (text) => text.charAt(0).toUpperCase() + text.slice(1);
const dimmerService = this.accessory.getServiceById(this.hap.Service.Lightbulb, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION);
const discoBatteryService = this.accessory.getService(this.hap.Service.Battery);
const discoLaserSwitchService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_DISCO_LASER);
const discoLedSwitchService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_DISCO_LED);
const discoVehicleArrivingContactService = this.accessory.getServiceById(this.hap.Service.ContactSensor, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_ARRIVING);
const discoVehicleLeavingContactService = this.accessory.getServiceById(this.hap.Service.ContactSensor, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_LEAVING);
const discoVehiclePresenceOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_DISCO_VEHICLE_PRESENCE);
const doorOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN);
const garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);
const konnectedStrobeSwitchService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_KONNECTED_STROBE);
const lightBulbService = this.accessory.getService(this.hap.Service.Lightbulb);
const lockoutService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_LOCKOUT);
const motionOccupancyService = this.accessory.getServiceById(this.hap.Service.OccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION);
const motionService = this.accessory.getService(this.hap.Service.MotionSensor);
const switchService = this.accessory.getServiceById(this.hap.Service.Switch, RatgdoReservedNames.SWITCH_OPENER_AUTOMATION);
switch (event.id) {
case "availability":
// Update our availability.
discoVehicleArrivingContactService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
discoVehicleLeavingContactService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
discoVehiclePresenceOccupancyService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
doorOccupancyService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
motionOccupancyService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
motionService?.updateCharacteristic(this.hap.Characteristic.StatusActive, event.state === "online");
// Inform the user if our availability state has changed.
if (this.status.availability !== (event.state === "online")) {
this.status.availability = event.state === "online";
this.log.info("Ratgdo %s.", this.status.availability ? "connected" : "disconnected");
}
break;
case "battery":
switch (event.state) {
case "CHARGING":
this.status.discoBatteryState = this.hap.Characteristic.ChargingState.CHARGING;
break;
case "FULL":
case "UNKNOWN":
this.status.discoBatteryState = this.hap.Characteristic.ChargingState.NOT_CHARGING;
break;
default:
this.log.error("Unknown battery state received: %s", event.state);
return;
}
discoBatteryService?.updateCharacteristic(this.hap.Characteristic.ChargingState, this.status.discoBatteryState);
break;
case "binary_sensor-motion":
// We only want motion detected events. We timeout the motion event on our own to allow for automations and a more holistic user experience.
if (event.state !== "ON") {
break;
}
this.status.motion = true;
// Update the motion sensor state.
motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.status.motion);
// 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
// cycle, let the user know.
if (this.motionTimer) {
clearTimeout(this.motionTimer);
}
else {
if (this.hints.logMotion) {
this.log.info("Motion detected.");
}
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "motion", this.status.motion.toString());
}
// Set a timer for the motion event.
this.motionTimer = setTimeout(() => {
this.status.motion = false;
motionService?.updateCharacteristic(this.hap.Characteristic.MotionDetected, this.status.motion);
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "motion", this.status.motion.toString());
}, RATGDO_MOTION_DURATION * 1000);
// If we don't have occupancy sensor support configured, we're done.
if (!this.hints.motionOccupancySensor) {
break;
}
// Kill any inflight occupancy sensor.
if (this.motionOccupancyTimer) {
clearTimeout(this.motionOccupancyTimer);
this.motionOccupancyTimer = null;
}
// If the motion occupancy sensor isn't already triggered, let's do so now.
if (motionOccupancyService?.getCharacteristic(this.hap.Characteristic.OccupancyDetected).value !== true) {
// Trigger the occupancy event in HomeKit.
motionOccupancyService?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, true);
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "occupancy", "true");
// Log the event.
if (this.hints.logMotion) {
this.log.info("Occupancy detected.");
}
}
// Reset our occupancy state after occupancyDuration.
this.motionOccupancyTimer = setTimeout(() => {
// Reset the occupancy sensor.
motionOccupancyService?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "occupancy", "false");
// Log the event.
if (this.hints.logMotion) {
this.log.info("Occupancy no longer detected.");
}
// Delete the timer.
this.motionOccupancyTimer = null;
}, this.hints.motionOccupancyDuration * 1000);
break;
case "binary_sensor-obstruction":
garageDoorService?.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, event.state === "ON");
// Only act if we're not already at the state we're updating to.
if (this.status.obstruction !== (event.state === "ON")) {
this.status.obstruction = event.state === "ON";
if (this.hints.logObstruction) {
this.log.info("Obstruction %sdetected.", this.status.obstruction ? "" : "no longer ");
}
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "obstruction", this.status.obstruction.toString());
}
break;
case "binary_sensor-vehicle_arriving":
discoVehicleArrivingContactService?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, event.state === "ON");
// Only act if we're not already at the state we're updating to.
if (this.status.discoVehicleArriving !== (event.state === "ON")) {
this.status.discoVehicleArriving = event.state === "ON";
if (this.hints.logVehiclePresence) {
this.log.info("Vehicle arriving %sdetected.", this.status.discoVehicleArriving ? "" : "no longer ");
}
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "vehiclearriving", this.status.discoVehicleArriving.toString());
}
break;
case "binary_sensor-vehicle_detected":
discoVehiclePresenceOccupancyService?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, event.state === "ON");
// Only act if we're not already at the state we're updating to.
if (this.status.discoVehiclePresence !== (event.state === "ON")) {
this.status.discoVehiclePresence = event.state === "ON";
if (this.hints.logVehiclePresence) {
this.log.info("Vehicle %sdetected.", this.status.discoVehiclePresence ? "" : "no longer ");
}
// Publish to MQTT, if the user has configured it.
this.platform.mqtt?.publish(this.device.mac, "vehiclepresence", this.status.discoVehiclePresence.toString());
}
break;
case "binary_sensor-vehicle_leaving":
discoVehicleLeavingContactService?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, event.state === "ON");
// Only act if we're not already at the state we're updating to.
if (this.s