console.loggerName = 'js.watch'; console.log('Load watch module'); class Watch { #item #watchItemName #watchObjects = new Object(); constructor(itemName) { console.log('Create new Watch instance for ' + itemName); // Check if item to watch is existing this.#item = items.getItem(itemName, true); if (this.#item == null) { throw('Item ' + itemName + ' not existing'); } this.#watchItemName = itemName; // Create watch rule this.#createWatchRule(); } add(params) { // Generate watchUUID let watchUUID = utils.randomUUID(); // Validate config for watchObject if (!this.validateWatchConfig(params)) { console.warn('Failed to add watch object for ' + this.#watchItemName + ' because no valid config was provided'); return; } // Set default values if no config provided let operator = (params['operator'] !== undefined) ? params['operator'] : '=='; // Create watch object and return UUID console.log('Add watch object for item ' + this.#watchItemName + ' with state ' + params['targetState'] + ' and operator ' + operator + ' with UUID ' + watchUUID); this.#watchObjects[watchUUID] = { targetState: this.#stateToValue(params['targetState']), operator: operator, alertFunc: params['alertFunc'], alertDelay: (params['alertDelay'] !== undefined) ? params['alertDelay'] : 0, alertRepeat: (params['alertRepeat'] !== undefined) ? params['alertRepeat'] : 0, initialAlertFunc: (params['initialAlertFunc'] !== undefined) ? params['initialAlertFunc'] : '', endAlertFunc: (params['endAlertFunc'] !== undefined) ? params['endAlertFunc'] : '', hysteresis: (params['hysteresis'] !== undefined) ? this.#stateToValue(params['hysteresis']) : '', alert: false } // Check if watchObject is already triggered by currentState this.#checkAlertState(watchUUID); // return id of watchObject return watchUUID; } delete(watchUUID) { console.log('Delete watch object for item ' + this.#watchItemName + ' with watchUUID ' + watchUUID); // End repeatAlertTimer if existing if (this.#watchObjects[watchUUID].hasOwnProperty('repeatAlertTimer') && this.#watchObjects[watchUUID].repeatAlertTimer.isActive()) { console.log('Cancel repeatAlertTimer for ' + watchUUID); this.#watchObjects[watchUUID].repeatAlertTimer.cancel(); } // End startAlertTimer if existing if (this.#watchObjects[watchUUID].hasOwnProperty('startAlertTimer') && this.#watchObjects[watchUUID].startAlertTimer.isActive()) { console.log('Cancel startAlertTimer for ' + watchUUID); this.#watchObjects[watchUUID].startAlertTimer.cancel(); } delete this.#watchObjects[watchUUID]; } deleteAll() { for (let watchUUID of Object.keys(this.#watchObjects)) { this.delete(watchUUID); } } validateWatchConfig(params) { if (params['targetState'] === undefined) { console.error('No targetState set'); return false; } if (params['alertFunc'] === undefined) { console.error('No alertFunc set'); return false; } // return true if config is valid return true } #applyHysteresis(currentState, targetState, hysteresis) { console.log('Applying hysteresis with: ' + currentState + ', ' + targetState + ', ' + hysteresis); let delta = Math.abs(targetState - currentState); console.log(delta); if (delta < hysteresis) { return false; } return true; } #checkAlertState(watchUUID) { let currentState = this.#stateToValue(this.#item.state); // Do comparison if (this.#compare(currentState, this.#watchObjects[watchUUID].targetState, this.#watchObjects[watchUUID].operator)) { // Comparison successful console.log('State ' + currentState + ' is ' + this.#watchObjects[watchUUID].operator + ' ' + this.#watchObjects[watchUUID].targetState + ' triggered by ' + watchUUID); if (this.#watchObjects[watchUUID].alert == true) { // Comparison successful and alert is already active this.#rescheduleAlert(watchUUID); } else { // Comparison successful and alert is not active // Start alert this.#startAlert(watchUUID); } } else if (this.#watchObjects[watchUUID].alert == true) { // Comparison failed but alert is active // Skip if currentState fails hysteresis test if (this.#watchObjects[watchUUID].hysteresis != '' && !this.#applyHysteresis(currentState, this.#watchObjects[watchUUID].targetState, this.#watchObjects[watchUUID].hysteresis)) { console.log('CurrentState is still in boundaries provided by hysteresis value'); return; } // End alert this.#endAlert(watchUUID); } } #compare(a, b, operator) { // Hint: a = currentState, b = targetState switch (operator) { case '==': return (a == b); break; case '!=': return !(a == b); break; case '>': return (a > b); break; case '<': return (a < b); break; } } #createWatchRule() { // Create openHAB rule console.log('Create openHAB watch rule for item ' + this.#watchItemName); let ruleID = rules.JSRule({ name: 'Watch rule for ' + this.#watchItemName, triggers: [triggers.ItemStateChangeTrigger(this.#watchItemName)], execute: (event) => { this.#processStateChange(event) }, }); } #endAlert(watchUUID) { console.log('End alert for watchObject ' + watchUUID + ' triggered'); this.#watchObjects[watchUUID].alert = false; // Run end alert function if existing if (this.#watchObjects[watchUUID]. endAlertFunc != '') { console.log('Run end alert function for watchObject ' + watchUUID); this.#watchObjects[watchUUID]. endAlertFunc(); } // End repeatAlertTimer if existing if (this.#watchObjects[watchUUID].hasOwnProperty('repeatAlertTimer') && this.#watchObjects[watchUUID].repeatAlertTimer.isActive()) { console.log('Cancel repeatAlertTimer'); this.#watchObjects[watchUUID].repeatAlertTimer.cancel(); } // End startAlertTimer if startAlert started timer with delay if (this.#watchObjects[watchUUID].startAlertTimer.isActive()) { console.log('Cancel startAlertTimer'); this.#watchObjects[watchUUID].startAlertTimer.cancel(); } } #processStateChange(event) { // Skip if function is triggered without openHAB event if (event === undefined || event.eventType === undefined) { console.warn('ProcessStateChange triggered without openHAB event'); return; } console.log('Processing state ' + this.#item.state + ' for ' + this.#watchItemName); // Iterate through watchObjetcs // todo: rework to only fetch UUID for (let [watchUUID, watchObject] of Object.entries(this.#watchObjects)) { this.#checkAlertState(watchUUID); } } #rescheduleAlert(watchUUID) { console.log('Subsequent alert for ' + watchUUID); } #startAlert(watchUUID) { console.log('Initial alert for watchObject ' + watchUUID + ' triggered'); // Set alertFunc as inital alert function if no initialAlertFunc is set let initialAlertFunc = this.#watchObjects[watchUUID].alertFunc; if (this.#watchObjects[watchUUID].initialAlertFunc != '') { initialAlertFunc = this.#watchObjects[watchUUID].initialAlertFunc; } // Create timer to run initial alert rule this.#watchObjects[watchUUID].alert = true; this.#watchObjects[watchUUID].startAlertTimer = actions.ScriptExecution.createTimer('startAlert ' + watchUUID, time.toZDT(this.#watchObjects[watchUUID].alertDelay), () => { console.log('Run initial alert function for watchObject ' + watchUUID); initialAlertFunc(); } ); // Create timer to run repeat alert rule if required if (this.#watchObjects[watchUUID].alertRepeat != '') { console.log('wiederholter alert notwendig'); this.#watchObjects[watchUUID].repeatAlertTimer = actions.ScriptExecution.createTimer('repeatAlarm ' + watchUUID, time.toZDT(this.#watchObjects[watchUUID].alertDelay + this.#watchObjects[watchUUID].alertRepeat), () => { console.log('Run repeat alert function for watchObject ' + watchUUID); this.#watchObjects[watchUUID].alertFunc(); this.#watchObjects[watchUUID].repeatAlertTimer.reschedule(time.ZonedDateTime.now().plusSeconds(this.#watchObjects[watchUUID].alertRepeat)); } ); } } #stateToValue(state) { console.log('Converting value ' + state + ' of type ' + typeof state); if(typeof state === 'string') { if(state.includes(' ')) { try { console.log('Quantity: ' + state); return Quantity(state) } catch(e) { // do nothing, leave it as a String console.log('Not a Quantity but has a space, leaving as a string: ' + state); return state; } } else if(!isNaN(state)) { console.log('Number: ' + state) return Number.parseFloat(state); } else if(state == 'UnDefType' || state == 'NULL' || state == 'UNDEF') { console.log('UnDefType: ' + state); return 'UnDefType'; } console.log('String: ' + state); return state; } else if(state instanceof DecimalType || state instanceof PercentType) { console.log('DecimalType or PercentType: ' + state); return state.floatValue(); } else if(state instanceof QuantityType) { console.log('QuantityType: ' + state); return Quantity(state); } else { console.log('String: ' + state); return state.toString(); } } } module.exports = { Watch, };