Skip to content

Instantly share code, notes, and snippets.

@tarag
Last active April 30, 2021 07:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tarag/5131b1e7bf08c5cf1fed7f7be7876a67 to your computer and use it in GitHub Desktop.
Save tarag/5131b1e7bf08c5cf1fed7f7be7876a67 to your computer and use it in GitHub Desktop.
Position estimator for openHAB roller shutters using JSR223 JS rules (OH3)
/**
* 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);
// 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" }
'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