Compare commits

...

20 Commits

Author SHA1 Message Date
Christian Weimann
fd69b4de1b refactor(helpers): rename distanceBetween to distanceFrom 2025-02-15 08:50:28 +01:00
Christian Weimann
d2b9359fee feat(helpers): add debounceFunction and distanceBetween 2025-02-09 08:02:41 +01:00
Christian Weimann
b5eb470609 feat(utils): add SceneMgr and helpers to exports 2025-02-09 08:01:54 +01:00
Christian Weimann
475d238ea3 chore(git): add .gitignore file to exclude node_modules and package files
This change introduces a .gitignore file to prevent node_modules, package.json, and package-lock.json from being tracked by Git. This helps keep the repository clean by excluding directories and files that are generated or unnecessary for version control.
2025-02-09 04:38:17 +01:00
Christian Weimann
c5ed9c9c44 refactor(utils): rename and update SceneController to SceneMgr 2025-01-18 04:25:42 +01:00
Christian Weimann
13bbf41d0b refactor(SceneController): rename SceneMgr to SceneController 2025-01-14 03:54:21 +01:00
Christian Weimann
de8e708088 refactor(SceneMgr): inline scene initialization method 2025-01-14 03:50:51 +01:00
Christian Weimann
fa635f46d9 fix(TimerMgr): cancel existing timer before creating a new one 2025-01-13 08:59:18 +01:00
Christian Weimann
4a7185a1a5 feat(timers): allow passing additional parameters to timers 2025-01-12 19:19:57 +01:00
Christian Weimann
b9e5c22184 fix(timing): prevent duplicate timer creation by canceling existing timer 2025-01-12 12:42:36 +01:00
Christian Weimann
47a862780d style(SceneMgr): reorder import statements for consistency 2025-01-12 12:18:24 +01:00
Christian Weimann
fd10e13238 refactor(scene): extract scenes initialization into a separate method 2025-01-12 07:09:31 +01:00
Christian Weimann
6d7d00fcd3 refactor(scene): rename accumulator variable in scenes reducer 2025-01-12 07:07:33 +01:00
Christian Weimann
2571f9c01e chore(logging): add informational logs for SceneMgr and Scene initialization 2025-01-12 06:58:43 +01:00
Christian Weimann
0c5566a7eb fix(scene): correct method call to createSceneSwitchItem 2025-01-11 10:05:45 +01:00
Christian Weimann
27446b13b8 feat(scene-manager): add 'Scene' tag to stateful scene switch items 2025-01-11 10:00:19 +01:00
Christian Weimann
47d6b80511 refactor(TimerMgr): streamline timer identification and logging 2025-01-10 06:08:04 +01:00
Christian Weimann
33e5781ee8 fix(TimerMgr): add warning for rescheduling non-existent timers 2025-01-10 06:05:20 +01:00
Christian Weimann
e299b96d15 fix(timer): handle missing timer in isActive method 2025-01-10 06:03:23 +01:00
Christian Weimann
7852cda9e6 fix(timers): prevent duplicate timer creation 2025-01-10 06:00:17 +01:00
5 changed files with 147 additions and 79 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
package.json
package-lock.json

View File

@@ -1,25 +1,33 @@
// Settings
const ITEMPREFIX = "System_StatefulScene";
const DEBOUNCETIME = 3000;
const LOGGER = "org.openhab.js.SceneMgr";
// Logging
const log = Java.type('org.slf4j.LoggerFactory').getLogger(LOGGER);
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 { ruleRegistry } = require('@runtime/RuleSupport');
const tm = new TimerMgr();
const { ruleRegistry } = require('@runtime/RuleSupport');
// Functions
class SceneMgr {
constructor() {
this.scenes = this.getScenes().reduce((accumulator, scene) => {
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();
accumulator[sceneUID] = new Scene(scene);
return accumulator;
scenes[sceneUID] = new Scene(scene);
return scenes;
}, {});
}
@@ -44,17 +52,19 @@ class SceneMgr {
}
}
getScenes() {
#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;
return hasSceneTag && hasStatefulTag && isEnabled; // Todo: refactoring enabled
});
}
@@ -62,28 +72,35 @@ class SceneMgr {
class Scene {
constructor(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]) {
createSceneSwitchItem();
this.createSceneSwitchItem();
}
// Create required scene rules for the scene
this.createSceneRules();
}
createSceneSwitchItem() {
log.info(`Creating scene switch for ${this.sceneSwitchItemName}`);
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']
tags: ['Stateful', 'Control', 'Scene']
});
}
@@ -110,35 +127,21 @@ class Scene {
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;
if (!this.#isActive) {
console.debug(`Scene ${this.sceneName}: Skip evaluation because scene is not active`);
return;
}
const evaluate = () => {
log.info(`Evaluate scene ${sceneName}`);
log.info(`Evaluate scene ${this.sceneName}`);
const scenceConfig = this.getSceneConfig();
let conditionsFailed = false;
@@ -160,24 +163,22 @@ class Scene {
}
if (conditionsFailed) {
log.info(`Switch off sceneSwitch for scene ${sceneName} because conditions failed`);
items[sceneSwitchItemName].postUpdate("OFF");
this.disableEvaluationRule();
log.info(`Switch off sceneSwitch for scene ${this.sceneName} because conditions failed`);
items[this.sceneSwitchItemName].postUpdate("OFF");
this.#isActive = false;
}
};
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`);
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 ${sceneName}`);
log.info(`Create timer to evaluate scene ${this.sceneName}`);
if (tm.hasTimer(timerUID)) {
tm.cancel(timerUID);
}
tm.create(timerUID, timeout, evaluate);
}
@@ -224,39 +225,39 @@ class Scene {
}
rules.runRule(this.sceneUID);
java.lang.Thread.sleep(2500);
this.enableEvaluationRule();
this.#isActive = true;
break;
case "OFF":
log.info("Deactivate scene " + this.sceneName);
console.debug(`Scene ${this.sceneName}: Deactivate scene`);
if (this.hasRestoreEnabled()) {
log.info("Restore is enabled");
log.debug(`Scene ${this.sceneName}: Restore previous values before scene was activated`);
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);
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.disableEvaluationRule();
this.#isActive = false;
break;
}
}
}
// Load script
const sm = new SceneMgr();
function getSceneMgr() {
return new SceneMgr();
}
// Unload script
require('@runtime').lifecycleTracker.addDisposeHook(() => {
log.info('Deinitialization of SceneMgr.js');
sm.purgeUnusedSceneSwitchItems();
tm.cancelAll();
});
module.exports = {
getSceneMgr
};

View File

@@ -9,36 +9,43 @@ class TimerMgr {
log.info('Initialization of TimerMgr helper class');
}
create(id, timeout, func) {
create(id, timeout, func, ...params) {
if (this.hasTimer(id)) {
this.cancel(id);
}
log.debug("Create timer with id " + id);
this.#timers[id] = actions.ScriptExecution.createTimer(id, timeout, func);
this.#timers[id] = actions.ScriptExecution.createTimer(id, timeout, func, ...params);
}
cancel(identifier) {
// Return if no timer with the respactive identifier is available
if (!this.#timers.hasOwnProperty(identifier)) {
log.debug(`No timer with identifier ${identifier} available to cancel`);
cancel(id) {
// Return if no timer with the respactive id is available
if (!this.hasTimer(id)) {
log.warn(`No timer with id ${id} available to cancel`);
return false;
}
// Check if timer is active
if (!this.#timers[identifier].isActive()) {
log.debug(`Timer with identifier ${identifier} not running. Cancel anyway`);
if (!this.#timers[id].isActive()) {
log.debug(`Timer with id ${id} not running. Cancel anyway`);
} else {
log.debug(`Cancel timer with identifier ${identifier}`);
log.debug(`Cancel timer with identifier ${id}`);
}
// Cancel timer
this.#timers[identifier].cancel();
delete this.#timers[identifier];
this.#timers[id].cancel();
delete this.#timers[id];
}
hasTimer(id) {
return this.#timers.hasOwnProperty(id);
}
isActive(identifier) {
return this.#timers[identifier].isActive();
isActive(id) {
if (!this.hasTimer(id)) {
log.warn(`No timer with id ${id} found`);
return false;
}
return this.#timers[id].isActive();
}
cancelAll() {
@@ -58,6 +65,10 @@ class TimerMgr {
}
reschedule(id, timeout) {
if (!this.hasTimer(id)) {
log.warn(`Cannot reschedule non-existent timer with id ${id}`);
return false;
}
this.#timers[id].reschedule(timeout);
}
}

51
utils/helpers.js Normal file
View File

@@ -0,0 +1,51 @@
// Load dependencies
const { TimerMgr } = require('../utils');
const tm = new TimerMgr();
function debounceFunction(id, debounceTimeout, func, params) {
console.warn('debounce', id, debounceTimeout);
timeout = time.ZonedDateTime.now().plusNanos(debounceTimeout * 1000000);
if (tm.hasTimer(id) && tm.isActive(id)) {
log.info(`Skip evaluation of function with id ${id} as function is already running. Reschedule timer`);
tm.reschedule(id, timeout);
} else {
log.info(`Create timer to execute function ${id}`);
if (tm.hasTimer(id)) {
tm.cancel(id);
}
tm.create(id, timeout, func, params);
}
}
function distanceFrom(locationA, locationB) {
const [lat1, lon1] = locationA.split(',').map(Number);
const [lat2, lon2] = locationB.split(',').map(Number);
const toRadians = (degree) => degree * (Math.PI / 180);
const radLat1 = toRadians(lat1);
const radLon1 = toRadians(lon1);
const radLat2 = toRadians(lat2);
const radLon2 = toRadians(lon2);
if (lat1 === lat2 && lon1 === lon2) return '0 m';
const dlon = radLon2 - radLon1;
const dlat = radLat2 - radLat1;
const a = Math.pow(Math.sin(dlat / 2), 2)
+ Math.cos(radLat1) * Math.cos(radLat2)
* Math.pow(Math.sin(dlon / 2), 2);
const c = 2 * Math.asin(Math.sqrt(a));
const r = 6371000; // Radius of earth in meters
return `${(c * r).toFixed()} m`;
}
module.exports = {
debounceFunction,
distanceFrom
};

View File

@@ -3,5 +3,7 @@ console.loggerName = "org.openhab.js.utils";
console.log("Loading utils");
module.exports = {
get TimerMgr() { return require('./TimerMgr.js').getTimerMgr; }
get TimerMgr() { return require('./TimerMgr.js').getTimerMgr; },
get SceneMgr() { return require('./SceneMgr.js').getSceneMgr; },
get helpers() { return require('./helpers.js') }
}