UNPKG

garagedoor-accfactory

Version:

HomeKit garage door opener system using HAP-NodeJS library

372 lines (326 loc) 17.6 kB
// Code version 15/10/2024 // Mark Hulskamp 'use strict'; // Define external module requirements import GPIO from 'rpio'; // Define nodejs module requirements import EventEmitter from 'node:events'; import { setTimeout } from 'node:timers'; // Import our modules import HomeKitDevice from './HomeKitDevice.js'; export default class GarageDoor extends HomeKitDevice { static DOOREVENT = 'DOOREVENT'; // Door status event tag doorService = undefined; // HomeKit service for this garage door currentDoorStatus = undefined; // Internal data only for this class #eventEmitter = undefined; #lastDoorStatus = undefined; #moveStartedTime = undefined; constructor(accessory, api, log, eventEmitter, deviceData) { super(accessory, api, log, eventEmitter, deviceData); // Init the GPIO (rpio) library. This only needs to be done once before using library functions GPIO.init({ gpiomem: true }); GPIO.init({ mapping: 'gpio' }); // Validate if eventEmitter object passed to us is an instance of EventEmitter if (eventEmitter instanceof EventEmitter === true) { this.#eventEmitter = eventEmitter; } this.currentDoorStatus = 'stopped'; this.#lastDoorStatus = 'unknown'; } // Class functions addServices() { // Create extra details for output let postSetupDetails = []; // Setup the garagedoor service if not already present on the accessory this.doorService = this.accessory.getService(this.hap.Service.GarageDoorOpener); if (this.doorService === undefined) { this.doorService = this.accessory.addService(this.hap.Service.GarageDoorOpener, '', 1); } if (this.doorService.testCharacteristic(this.hap.Characteristic.StatusFault) === false) { // Used if the sensors report incorrect readings, such as both "high" this.doorService.addCharacteristic(this.hap.Characteristic.StatusFault); } this.doorService.setPrimaryService(); // Setup intial characteristic values this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.NO_FAULT); this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.STOPPED); // Setup GPIO pins if (this.deviceData.pushButton === undefined || (this.deviceData.pushButton < 0 && this.deviceData.pushButton > 26)) { this?.log?.warn && this.log.warn('No relay valid pin specifed for door open/close button on "%s"', this.deviceData.description); this?.log?.warn && this.log.warn('We will be unable to operated garage door'); } if (this.deviceData.pushButton !== undefined && this.deviceData.pushButton >= 0 && this.deviceData.pushButton <= 26) { // Push button GPIO.open(this.deviceData.pushButton, GPIO.OUTPUT, GPIO.LOW); this?.log?.debug && this.log.debug('Setup open/close relay on "%s" using GPIO pin "%s"', this.deviceData.description, this.deviceData.pushButton); } if (this.deviceData.closedSensor !== undefined && this.deviceData.closedSensor >= 0 && this.deviceData.closedSensor <= 26) { // Door closed sensor GPIO.open(this.deviceData.closedSensor, GPIO.INPUT, GPIO.PULL_DOWN); postSetupDetails.push('Door closed sensor'); this?.log?.debug && this.log.debug('Setup closed door sensor on "%s" using GPIO pin "%s"', this.deviceData.description, this.deviceData.closedSensor); } if (this.deviceData.openSensor !== undefined && this.deviceData.openSensor >= 0 && this.deviceData.openSensor <= 26) { // Door open sensor GPIO.open(this.deviceData.openSensor, GPIO.INPUT, GPIO.PULL_DOWN); postSetupDetails.push('Door open sensor'); this?.log?.debug && this.log.debug('Setup open door sensor on "%s" using GPIO pin "%s"', this.deviceData.description, this.deviceData.openSensor); } if ( this.deviceData.obstructionSensor !== undefined && this.deviceData.obstructionSensor >= 0 && this.deviceData.obstructionSensor <= 26 ) { // Door obstruction sensor GPIO.open(this.deviceData.obstructionSensor, GPIO.INPUT, GPIO.PULL_DOWN); postSetupDetails.push('Obstruction sensor'); this?.log?.debug && this.log.debug('Setup obstruction sensor on "%s" using GPIO pin "%s"', this.deviceData.description, this.deviceData.openSensor); } // Setup callbacks for characteristics this.doorService.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet((value) => { this.setDoorPosition(value); }); this.doorService.getCharacteristic(this.hap.Characteristic.CurrentDoorState).onGet(() => { let status = this.getDoorPosition(); // Convert our internal string status into the HomeKit number value return this.hap.Characteristic.CurrentDoorState[status.toUpperCase()]; }); // Setup linkage to EveHome app if configured todo so if ( this.deviceData?.eveHistory === true && this.doorService !== undefined && typeof this.historyService?.linkToEveHome === 'function' ) { this.historyService.linkToEveHome(this.doorService, { description: this.deviceData.description, }); } return postSetupDetails; } setDoorPosition(value) { // Set position of the door. (will either be open or closed) if ((value === this.hap.Characteristic.TargetDoorState.CLOSED || value === 'close') && this.isClosed() === false) { if (this.currentDoorState === 'opening') { // Since door is "moving", press button to stop. Second press below will close ie: reverse this.pressButton(); this.#lastDoorStatus = 'stopped'; } // "Press" garage opener/closer button, and update HomeKit status to show door moving. // the poll function will update to the closed status when sensor triggered this.pressButton(); } if ((value === this.hap.Characteristic.TargetDoorState.OPEN || value === 'open') && this.isOpen() === false) { if (this.currentDoorState === 'closing') { // Since door is "moving", press button to stop. Second press below will close ie: reverse this.pressButton(); this.#lastDoorStatus = 'stopped'; } // "Press" garage opener/closer button, and update HomeKit status to show door moving. // the poll function will update to the open status when sensor triggered this.pressButton(); } } getDoorPosition() { return this.currentDoorStatus; } pressButton() { if (this.deviceData.pushButton === undefined) { return; } // Simulate pressing the controller button // Write high out first to trigger relay, then wait defined millisecond period and put back to low to untrigger GPIO.write(this.deviceData.pushButton, GPIO.HIGH); GPIO.msleep(500); GPIO.write(this.deviceData.pushButton, GPIO.LOW); GPIO.msleep(500); } isOpen(openSensor) { if (this.deviceData.openSensor === undefined && openSensor === undefined) { return; } if (openSensor === undefined && this.deviceData.openSensor !== undefined) { openSensor = this.deviceData.openSensor; } return GPIO.read(openSensor) === GPIO.HIGH ? true : false; // If high on sensor, means door is opened } isClosed(closedSensor) { if (this.deviceData.closedSensor === undefined && closedSensor === undefined) { return; } if (closedSensor === undefined && this.deviceData.closedSensor !== undefined) { closedSensor = this.deviceData.closedSensor; } return GPIO.read(closedSensor) === GPIO.HIGH ? true : false; // If high on sensor, means door is closed } hasObstruction(obstructionSensor) { if (this.deviceData.obstructionSensor === undefined && obstructionSensor === undefined) { return; } if (obstructionSensor === undefined && this.deviceData.obstructionSensor !== undefined) { obstructionSensor = this.deviceData.obstructionSensor; } return GPIO.read(obstructionSensor) === GPIO.HIGH ? true : false; // If high, obstruction detected } messageServices(type, message) { if (type === GarageDoor.DOOREVENT) { if (message.status === 'closed' && this.currentDoorStatus !== 'closed') { // Closed this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.NO_FAULT); this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); this.doorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); this.currentDoorStatus = 'closed'; if (typeof this.historyService?.addHistory === 'function' && this.doorService !== undefined) { // Log door closed to history service if present let tempEntry = this.historyService.lastHistory(this.doorService); if (tempEntry?.status !== 0) { this.historyService.addHistory(this.doorService, { time: Math.floor(Date.now() / 1000), status: 0 }); // closed } } this?.log?.success && this.log.success('Door "%s" is closed', this.deviceData.description); } if (message.status === 'open' && this.currentDoorStatus !== 'open') { // Open this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.NO_FAULT); this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); this.doorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); this.currentDoorStatus = 'open'; if (typeof this.historyService?.addHistory === 'function' && this.doorService !== undefined) { // Log door opened to history service if present let tempEntry = this.historyService.lastHistory(this.doorService); if (tempEntry?.status !== 1) { this.historyService.addHistory(this.doorService, { time: Math.floor(Date.now() / 1000), status: 1 }); // open } } this?.log?.warn && this.log.warn('Door "%s" is open', this.deviceData.description); } if (message.status === 'moving') { // Moving this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.NO_FAULT); if (message.last === 'closed' && this.currentDoorStatus !== 'opening') { // Since door was last closed, and now its moving, assume its opening this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.OPENING); this.doorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); this.currentDoorStatus = 'opening'; this?.log?.debug && this.log.debug('Door "%s" is opening', this.deviceData.description); } if (message.last === 'open' && this.currentDoorStatus !== 'closing') { // Since door was last open, and now its moving, assume its closing this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSING); this.doorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.CurrentDoorState.CLOSED); this.currentDoorStatus = 'closing'; this?.log?.debug && this.log.debug('Door "%s" is closing', this.deviceData.description); } } if (message.status === 'stopped') { // Stopped if (this.currentDoorStatus !== 'stopped') { this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.NO_FAULT); this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.STOPPED); this.doorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.hap.Characteristic.CurrentDoorState.OPEN); this.currentDoorStatus = 'stopped'; if (typeof this.historyService?.addHistory === 'function' && this.doorService !== undefined) { // Log door opened to history service if present let tempEntry = this.historyService.lastHistory(this.doorService); if (tempEntry?.status !== 1) { this.historyService.addHistory(this.doorService, { time: Math.floor(Date.now() / 1000), status: 1 }); // open } } this?.log?.debug && this.log.debug('Door "%s" has stopped moving', this.deviceData.description); } } if (message.status === 'fault') { // Faulty sensors this.doorService.updateCharacteristic(this.hap.Characteristic.StatusFault, this.hap.Characteristic.StatusFault.GENERAL_FAULT); this.doorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.STOPPED); // What should current door status be??? this?.log?.error && this.log.error('Door "%s" is reporting fault with sensors', this.deviceData.description); } if (message.status === 'obstruction' || message.status === 'clear') { // Door obstruction, either active or cleared this.doorService.updateCharacteristic(this.hap.Characteristic.ObstructionDetected, message.status === 'obstruction' ? true : false); this?.log?.warn && this.log.warn('Door "%s" is reporting an obstruction', this.deviceData.description); // <---- Implement. Do we stop door from being closed if obstructed? or just allow to open?? or not allow movement at all? } } } updateServices(deviceData) { let doorOpen = this.isOpen(deviceData.openSensor); let doorClosed = this.isClosed(deviceData.closeSensor); let obstruction = this.hasObstruction(deviceData.obstructionSensor); // Work out the current status of the door using configured sensors. // This will either be "open", "closed", "moving", "stopped" // We'll send a message about its status once determined if (doorClosed === true && doorOpen === false) { // Door is fully closed this.#moveStartedTime = undefined; this.#lastDoorStatus = 'closed'; if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: 'closed' }); } } if (doorClosed === false && doorOpen === true) { // Door is fully open this.#moveStartedTime = undefined; this.#lastDoorStatus = 'open'; if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: 'open' }); } } if (doorClosed === false && doorOpen === false) { // Door is neither open or closed, so door is either moving or stopped. if (this.#moveStartedTime === undefined) { this.#moveStartedTime = Date.now(); // Time we detected first movement from either open or closed if (this.#lastDoorStatus === 'stopped') { // Detected movement after stopped state, ie: we've pressed the push button // Stopped state is assumed to be door open, as neither detected open or closed this.#lastDoorStatus = 'open'; } } let duration = Math.floor(Date.now()) - (this.#moveStartedTime !== undefined ? this.#moveStartedTime : 0); if ( this.#lastDoorStatus === 'unknown' || this.#lastDoorStatus === 'stopped' || (this.#lastDoorStatus === 'open' && duration > deviceData.closeTime * 1000) || (this.#lastDoorStatus === 'closed' && duration > deviceData.openTime * 1000) ) { // Since the door state isn't open or closed OR open or closed status and moving time has been exceeded for configured times // In this case we'll assume door has stopped this.#lastDoorStatus = 'stopped'; if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: 'stopped' }); } } else { if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: 'moving', last: this.#lastDoorStatus, duration: duration, }); } } } if (doorClosed === true && doorOpen === true) { // Is reading both open and close, we'll assume fault with sensors if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: 'fault', last: this.#lastDoorStatus }); } } if (obstruction !== undefined) { // Since obstruction didn't return an undefined value, this means we have a configured obstruction sensor and its returned its status if (this.#eventEmitter !== undefined) { this.#eventEmitter.emit(this.uuid, GarageDoor.DOOREVENT, { status: obstruction === true ? 'obstruction' : 'clear' }); } } // Perform this again after a short period by issuing an device update message // The updateServices function will only be called again from this message if some data has changed // We can force this by adding a "timestamp" field to the data object setTimeout(() => { this.#eventEmitter.emit(this.uuid, HomeKitDevice.UPDATE, { lastDoorCheckTime: Date.now() }); }, 1000); } }