console.loggerName = 'js.watch'; console.log('Load watch module'); class Watch { #watchItem #watchObjects = new Object(); constructor(watchItem) { // Fetch item if provided itemName is a string if (typeof watchItem === 'string' || watchItem instanceof String) { this.#watchItem = items[watchItem]; } else { this.#watchItem = watchItem; } // Throw error if item is not existing if (this.#watchItem === null) { throw(`Item ${watchItem} not existing`); } console.log(`Create new Watch instance for ${this.#watchItem.name}`); // 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.#watchItem.name} 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.#watchItem.name} with state ${params['targetState']} and operator ${operator} with UUID ${watchUUID}`); this.#watchObjects[watchUUID] = { targetState: 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) ? lib.convertValue(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.#watchItem.name} 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]; // Delete watchRule if no more watchObjects are present if (Object.keys(this.#watchObjects).length == 0) { console.debug(`Remove openHAB watch rule for item ${this.#watchItem.name}`); rules.removeRule(this.watchRuleID); } } 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) { console.debug(`Check if item is in alert state for watchObject ${watchUUID}`) // Convert currentState for comparison let currentState = lib.convertValue(this.#watchItem.state); // Do comparison if (lib.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); } } #createWatchRule() { // Create openHAB rule console.log(`Create openHAB watch rule for item ${this.#watchItem.name}`); this.watchRuleID = String(utils.randomUUID()); rules.JSRule({ id: this.watchRuleID, name: 'Watch rule for ' + this.#watchItem.name, triggers: [triggers.ItemStateChangeTrigger(this.#watchItem.name)], execute: (event) => { this.#processItemEvent(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].hasOwnProperty('startAlertTimer') && this.#watchObjects[watchUUID].startAlertTimer.isActive()) { console.log('Cancel startAlertTimer'); this.#watchObjects[watchUUID].startAlertTimer.cancel(); } } #processItemEvent(event) { // Skip if function is triggered without openHAB event if (event === undefined || event.eventType === undefined) { console.warn(`ProcessItemEvent for ${this.#watchItem.name} triggered without openHAB event`); return; } console.log(`Processing state ${this.#watchItem.state} for ${this.#watchItem.name}`); // 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; } let alertDelay = this.#watchObjects[watchUUID].alertDelay; let alertRepeat = this.#watchObjects[watchUUID].alertRepeat; let alertDelayAbsolute = 0; // Set alert state this.#watchObjects[watchUUID].alert = true; // Execute initial alert function or create timer to run initial alert rule if required if (alertDelay == '') { console.log(`Run initial alert function for watchObject ${watchUUID}`); initialAlertFunc(); } else { console.log(`Shedule initial alert function for watchObject ${watchUUID} with delay setting ${alertDelay}`); let alertDelayZDT = time.toZDT(alertDelay); alertDelayAbsolute = (alertDelayZDT.getMillisFromNow() / 1000); this.#watchObjects[watchUUID].startAlertTimer = actions.ScriptExecution.createTimer('startAlert ' + watchUUID, alertDelayZDT, () => { console.log('Run initial alert function for watchObject ' + watchUUID); initialAlertFunc(); } ); } // Create timer to run repeat alert rule if required if (alertRepeat != '') { console.log(`Shedule repeat alert function for watchObject ${watchUUID} with repeat setting ${alertRepeat}`); this.#watchObjects[watchUUID].repeatAlertTimer = actions.ScriptExecution.createTimer('repeatAlarm ' + watchUUID, time.toZDT(alertRepeat).plusSeconds(alertDelayAbsolute), () => { console.log('Run repeat alert function for watchObject ' + watchUUID); this.#watchObjects[watchUUID].alertFunc(); this.#watchObjects[watchUUID].repeatAlertTimer.reschedule(time.toZDT(alertRepeat)); } ); } } } module.exports = { Watch, };