// 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]) { this.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', 'Scene'] }); } 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(); });