263 lines
8.7 KiB
JavaScript
263 lines
8.7 KiB
JavaScript
// 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
|
|
}; |