UNPKG

homebridge-gpio-blinds

Version:

Homebridge plugin to control blinds via Raspberry Pi GPIO pins

220 lines (185 loc) 8.77 kB
var _ = require('underscore'); var rpio = require('rpio'); var Service, Characteristic, HomebridgeAPI; const STATE_DECREASING = 0; const STATE_INCREASING = 1; const STATE_STOPPED = 2; module.exports = function(homebridge) { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; HomebridgeAPI = homebridge; homebridge.registerAccessory('homebridge-gpio-blinds', 'Blinds', BlindsAccessory); } function BlindsAccessory(log, config) { _.defaults(config, {durationOffset: 0, activeLow: true, reedSwitchActiveLow: true}); this.log = log; this.name = config['name']; this.pinUp = config['pinUp']; this.pinDown = config['pinDown']; this.durationUp = config['durationUp']; this.durationDown = config['durationDown']; this.durationOffset = config['durationOffset']; this.pinClosed = config['pinClosed']; this.pinOpen = config['pinOpen']; this.initialState = config['activeLow'] ? rpio.HIGH : rpio.LOW; this.activeState = config['activeLow'] ? rpio.LOW : rpio.HIGH; this.reedSwitchActiveState = config['reedSwitchActiveLow'] ? rpio.LOW : rpio.HIGH; this.cacheDirectory = HomebridgeAPI.user.persistPath(); this.storage = require('node-persist'); this.storage.initSync({dir:this.cacheDirectory, forgiveParseErrors: true}); var cachedCurrentPosition = this.storage.getItemSync(this.name); if((cachedCurrentPosition === undefined) || (cachedCurrentPosition === false)) { this.currentPosition = 0; // down by default } else { this.currentPosition = cachedCurrentPosition; } this.targetPosition = this.currentPosition; this.positionState = STATE_STOPPED; // stopped by default this.service = new Service.WindowCovering(this.name); this.infoService = new Service.AccessoryInformation(); this.infoService .setCharacteristic(Characteristic.Manufacturer, 'Radoslaw Sporny') .setCharacteristic(Characteristic.Model, 'RaspberryPi GPIO Blinds') .setCharacteristic(Characteristic.SerialNumber, 'Version 1.2.0'); this.finalBlindsStateTimeout; this.togglePinTimeout; this.intervalUp = this.durationUp / 100; this.intervalDown = this.durationDown / 100; this.currentPositionInterval; // use gpio pin numbering rpio.init({ mapping: 'gpio' }); rpio.open(this.pinUp, rpio.OUTPUT, this.initialState); rpio.open(this.pinDown, rpio.OUTPUT, this.initialState); if (this.pinClosed) rpio.open(this.pinClosed, rpio.INPUT, rpio.PULL_UP); if (this.pinOpen) rpio.open(this.pinOpen, rpio.INPUT, rpio.PULL_UP); this.service .getCharacteristic(Characteristic.CurrentPosition) .on('get', this.getCurrentPosition.bind(this)); this.service .getCharacteristic(Characteristic.PositionState) .on('get', this.getPositionState.bind(this)); this.service .getCharacteristic(Characteristic.TargetPosition) .on('get', this.getTargetPosition.bind(this)) .on('set', this.setTargetPosition.bind(this)); } BlindsAccessory.prototype.getPositionState = function(callback) { this.log("Position state: %s", this.positionState); callback(null, this.positionState); } BlindsAccessory.prototype.getCurrentPosition = function(callback) { this.log("Current position: %s", this.currentPosition); callback(null, this.currentPosition); } BlindsAccessory.prototype.getTargetPosition = function(callback) { var updatedPosition; if (this.openCloseSensorMalfunction()) { this.log("Open and close reed switches are active, setting to 50"); updatedPosition = 50; } else if (this.closedAndOutOfSync()) { this.log("Current position is out of sync, setting to 0"); updatedPosition = 0; } else if (this.openAndOutOfSync()) { this.log("Current position is out of sync, setting to 100"); updatedPosition = 100; } else if (this.partiallyOpenAndOutOfSync()) { this.log("Current position is out of sync, setting to 50"); updatedPosition = 50; } if (updatedPosition !== undefined) { this.currentPosition = updatedPosition; this.targetPosition = updatedPosition; this.storage.setItemSync(this.name, updatedPosition); } this.log("Target position: %s", this.targetPosition); callback(null, this.targetPosition); } BlindsAccessory.prototype.setTargetPosition = function(position, callback) { this.log("Setting target position to %s", position); // If blind is moving, stop it and do NOT set a new target position if (this.positionState != STATE_STOPPED) { this.log("Blind is moving, stopping movement before accepting new target position."); rpio.write(this.pinUp, this.initialState); rpio.write(this.pinDown, this.initialState); clearInterval(this.currentPositionInterval); clearTimeout(this.finalBlindsStateTimeout); clearTimeout(this.togglePinTimeout); this.targetPosition = this.currentPosition; this.positionState = STATE_STOPPED; this.service.setCharacteristic(Characteristic.PositionState, STATE_STOPPED); this.service.setCharacteristic(Characteristic.CurrentPosition, this.currentPosition); this.storage.setItemSync(this.name, this.currentPosition); this.setTargetPosition(this.currentPosition, callback); return true; } // Only set new target position if blind is stopped this.targetPosition = position; var moveUp = (this.targetPosition >= this.currentPosition); var duration; if (this.currentPosition == position) { this.log('Current position already matches target position. There is nothing to do.'); callback(); return true; } if (moveUp) { duration = Math.round((this.targetPosition - this.currentPosition) / 100 * this.durationUp); this.currentPositionInterval = setInterval(this.setCurrentPosition.bind(this, moveUp), this.intervalUp); } else { duration = Math.round((this.currentPosition - this.targetPosition) / 100 * this.durationDown); this.currentPositionInterval = setInterval(this.setCurrentPosition.bind(this, moveUp), this.intervalDown); } this.log((moveUp ? 'Moving up' : 'Moving down') + ". Duration: %s ms.", duration); this.service.setCharacteristic(Characteristic.PositionState, (moveUp ? STATE_INCREASING : STATE_DECREASING)); this.positionState = (moveUp ? STATE_INCREASING : STATE_DECREASING); if (this.durationOffset && (this.targetPosition == 0 || this.targetPosition == 100)) duration += this.durationOffset; this.finalBlindsStateTimeout = setTimeout(this.setFinalBlindsState.bind(this), duration); this.togglePin((moveUp ? this.pinUp : this.pinDown), duration); callback(); return true; } BlindsAccessory.prototype.togglePin = function(pin, duration) { if (rpio.read(pin) != this.activeState) rpio.write(pin, this.activeState); this.togglePinTimeout = setTimeout(function() { rpio.write(pin, this.initialState); }.bind(this), parseInt(duration)); } BlindsAccessory.prototype.setFinalBlindsState = function() { clearInterval(this.currentPositionInterval); this.positionState = STATE_STOPPED; this.service.setCharacteristic(Characteristic.PositionState, STATE_STOPPED); this.service.setCharacteristic(Characteristic.CurrentPosition, this.targetPosition); this.currentPosition = this.targetPosition; this.storage.setItemSync(this.name, this.currentPosition); this.log("Successfully moved to target position: %s", this.targetPosition); } BlindsAccessory.prototype.setCurrentPosition = function(moveUp) { if (moveUp) { this.currentPosition = Math.min(100, this.currentPosition + 1); } else { this.currentPosition = Math.max(0, this.currentPosition - 1); } this.storage.setItemSync(this.name, this.currentPosition); } BlindsAccessory.prototype.closedAndOutOfSync = function() { return this.currentPosition != 0 && this.pinClosed && (rpio.read(this.pinClosed) == this.reedSwitchActiveState); } BlindsAccessory.prototype.openAndOutOfSync = function() { return this.currentPosition != 100 && this.pinOpen && (rpio.read(this.pinOpen) == this.reedSwitchActiveState); } BlindsAccessory.prototype.partiallyOpenAndOutOfSync = function() { return (this.currentPosition == 0 && this.pinClosed && (rpio.read(this.pinClosed) != this.reedSwitchActiveState)) || (this.currentPosition == 100 && this.pinOpen && (rpio.read(this.pinOpen) != this.reedSwitchActiveState)); } BlindsAccessory.prototype.openCloseSensorMalfunction = function() { return (this.pinClosed && this.pinOpen && (rpio.read(this.pinClosed) == this.reedSwitchActiveState) && (rpio.read(this.pinOpen) == this.reedSwitchActiveState)); } BlindsAccessory.prototype.oppositeDirection = function(moveUp) { return (this.positionState == STATE_INCREASING && !moveUp) || (this.positionState == STATE_DECREASING && moveUp); } BlindsAccessory.prototype.getServices = function() { return [this.infoService, this.service]; }