UNPKG

homebridge-ratgdo

Version:

HomeKit integration using Ratgdo and Konnected devices for LiftMaster and Chamberlain garage door openers, without requiring myQ.

812 lines (811 loc) 68.3 kB
import { acquireService, sanitizeName, validService } 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.lock = this.hasFeature("Opener.Lock"); 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") + (this.device.model ? " " + this.device.model : "")); // 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 lock state. this.platform.mqtt?.subscribeGet(this.device.mac, "lock", "Lock", () => { return this.status.lock.toString(); }); // 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.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 if the user has enabled it. if (this.hints.lock) { service.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet((value) => { if (!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)); } else { // Remove any remnants of our locks. [this.hap.Characteristic.LockCurrentState, this.hap.Characteristic.LockTargetState] .map(characteristic => service.removeCharacteristic(service.getCharacteristic(characteristic))); } // 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, this.hints.light)) { return false; } // Acquire the service. const service = acquireService(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) => 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, this.hints.motionSensor)) { return false; } // Acquire the service. const service = acquireService(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, this.hints.automationDimmer, RatgdoReservedNames.DIMMER_OPENER_AUTOMATION)) { return false; } // Acquire the service. const service = acquireService(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, this.hints.automationSwitch, RatgdoReservedNames.SWITCH_OPENER_AUTOMATION)) { return false; } // Acquire the service. const service = acquireService(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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoBattery)) { return false; } // Acquire the service. const service = acquireService(this.accessory, this.hap.Service.Battery, this.name); if (!service) { this.log.error("Unable to add the Ratgdo (ESP32) 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) 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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoLaserSwitch, RatgdoReservedNames.SWITCH_DISCO_LASER)) { return false; } // Acquire the service. const service = acquireService(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) => 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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoLedSwitch, RatgdoReservedNames.SWITCH_DISCO_LED)) { return false; } // Acquire the service. const service = acquireService(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) => 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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoVehicleArriving, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_ARRIVING)) { return false; } // Acquire the service. const service = acquireService(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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoVehicleLeaving, RatgdoReservedNames.CONTACT_DISCO_VEHICLE_LEAVING)) { return false; } // Acquire the service. const service = acquireService(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, (this.device.variant === RatgdoVariant.RATGDO) && this.hints.discoVehiclePresence, RatgdoReservedNames.OCCUPANCY_DISCO_VEHICLE_PRESENCE)) { return false; } // Acquire the service. const service = acquireService(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, (this.device.variant === RatgdoVariant.KONNECTED) && this.hints.konnectedPcwSwitch, RatgdoReservedNames.SWITCH_KONNECTED_PCW)) { return false; } // Acquire the service. const service = acquireService(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((value) => { // Default to reseting our switch state after the pre-close warning has completed playing. let resetTimer = 5000; // Send the command. if (!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, (this.device.variant === RatgdoVariant.KONNECTED) && this.hints.konnectedStrobeSwitch, RatgdoReservedNames.SWITCH_KONNECTED_STROBE)) { return false; } // Acquire the service. const service = acquireService(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) => 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, this.hints.lock && this.hints.lockoutSwitch, RatgdoReservedNames.SWITCH_LOCKOUT)) { return false; } // Acquire the service. const service = acquireService(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((value) => { // Inform the user. this.log.info("Automation wireless remote lockout switch: remotes are %s.", value ? "locked out" : "permitted"); // Send the command. if (!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, this.hints.doorOpenOccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_DOOR_OPEN)) { return false; } // Acquire the service. const service = acquireService(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, this.hints.motionOccupancySensor, RatgdoReservedNames.OCCUPANCY_SENSOR_MOTION)) { return false; } // Acquire the service. const service = acquireService(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. 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. this.command("door", targetAction, position); } return true; } // Refresh our state. refresh() { this.command("refresh"); } // 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 information. this.configureInfo(); // 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("%sRatgdo %s.", (event.value === "encrypted") ? "\u{1F512}\uFE0E " : "", 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.status.discoVehicleLeaving !== (event.state === "ON")) { this.status.discoVehicleLeaving = event.state === "ON"; if (this.hints.logVehiclePresence) { this.log.info("Vehicle leaving %sdetected.", this.status.discoVehicleLeaving ? "" : "no longer "); } // Publish to MQTT, if the user has configured it. this.platform.mqtt?.publish(this.device.mac, "vehicleleaving", this.status.discoVehicleLeaving.toString()); } break; case "cover-door": case "cover-garage_door": // Determine what action the opener is currently executing. switch (event.current_operation) { case "CLOSING": case "OPENING": // eslint-disable-next-line camelcase event.current_operation = event.current_operation.toLowerCase(); break; case "IDLE": // We're in a stopped rather than open state if the door is in a position greater than 0. // eslint-disable-next-line camelcase event.current_operation = ((event.state === "OPEN") && (event.position !== undefined) && (event.position > 0) && (event.position < 1)) ? "stopped" : event.state.toLowerCase(); break; default: this.log.error("Unknown door operation detected: %s.", event.current_operation); return; } // Update our door position automation dimmer. if (event.position !== undefined) { this.status.doorPosition = event.position * 100; dimmerService?.updateCharacteristic(this.hap.Characteristic.Brightness, this.status.doorPosition); dimmerService?.updateCharacteristic(this.hap.Characteristic.On, this.status.doorPosition > 0); this.log.debug("Door state: %s% open.", this.status.doorPosition.toFixed(0)); } // If we're already in the state we're updating to, we're done. if (this.translateCurrentDoorState(this.status.door) === event.current_operation) { break;