From 18baf76eecf2906c0f7b39a9743416489caf9c33 Mon Sep 17 00:00:00 2001 From: Christian Weimann Date: Fri, 10 Jan 2025 05:24:41 +0100 Subject: [PATCH] First commit --- SceneMgr.js | 262 ++++++++++++++++++++++++++++++++++++++++++++++ utils/TimerMgr.js | 73 +++++++++++++ utils/index.js | 7 ++ 3 files changed, 342 insertions(+) create mode 100644 SceneMgr.js create mode 100644 utils/TimerMgr.js create mode 100644 utils/index.js diff --git a/SceneMgr.js b/SceneMgr.js new file mode 100644 index 0000000..b5df56f --- /dev/null +++ b/SceneMgr.js @@ -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(); +}); diff --git a/utils/TimerMgr.js b/utils/TimerMgr.js new file mode 100644 index 0000000..e8b9a4e --- /dev/null +++ b/utils/TimerMgr.js @@ -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 +}; \ No newline at end of file diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..15df662 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,7 @@ +// Logging +console.loggerName = "org.openhab.js.utils"; +console.log("Loading utils"); + +module.exports = { + get TimerMgr() { return require('./TimerMgr.js').getTimerMgr; } +} \ No newline at end of file