Last active
April 30, 2021 07:53
-
-
Save tarag/5131b1e7bf08c5cf1fed7f7be7876a67 to your computer and use it in GitHub Desktop.
Position estimator for openHAB roller shutters using JSR223 JS rules (OH3)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* js-timeout-polyfill | |
* @see https://blogs.oracle.com/nashorn/entry/setinterval_and_settimeout_javascript_functions | |
*/ | |
(function (global) { | |
'use strict'; | |
if (global.setTimeout || | |
global.clearTimeout || | |
global.setInterval || | |
global.clearInterval) { | |
return; | |
} | |
var Timer = global.Java.type('java.util.Timer'); | |
function toCompatibleNumber(val) { | |
switch (typeof val) { | |
case 'number': | |
break; | |
case 'string': | |
val = parseInt(val, 10); | |
break; | |
case 'boolean': | |
case 'object': | |
val = 0; | |
break; | |
} | |
return val > 0 ? val : 0; | |
} | |
function setTimerRequest(handler, delay, interval, args) { | |
handler = handler || function () { | |
}; | |
delay = toCompatibleNumber(delay); | |
interval = toCompatibleNumber(interval); | |
var applyHandler = function () { | |
handler.apply(this, args); | |
}; | |
/*var runLater = function () { | |
Platform.runLater(applyHandler); | |
};*/ | |
var timer; | |
if (interval > 0) { | |
timer = new Timer('setIntervalRequest', true); | |
timer.schedule(applyHandler, delay, interval); | |
} else { | |
timer = new Timer('setTimeoutRequest', false); | |
timer.schedule(applyHandler, delay); | |
} | |
return timer; | |
} | |
function clearTimerRequest(timer) { | |
timer.cancel(); | |
} | |
///////////////// | |
// Set polyfills | |
///////////////// | |
global.setInterval = function setInterval() { | |
var args = Array.prototype.slice.call(arguments); | |
var handler = args.shift(); | |
var ms = args.shift(); | |
return setTimerRequest(handler, ms, ms, args); | |
}; | |
global.clearInterval = function clearInterval(timer) { | |
clearTimerRequest(timer); | |
}; | |
global.setTimeout = function setTimeout() { | |
var args = Array.prototype.slice.call(arguments); | |
var handler = args.shift(); | |
var ms = args.shift(); | |
return setTimerRequest(handler, ms, 0, args); | |
}; | |
global.clearTimeout = function clearTimeout(timer) { | |
clearTimerRequest(timer); | |
}; | |
global.setImmediate = function setImmediate() { | |
var args = Array.prototype.slice.call(arguments); | |
var handler = args.shift(); | |
return setTimerRequest(handler, 0, 0, args); | |
}; | |
global.clearImmediate = function clearImmediate(timer) { | |
clearTimerRequest(timer); | |
}; | |
})(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Items sending UP and DOWN commands to the actual physical shutter, via the rfxcom for Somfy shutters in this case | |
Rollershutter F0_Salon_Store_Sud_RS_CMD "Store sud" { channel="rfxcom:rfy:usb0:F0_Salon_Store_Sud:shutter" } | |
Rollershutter F0_Salon_Store_Nord_RS_CMD "Store nord" { channel="rfxcom:rfy:usb0:F0_Salon_Store_Nord:shutter" } | |
Rollershutter F0_Balcon_Store_Est_RS_CMD "Petit store banne" { channel="rfxcom:rfy:usb0:F0_Balcon_Store_Est:shutter" } | |
Rollershutter F0_Balcon_Store_Ouest_RS_CMD "Grand store banne" { channel="rfxcom:rfy:usb0:F0_Balcon_Store_Ouest:shutter" } | |
// `Virtual items` used by the position estimator, that can be linked to Homekit or other OH UI in order to control via either UP/DOWN or absolute position | |
Rollershutter F0_Salon_Store_Sud_RS_VAL "Store sud" [ "WindowCovering" ] { autoupdate="false" } | |
Rollershutter F0_Salon_Store_Nord_RS_VAL "Store nord" [ "WindowCovering" ] { autoupdate="false" } | |
Rollershutter F0_Balcon_Store_Est_RS_VAL "Petit store banne" [ "WindowCovering" ] { autoupdate="false" } | |
Rollershutter F0_Balcon_Store_Ouest_RS_VAL "Grand store banne" [ "WindowCovering" ] { autoupdate="false" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
var OPENHAB_CONF = Java.type("java.lang.System").getProperty("openhab.conf"); | |
var JS_PATH = OPENHAB_CONF + "/automation/lib/javascript"; | |
load(JS_PATH + "/core/rules.js"); | |
load(JS_PATH + "/core/triggers.js"); | |
load(JS_PATH + "/core/metadata.js"); | |
load(JS_PATH + "/core/log.js"); | |
load(JS_PATH + "/personal/js-timeout-polyfill.js"); // from https://gist.github.com/kayhadrin/4bf141103420b79126e434dfcf6d4826 | |
var ZonedDateTime = Java.type("java.time.ZonedDateTime"); | |
var ChronoUnit = Java.type("java.time.temporal.ChronoUnit"); | |
var LOG = getLogger(LOG_PREFIX + ".vasrollershutter"); | |
LOG.info("VASRollershutter (re)loaded!"); | |
// This JS class is used to manage a rollershutter position in an absolute fashion using movement times | |
// Conventions: fully up position corresponds to 0, fully down position corresponds to 100 (OH convention) | |
function VASRollershutter(name, uptime, downtime) { | |
this.name = name; | |
this.itemStateStr = name + "_RS_VAL"; | |
this.realItemStr = name + "_RS_CMD"; | |
this.uptime = uptime; | |
this.downtime = downtime; | |
this.stopTime = 1000; | |
this.position = 0; // Current position if stopped, or start position during travel 0: fully up, 100: fully down | |
this.targetPosition = 0; // Target position during movement | |
this.isMoving = false; | |
this.updateTimer = null; | |
this.direction = null; // "UP" or "DOWN" current movement direction | |
this.movingSince = null; // Time since which the last movement command has been sent | |
this.stopTimer = null; // Current timer used to stop movement | |
VASRollershutter.shutters[name] = this; | |
}; | |
VASRollershutter.prototype.processCommand = function (command) { | |
LOG.trace("VASRollershutter " + this.name + " processCommand(" + command + ")"); | |
this._updateState(this._currentPosition()); | |
if (command == "UP") { | |
this.moveTo(0); | |
} else if (command == "DOWN") { | |
this.moveTo(100); | |
} else if (command == "STOP") { | |
this.stop(); | |
} else { | |
var targetPosition = parseInt(command); | |
if (isNaN(targetPosition)) { | |
LOG.info("VASRollershutter " + this.name + " processCommand(" + command + "): invalid command"); | |
} else { | |
this.moveTo(targetPosition) | |
} | |
} | |
} | |
VASRollershutter.prototype.moveTo = function (targetPosition) { | |
if (targetPosition < 0 || targetPosition > 100) { | |
LOG.error("VASRollershutter moveTo() invalid position:" + targetPosition); | |
return; | |
} | |
if (targetPosition == this.position && !this.isMoving) { | |
LOG.info("VASRollershutter moveTo() position already current:" + targetPosition); | |
return; | |
} | |
LOG.info("VASRollershutter moveTo() targetPosition:" + targetPosition + " from:" + this._currentPosition() + " isMoving:" + this.isMoving); | |
// Compute time of movement | |
this.position = this._currentPosition(); | |
var posOffset = targetPosition - this.position; | |
var newCmd = posOffset > 0 ? "DOWN" : "UP"; | |
var time = (Math.abs(posOffset) / 100) * (posOffset > 0 ? this.downtime : this.uptime); | |
LOG.debug("VASRollershutter " + this.name + " computed movement offset:" + posOffset + "/" + newCmd + "/" + time + "ms"); | |
// TODO, do not send command if already moving in the right direction | |
this._setStopTimer(time); | |
this.targetPosition = targetPosition; | |
this.isMoving = true; | |
this.direction = newCmd; | |
this.movingSince = ZonedDateTime.now(); | |
LOG.debug("VASRollershutter " + this.name + " sending command for movement:" + this.direction + ", timer set in " + time + "ms"); | |
this._itemCommand(this.direction); | |
if (this.updateTimer == null) { | |
this.updateTimer = setTimeout(function (rl) { | |
rl._updateTimeout(); | |
}, 1000, this); | |
} | |
} | |
// Registry | |
VASRollershutter.shutters = {}; | |
// Static methods | |
VASRollershutter.getShutter = function (name) { | |
LOG.info("VASRollershutter getShutter(" + name + ")"); | |
var shutter = VASRollershutter.shutters[name] | |
if (shutter == null) { | |
LOG.info("VASRollershutter get(" + name + ") -> null"); | |
} else { | |
LOG.info("VASRollershutter get(" + name + ") -> found one"); | |
} | |
return shutter; | |
} | |
// Private methods | |
VASRollershutter.prototype.stop = function () { | |
LOG.trace("VASRollershutter " + this.name + " stop()"); | |
this.position = this._currentPosition(); | |
if (this.stopTimer) { | |
this.stopTimer.cancel(); | |
this.stopTimer = null; | |
} | |
if (this.updateTimer) { | |
this.updateTimer.cancel(); | |
this.updateTimer = null; | |
} | |
this.isMoving = false; | |
this._updateState(this._currentPosition()); | |
this._itemCommand("STOP"); | |
} | |
// Send command to actual rollershutter item | |
VASRollershutter.prototype._itemCommand = function (command) { | |
LOG.debug("VASRollershutter " + this.name + " sending command '" + command + "' to " + this.realItemStr); | |
events.sendCommand(this.realItemStr, command); | |
} | |
// Creates timer according to target position | |
VASRollershutter.prototype._currentPosition = function () { | |
if (this.isMoving) { | |
LOG.trace("VASRollershutter " + this.name + " _currentPosition() while moving"); | |
var millis = this.movingSince.until(ZonedDateTime.now(), ChronoUnit.MILLIS); | |
var delta = 0; | |
if (this.direction == "UP") { | |
delta = - millis / this.uptime * 100; | |
} else { | |
delta = millis / this.downtime * 100; | |
} | |
return Math.max(0, Math.min(100, Math.round(this.position + delta))); | |
} else return this.position; | |
} | |
// Creates timer according to target position | |
VASRollershutter.prototype._setStopTimer = function (millis) { | |
if (this.stopTimer) { | |
this.stopTimer.cancel(); | |
this.stopTimer = null; | |
} | |
this.stopTimer = setTimeout(function (rl) { | |
rl._timeout(); | |
}, millis, this); | |
} | |
// Called upon timer expiration | |
VASRollershutter.prototype._timeout = function () { | |
if (this.targetPosition == 0 || this.targetPosition == 100) { | |
// Don't send stop command to re-sync position using the motor end stop | |
LOG.info("VASRollershutter " + this.name + " arrived at end position, not stopping for calibration"); | |
} else { | |
LOG.info("VASRollershutter " + this.name + " arrived at position, sending STOP command"); | |
this._itemCommand("STOP"); | |
} | |
this.stopTimer = null; | |
this.isMoving = false; | |
this.direction = null; | |
this.position = this.targetPosition; | |
this.targetPosition = -1; | |
this._updateState(this.position); | |
} | |
// Called upon update timer expiration | |
VASRollershutter.prototype._updateTimeout = function () { | |
if (this.isMoving) { | |
this._updateState(this._currentPosition()); | |
this.updateTimer = setTimeout(function (rl) { | |
rl._updateTimeout(); | |
}, 1000, this); | |
} else { | |
// Reset object | |
this.updateTimer = null; | |
} | |
} | |
// Updates the state of the item to current position | |
VASRollershutter.prototype._updateState = function (position) { | |
LOG.trace("VASRollershutter " + this.name + " _updateState(" + position + ")"); | |
events.postUpdate(this.itemStateStr, position); | |
} | |
// Rule execution callback | |
function VASRollershutterExecute (event) { | |
LOG.info("Rollershutter callback event: " + event + " (" + typeof(event) + ")"); | |
var name = event.getItemName().split("_RS_VAL")[0]; | |
LOG.debug("Rollershutter " + name); | |
if (name != null) { | |
VASRollershutter.getShutter(name).processCommand(event.getItemCommand()); | |
} else { | |
LOG.error("Rollershutter command rule invalid item name:" + event.getItemName()); | |
} | |
} | |
// Here VASRollershutter object are instantiated for each device | |
// Parameters are the items name prefix and the time for complete | |
// movement up and down (milliseconds) | |
new VASRollershutter("F0_Salon_Store_Sud", 22000, 21500); | |
new VASRollershutter("F0_Salon_Store_Nord", 14900, 14000); | |
new VASRollershutter("F0_Balcon_Store_Est", 23000, 21600); | |
new VASRollershutter("F0_Balcon_Store_Ouest", 23700, 22100); | |
// Registration of the rule | |
when("Item F0_Balcon_Store_Ouest_RS_VAL received command")(VASRollershutterExecute); | |
when("Item F0_Balcon_Store_Est_RS_VAL received command")(VASRollershutterExecute); | |
when("Item F0_Salon_Store_Nord_RS_VAL received command")(VASRollershutterExecute); | |
when("Item F0_Salon_Store_Sud_RS_VAL received command")(VASRollershutterExecute); | |
rule("VASRollerShutter", "Processes virtual roller shutter items commands")(VASRollershutterExecute); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment