First commit
This commit is contained in:
262
SceneMgr.js
Normal file
262
SceneMgr.js
Normal file
@@ -0,0 +1,262 @@
|
||||
// Settings
|
||||
const ITEMPREFIX = "System_StatefulScene";
|
||||
const DEBOUNCETIME = 3000;
|
||||
const LOGGER = "org.openhab.js.SceneMgr";
|
||||
|
||||
// Logging
|
||||
const log = Java.type('org.slf4j.LoggerFactory').getLogger(LOGGER);
|
||||
|
||||
// Load dependencies
|
||||
const { TimerMgr } = require('../utils');
|
||||
const { ruleRegistry } = require('@runtime/RuleSupport');
|
||||
|
||||
const tm = new TimerMgr();
|
||||
|
||||
// Functions
|
||||
class SceneMgr {
|
||||
|
||||
constructor() {
|
||||
this.scenes = this.getScenes().reduce((accumulator, scene) => {
|
||||
const sceneUID = scene.getUID();
|
||||
accumulator[sceneUID] = new Scene(scene);
|
||||
return accumulator;
|
||||
}, {});
|
||||
}
|
||||
|
||||
isSceneEnabled(sceneUID) {
|
||||
try {
|
||||
return rules.isEnabled(sceneUID);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
purgeUnusedSceneSwitchItems() {
|
||||
log.debug("Purge unused scene switch items");
|
||||
|
||||
for (const sceneSwitchItem of items.getItemsByTag("Stateful")) {
|
||||
const ruleUID = sceneSwitchItem.name.split("_")[2];
|
||||
|
||||
if (!this.isSceneEnabled(ruleUID)) {
|
||||
log.debug("Remove unused scene switch item " + sceneSwitchItem.name);
|
||||
items.removeItem(sceneSwitchItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getScenes() {
|
||||
|
||||
const allRules = utils.javaSetToJsArray(ruleRegistry.getAll());
|
||||
|
||||
return allRules.filter(rule => {
|
||||
const ruleTags = utils.javaSetToJsArray(rule.getTags());
|
||||
const hasSceneTag = ruleTags.includes("Scene");
|
||||
const hasStatefulTag = ruleTags.includes("Stateful");
|
||||
const isEnabled = rules.isEnabled(rule.getUID());
|
||||
|
||||
return hasSceneTag && hasStatefulTag && isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Scene {
|
||||
|
||||
constructor(scene) {
|
||||
|
||||
this.scene = scene;
|
||||
this.sceneUID = this.scene.getUID();
|
||||
this.sceneName = this.scene.getName();
|
||||
this.sceneSwitchItemName = ITEMPREFIX + "_" + this.sceneUID;
|
||||
this.evaluationRuleUID = "SceneItems" + this.sceneUID;
|
||||
|
||||
if (!items[this.sceneSwitchItemName]) {
|
||||
createSceneSwitchItem();
|
||||
}
|
||||
|
||||
this.createSceneRules();
|
||||
}
|
||||
|
||||
createSceneSwitchItem() {
|
||||
log.info(`Creating scene switch for ${this.sceneSwitchItemName}`);
|
||||
items.addItem({
|
||||
name: this.sceneSwitchItemName,
|
||||
type: 'Switch',
|
||||
label: `Switch for stateful scene ${this.sceneName}`,
|
||||
tags: ['Stateful', 'Control']
|
||||
});
|
||||
}
|
||||
|
||||
createSceneRules() {
|
||||
|
||||
rules.JSRule({
|
||||
name: "Switch rule for stateful scene " + this.sceneName,
|
||||
description: "Scene switch for " + this.sceneName + " received command",
|
||||
triggers: [triggers.ItemCommandTrigger(this.sceneSwitchItemName)],
|
||||
execute: (event) => {
|
||||
this.switchScene(event.receivedCommand);
|
||||
},
|
||||
id: "StatefulSwitch" + this.sceneUID,
|
||||
overwrite: true
|
||||
});
|
||||
|
||||
rules.JSRule({
|
||||
name: "Evaluation rule for stateful scene " + this.sceneName,
|
||||
description: "Evaluate stateful scene " + this.sceneName + " if scene items have changed",
|
||||
triggers: this.getSceneItems().map(itemName => triggers.ItemStateChangeTrigger(itemName)),
|
||||
execute: () => {
|
||||
this.evaluateScene();
|
||||
},
|
||||
id: this.evaluationRuleUID,
|
||||
overwrite: true
|
||||
});
|
||||
this.disableEvaluationRule();
|
||||
|
||||
}
|
||||
|
||||
disableEvaluationRule() {
|
||||
// Log the action using template literals for cleaner string interpolation
|
||||
log.debug(`Disable evaluation rule for scene ${this.sceneName}`);
|
||||
|
||||
// Disable the evaluation rule by setting its enabled state to false
|
||||
rules.setEnabled(this.evaluationRuleUID, false);
|
||||
}
|
||||
|
||||
enableEvaluationRule() {
|
||||
// Log the action using template literals for better readability
|
||||
log.info(`Enable evaluation rule for scene ${this.sceneName}`);
|
||||
|
||||
// Enable the evaluation rule by setting its enabled state to true
|
||||
rules.setEnabled(this.evaluationRuleUID, true);
|
||||
}
|
||||
|
||||
evaluateScene() {
|
||||
|
||||
const sceneUID = this.scene.getUID();
|
||||
const sceneName = this.scene.getName();
|
||||
const timerUID = "EvaluateScene_" + sceneUID;
|
||||
const sceneSwitchItemName = ITEMPREFIX + "_" + sceneUID;
|
||||
|
||||
const evaluate = () => {
|
||||
log.info(`Evaluate scene ${sceneName}`);
|
||||
|
||||
const scenceConfig = this.getSceneConfig();
|
||||
let conditionsFailed = false;
|
||||
|
||||
for (const [itemName, targetState] of Object.entries(scenceConfig)) {
|
||||
|
||||
const itemType = items[itemName].type;
|
||||
const currentState = (itemType === "Dimmer" || itemType === "Number")
|
||||
? Math.round(items[itemName].numericState)
|
||||
: items[itemName].state;
|
||||
|
||||
if (currentState == targetState) {
|
||||
log.info("Compare " + itemName + " (type: " + itemType + ") with currenState " + currentState + " and targetState " + targetState + " successfull");
|
||||
} else {
|
||||
log.info("Compare " + itemName + " (type: " + itemType + ") with currenState " + currentState + " and targetState " + targetState + " failed");
|
||||
conditionsFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditionsFailed) {
|
||||
log.info(`Switch off sceneSwitch for scene ${sceneName} because conditions failed`);
|
||||
items[sceneSwitchItemName].postUpdate("OFF");
|
||||
this.disableEvaluationRule();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (items[sceneSwitchItemName].state === "OFF") {
|
||||
log.info(`Skip evaluation of scene ${sceneName} because scene is off`);
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = time.ZonedDateTime.now().plusNanos(DEBOUNCETIME * 1000000);
|
||||
if (tm.hasTimer(timerUID) && tm.isActive(timerUID)) {
|
||||
log.info(`Skip evaluation of scene ${sceneName} as evaluation function is already running. Reschedule timer`);
|
||||
tm.reschedule(timerUID, timeout);
|
||||
} else {
|
||||
log.info(`Create timer to evaluate scene ${sceneName}`);
|
||||
tm.create(timerUID, timeout, evaluate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getSceneItems() {
|
||||
// Retrieve scene configuration and return its keys as an array
|
||||
return Object.keys(this.getSceneConfig());
|
||||
}
|
||||
|
||||
getSceneConfig() {
|
||||
log.debug(`Get configuration for scene ${this.sceneName}`);
|
||||
|
||||
const sceneConfig = {};
|
||||
|
||||
for (const action of this.scene.getActions()) {
|
||||
const actionConfig = utils.javaMapToJsMap(action.getConfiguration());
|
||||
sceneConfig[actionConfig.get("itemName")] = actionConfig.get("command");
|
||||
}
|
||||
|
||||
return sceneConfig;
|
||||
}
|
||||
|
||||
hasRestoreEnabled() {
|
||||
|
||||
const ruleTags = utils.javaSetToJsArray(this.scene.getTags());
|
||||
const hasRestoreTag = ruleTags.includes("Restore");
|
||||
|
||||
return hasRestoreTag;
|
||||
|
||||
}
|
||||
|
||||
switchScene(switchCommand) {
|
||||
|
||||
log.info("Scene " + this.sceneName + " received command " + switchCommand);
|
||||
|
||||
switch(switchCommand) {
|
||||
case "ON":
|
||||
log.info("Activate scene " + this.sceneName);
|
||||
|
||||
if (this.hasRestoreEnabled()) {
|
||||
log.info("Restore is enabled")
|
||||
this.switchedOn = time.ZonedDateTime.now();
|
||||
}
|
||||
|
||||
rules.runRule(this.sceneUID);
|
||||
java.lang.Thread.sleep(2500);
|
||||
this.enableEvaluationRule();
|
||||
break;
|
||||
|
||||
case "OFF":
|
||||
log.info("Deactivate scene " + this.sceneName);
|
||||
|
||||
if (this.hasRestoreEnabled()) {
|
||||
log.info("Restore is enabled");
|
||||
|
||||
let timestamp = this.switchedOn.minusNanos(DEBOUNCETIME * 1000000)
|
||||
|
||||
for (const sceneItem of this.getSceneItems()) {
|
||||
let historicSceneItem = items[sceneItem].persistence.persistedState(timestamp);
|
||||
let historicSceneItemState = historicSceneItem.state
|
||||
items[sceneItem].sendCommandIfDifferent(historicSceneItemState);
|
||||
}
|
||||
}
|
||||
|
||||
this.disableEvaluationRule();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load script
|
||||
const sm = new SceneMgr();
|
||||
|
||||
// Unload script
|
||||
require('@runtime').lifecycleTracker.addDisposeHook(() => {
|
||||
log.info('Deinitialization of SceneMgr.js');
|
||||
|
||||
sm.purgeUnusedSceneSwitchItems();
|
||||
|
||||
tm.cancelAll();
|
||||
});
|
||||
73
utils/TimerMgr.js
Normal file
73
utils/TimerMgr.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// Logging
|
||||
console.loggerName = "org.openhab.js.utils.TimerMgr";
|
||||
console.log("Loading utils.TimerMgr");
|
||||
|
||||
class TimerMgr {
|
||||
|
||||
#timers = new Object();
|
||||
|
||||
constructor() {
|
||||
console.info('Initialization of TimerMgr helper class');
|
||||
}
|
||||
|
||||
create(id, timeout, func) {
|
||||
console.log("Create timer with id " + id);
|
||||
this.#timers[id] = actions.ScriptExecution.createTimer(id, timeout, func);
|
||||
}
|
||||
|
||||
cancel(identifier) {
|
||||
// Return if no timer with the respactive identifier is available
|
||||
if (!this.#timers.hasOwnProperty(identifier)) {
|
||||
console.log(`No timer with identifier ${identifier} available to cancel`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if timer is active
|
||||
if (!this.#timers[identifier].isActive()) {
|
||||
console.log(`Timer with identifier ${identifier} not running. Cancel anyway`);
|
||||
} else {
|
||||
console.log(`Cancel timer with identifier ${identifier}`);
|
||||
}
|
||||
|
||||
// Cancel timer
|
||||
this.#timers[identifier].cancel();
|
||||
delete this.#timers[identifier];
|
||||
}
|
||||
|
||||
hasTimer(id) {
|
||||
return this.#timers.hasOwnProperty(id);
|
||||
}
|
||||
|
||||
isActive(identifier) {
|
||||
return this.#timers[identifier].isActive();
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
// Fetch timers
|
||||
let timers = Object.keys(this.#timers);
|
||||
|
||||
// Return if no timers available
|
||||
if (timers.length == 0) {
|
||||
console.log('No timers available to cancel');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel all timers
|
||||
for (let timer of timers) {
|
||||
this.cancel(timer);
|
||||
}
|
||||
}
|
||||
|
||||
reschedule(id, timeout) {
|
||||
this.#timers[id].reschedule(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function getTimerMgr () {
|
||||
return new TimerMgr();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TimerMgr,
|
||||
getTimerMgr
|
||||
};
|
||||
7
utils/index.js
Normal file
7
utils/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Logging
|
||||
console.loggerName = "org.openhab.js.utils";
|
||||
console.log("Loading utils");
|
||||
|
||||
module.exports = {
|
||||
get TimerMgr() { return require('./TimerMgr.js').getTimerMgr; }
|
||||
}
|
||||
Reference in New Issue
Block a user