// Settings const ITEMPREFIX = "System_StatefulScene"; const DEBOUNCETIME = 3000; // Logging const log = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.js.utils.SceneMgr'); console.loggerName = 'org.openhab.js.utils.SceneMgr'; // Load dependencies const { TimerMgr } = require('../utils'); const tm = new TimerMgr(); const { ruleRegistry } = require('@runtime/RuleSupport'); // Functions class SceneMgr { constructor(itemPrefix, debounceTime) { // Load class properties this.itemPrefix = itemPrefix; this.debounceTime = debounceTime; // Log the initialization message for SceneMgr console.log('Initialization of SceneMgr'); // Initialize the scenes map by reducing the array of scenes into an object with unique IDs as keys. this.scenes = this.#getStatefulScenes().reduce((scenes, scene) => { const sceneUID = scene.getUID(); scenes[sceneUID] = new Scene(scene); return scenes; }, {}); } 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); } } } #getStatefulScenes() { // Fetch all rules from ruleRegistry const allRules = utils.javaSetToJsArray(ruleRegistry.getAll()); // Filter rules relating to stateful scenes depending on the respective tags 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; // Todo: refactoring enabled }); } } class Scene { #isActive = false; constructor(scene) { // Load class properties this.scene = scene; this.sceneUID = this.scene.getUID(); this.sceneName = this.scene.getName(); this.sceneSwitchItemName = ITEMPREFIX + "_" + this.sceneUID; this.evaluationRuleUID = "SceneItems" + this.sceneUID; // Log the initialization of the scene console.log(`Initialize scene ${this.sceneName}`); // Create switchItem for scene if it does not exits if (!items[this.sceneSwitchItemName]) { this.createSceneSwitchItem(); } // Create required scene rules for the scene this.createSceneRules(); } createSceneSwitchItem() { console.log(`${this.sceneName}: Create scene switch with item name ${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 }); } evaluateScene() { const sceneUID = this.scene.getUID(); const timerUID = "EvaluateScene_" + sceneUID; if (!this.#isActive) { console.debug(`Scene ${this.sceneName}: Skip evaluation because scene is not active`); return; } const evaluate = () => { log.info(`Evaluate scene ${this.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 ${this.sceneName} because conditions failed`); items[this.sceneSwitchItemName].postUpdate("OFF"); this.#isActive = false; } }; const timeout = time.ZonedDateTime.now().plusNanos(DEBOUNCETIME * 1000000); if (tm.hasTimer(timerUID) && tm.isActive(timerUID)) { log.info(`Skip evaluation of scene ${this.sceneName} as evaluation function is already running. Reschedule timer`); tm.reschedule(timerUID, timeout); } else { log.info(`Create timer to evaluate scene ${this.sceneName}`); if (tm.hasTimer(timerUID)) { tm.cancel(timerUID); } 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); this.#isActive = true; break; case "OFF": console.debug(`Scene ${this.sceneName}: Deactivate scene`); if (this.hasRestoreEnabled()) { log.debug(`Scene ${this.sceneName}: Restore previous values before scene was activated`); let timestamp = this.switchedOn.minusNanos(DEBOUNCETIME * 1000000) for (const sceneItemName of this.getSceneItems()) { let historicSceneItem = items[sceneItemName].persistence.persistedState(timestamp); if (historicSceneItem !== null) { items[sceneItemName].sendCommandIfDifferent(historicSceneItem.state); } else { console.warn(`Scene ${this.sceneName}: No previous value found for ${sceneItemName}`); } } } this.#isActive = false; break; } } } function getSceneMgr() { return new SceneMgr(); } module.exports = { getSceneMgr };