const log = Java.type('org.slf4j.LoggerFactory').getLogger('js.utils.watch'); log.info('Load watch module'); class Watch { #watchObjects = new Object(); #watchItems = new Object(); #timers = new timer.Timer(); constructor() { log.info('Initialization of watch helper class'); } add(params) { // Generate watchUUID let watchUUID = utils.randomUUID(); // Validate config for watchObject if (!this.validateWatchConfig(params)) { console.warn(`Failed to add watch object because no valid config was provided`); return; } if (!this.#watchItems.hasOwnProperty(params['item'])) { this.#createWatchRule(params['item']); } else { log.debug(`Watch rule for item ${params['item']} already existing`) } // Set default values if no config provided let operator = (params['operator'] !== undefined) ? params['operator'] : '=='; // Create watch object and return UUID log.info(`Add watch object for item ${params['item']} with state ${params['targetState']} and operator ${operator} with UUID ${watchUUID}`); this.#watchObjects[watchUUID] = { item: params['item'], 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) { // Get itemName let watchItemName = this.#watchObjects[watchUUID].item; log.info(`Delete watch object for item ${watchItemName} with watchUUID ${watchUUID}`); // End repeatAlertTimer if existing this.#timers.cancel('repeatAlarm ' + watchUUID); // End startAlertTimer if existing this.#timers.cancel('startAlert ' + watchUUID); delete this.#watchObjects[watchUUID]; // Delete watch rule if no more watchObjects for respective item are present if (Object.keys(this.#watchObjects).length == 0) { log.debug(`Remove openHAB watch rule for item ${watchItemName}`); rules.removeRule(this.#watchItems[watchItemName]); } } deleteAll() { for (let watchUUID of Object.keys(this.#watchObjects)) { this.delete(watchUUID); } } isAlert(watchUUID) { log.debug('Check if there is a running alert for watchObject ' + watchUUID); if (watchUUID in this.#watchObjects) { return this.#watchObjects[watchUUID].alert; } } validateWatchConfig(params) { if (params['item'] === undefined) { log.error('No item set'); return false; } if (params['targetState'] === undefined) { log.error('No targetState set'); return false; } if (params['alertFunc'] === undefined) { log.error('No alertFunc set'); return false; } // return true if config is valid return true } #applyHysteresis(currentState, targetState, hysteresis) { log.info(`Applying hysteresis with: ${currentState}, ${targetState}, ${hysteresis}`); let delta = Math.abs(targetState - currentState); log.info(delta); if (delta < hysteresis) { return false; } return true; } #checkAlertState(watchUUID) { // Get itemName let watchItemName = this.#watchObjects[watchUUID].item; log.debug(`Check if item ${watchItemName} is in alert state for watchObject ${watchUUID}`) // Convert currentState for comparison let currentState = lib.convertValue(items[watchItemName].state); // Do comparison if (lib.compare(currentState, this.#watchObjects[watchUUID].targetState, this.#watchObjects[watchUUID].operator)) { // Comparison successful log.info(`State ${currentState} is ${this.#watchObjects[watchUUID].operator} ${this.#watchObjects[watchUUID].targetState} triggered by ${watchUUID} (${watchItemName})`); 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)) { log.info('CurrentState is still in boundaries provided by hysteresis value'); return; } // End alert this.#endAlert(watchUUID); } } #createWatchRule(item) { // Create openHAB rule log.info(`Create openHAB watch rule for item ${item}`); let watchRuleID = String(utils.randomUUID()); rules.JSRule({ id: watchRuleID, name: item + ' watch rule', triggers: [triggers.ItemStateChangeTrigger(item)], tags: ['Watch'], execute: (event) => { this.#processItemEvent(event) }, }); this.#watchItems[item] = watchRuleID; } #endAlert(watchUUID) { log.info(`End alert for watchObject ${watchUUID} triggered`); this.#watchObjects[watchUUID].alert = false; // Run end alert function if existing if (this.#watchObjects[watchUUID].endAlertFunc != '') { log.info(`Run end alert function for watchObject ${watchUUID}`); this.#watchObjects[watchUUID].endAlertFunc(); } // End repeatAlertTimer if existing this.#timers.cancel('repeatAlarm ' + watchUUID); // End startAlertTimer if startAlert started timer with delay this.#timers.cancel('startAlert ' + watchUUID); } #processItemEvent(event) { // Skip if function is triggered without openHAB event if (event.eventType == 'manual') { console.warn(`ProcessItemEvent triggered without openHAB event`); return; } log.info(`Processing state ${event.newState} for ${event.itemName}`); for (const [id, watchObject] of Object.entries(this.#watchObjects)) { if (watchObject.item == event.itemName) { this.#checkAlertState(id); } } } #rescheduleAlert(watchUUID) { log.info(`Subsequent alert for ${watchUUID}`); } #startAlert(watchUUID) { // Get itemName let watchItemName = this.#watchObjects[watchUUID].item; log.info(`Initial alert for watchObject ${watchUUID} (${watchItemName}) 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 == '') { log.info(`Run initial alert function for watchObject ${watchUUID} (${watchItemName})`); initialAlertFunc(); } else { log.info(`Shedule initial alert function for watchObject ${watchUUID} (${watchItemName}) with delay setting ${alertDelay}`); let alertDelayZDT = time.toZDT(alertDelay); alertDelayAbsolute = (alertDelayZDT.getMillisFromNow() / 1000); this.#timers.create('startAlert ' + watchUUID, alertDelayZDT, () => { log.info('Run initial alert function for watchObject ' + watchUUID); initialAlertFunc(); } ); } // Create timer to run repeat alert rule if required if (alertRepeat != '') { log.info(`Shedule repeat alert function for watchObject ${watchUUID} (${watchItemName}) with repeat setting ${alertRepeat}`); this.#timers.create('repeatAlarm ' + watchUUID, time.toZDT(alertRepeat).plusSeconds(alertDelayAbsolute), () => { log.info('Run repeat alert function for watchObject ' + watchUUID); this.#watchObjects[watchUUID].alertFunc(); this.#watchObjects[watchUUID].repeatAlertTimer.reschedule(time.toZDT(alertRepeat)); } ); } } } module.exports = { Watch, };