diff --git a/equipmentMgr.js b/equipmentMgr.js new file mode 100644 index 0000000..8a70be2 --- /dev/null +++ b/equipmentMgr.js @@ -0,0 +1,34 @@ +console.loggerName = 'js.equipmentMgr'; +console.log('Load equipmentMgr'); + +const { + equipment, + watch +} = require('../utils'); + + +const eMgr = new Object(); + +for (let equipmentItem of items.getItems().filter(element => { return element.semantics.isEquipment })) { +//for (let equipmentItem of items.getItems().filter(element => { return (element.semantics.isEquipment == true) && (element.semantics.equipment == null) })) { + // Fetch equipment type + let equipmentType = equipmentItem.tags.filter(element => Object.keys(equipment).includes(element)); + if (equipmentType.length > 1) { + equipmentType.splice(equipmentType.indexOf('Equipment'), 1); + } + + // Initialize equipment class + eMgr[equipmentItem.name] = new equipment[equipmentType](equipmentItem); +} + +cache.shared.put('eMgr', eMgr); + +require('@runtime').lifecycleTracker.addDisposeHook(() => { + console.log('Deinitialization of equipmentMgr'); + + for (let equipmentItemName of Object.keys(eMgr)) { + eMgr[equipmentItemName].gc(); + } +}); + + diff --git a/utils/equipment.js b/utils/equipment.js new file mode 100644 index 0000000..ac685a7 --- /dev/null +++ b/utils/equipment.js @@ -0,0 +1,67 @@ +console.loggerName = 'js.equipment'; +console.log('Load equipment modules'); + +class Equipment { + constructor(equipmentItem) { + console.info('Initialization of eqipment ' + equipmentItem.name + ' with type ' + this.constructor.name); + + // Set equipmentItem + this.equipmentItem = equipmentItem; + + // Set stateItem, toDo: error when no stateItem existing + this.stateItem = items.getItem(this.equipmentItem.name + '_State'); + + // Initialization of properties + this.name = this.equipmentItem.name; + this.watch = new Object(); + } + + gc() { + console.log('Denitialization of eqipment ' + this.name); + + // Delete all watchObjects + for (let watchItem of Object.keys(this.watch)) { + this.watch[watchItem].deleteAll(); + } + } +} + +class Irrigation extends Equipment { + constructor(equipmentItem) { + super(equipmentItem); + } +} + +class IrrigationValve extends Equipment { + constructor(equipmentItem) { + super(equipmentItem); + + this.watch['state'] = new watch.Watch(this.stateItem.name); + this.autoOff = this.watch['state'].add({ + targetState: 'ON', + alertFunc: () => { this.stateItem.sendCommand('OFF'); }, + alertDelay: 'PT1M' + }); + } +} + + +class TowelRadiator extends Equipment { + constructor(equipmentItem) { + super(equipmentItem); + + this.watch['state'] = new watch.Watch(this.stateItem.name); + this.autoOff = this.watch['state'].add({ + targetState: 'ON', + alertFunc: () => { this.stateItem.sendCommand('OFF'); }, + alertDelay: 'PT30M' + }); + } +} + +module.exports = { + Equipment, + Irrigation, + IrrigationValve, + TowelRadiator +}; diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..5eaab4b --- /dev/null +++ b/utils/index.js @@ -0,0 +1,7 @@ +console.loggerName = 'js.utils'; +console.log('Load utils') + +module.exports = { + get equipment() { return require('./equipment') }, + get watch() { return require('./watch') }, +} \ No newline at end of file diff --git a/utils/watch.js b/utils/watch.js new file mode 100644 index 0000000..809f64f --- /dev/null +++ b/utils/watch.js @@ -0,0 +1,288 @@ +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, +}; + +/* +let test = new Watch('GB_TowelRadiator_State'); +test.add({ + targetState: 'OFF', + alertFunc: () => { console.log('Alle Achtung!'); }, +}); +*/ \ No newline at end of file