I tried it 5 more times, always with the same result…
For the full picture, this is my alteration to the script (one never knows whether that has any influence):
// Version 1.2
var {helpers, LoopingTimer, Gatekeeper, timeUtils, RateLimit} = require('openhab_rules_tools');
var {DecimalType, QuantityType, PercentType} = require('@runtime');
var loggerBase = 'org.openhab.automation.rules_tools.Threshold Alert.'+ruleUID;
console.loggerName = loggerBase;
osgi.getService('org.apache.karaf.log.core.LogService').setLevel(console.loggerName, 'DEBUG');
helpers.validateLibraries('4.1.0', '2.0.3');
console.debug('Starting threshold alert');
// ~~~~~~~~~~~~~~~~~~~~~~~~ Toevoegingen Erik ~~~~~~~~~~~~~~~~~~~~~~~~
var kamersenhaarden = [
"eetkamer",
"salon"
];
var verderzoeken = true;
for (let x = 0; x < kamersenhaarden.length && verderzoeken; x++) {
if (String(event.itemName).toLowerCase().includes(kamersenhaarden[x])) {
var kamernaam = kamersenhaarden[x];
verderzoeken = false;
}
}
var alertRuleUID = "haard_"+ kamernaam +"_procesbescherming_UIT";
// BELANGRIJK!
// BELANGRIJK!
// BELANGRIJK!- Hieronder moet alertRuleUID verwijderd worden!
// BELANGRIJK!
// BELANGRIJK!
// ~~~~~~~~~~~~~~~~~~~~~ Einde toevoegingen Erik ~~~~~~~~~~~~~~~~~~~~~
// Properties
var group = 'haarden_procesbeschermingstimers';
var thresholdStr = '0 s';
var operator = '==';
var comparison = (currState, threshold) => { return currState == threshold; };
var invert = false;
var defaultAlertDelay = 'PT2S';
var defaultRemPeriod = '';
var namespace = 'thresholdAlert';
//var alertRuleUID = 'haard_eetkamer_procesbescherming_UIT';
var endAlertUID = '';
var dndStart = '00:00';
var dndEnd = '00:00';
var gkDelay = 0;
var hystRange = '';
var rateLimitPeriod = '';
var reschedule = false;
var initAlertRuleUID = '';
// ~~~~~~~~~~~Functions
/**
* Converts an Item's state to a value we can compare in this rule.
*
* @param {State|string} an Item's state
* @return {String|float|Quantity} the state converted to a usable type
*/
var stateToValue = (state) => {
console.debug('Processing state ' + state + ' of type ' + typeof state);
if(typeof state === 'string') {
console.debug('state is a string: ' + state);
if(state.includes(' ')) {
try {
console.debug('state is a Quantity: ' + state);
return Quantity(state)
} catch(e) {
// do nothing, leave it as a String
console.debug('Not a Quantity but has a space, leaving as a string: ' + state);
return state;
}
}
else if(state == '') {
console.debug('state is the empty string, no conversion possible');
return state;
}
else if(!isNaN(state)) {
console.debug('state is a number: ' + state)
return Number.parseFloat(state);
}
else if(state == 'UnDefType' || state == 'NULL' || state == 'UNDEF') {
console.debug('state is an undef type, normalizing to UnDefType');
return 'UnDefType';
}
console.debug('Leaving state as a string');
return state;
}
else if(state instanceof DecimalType || state instanceof PercentType) {
console.debug('state is a DecimalType or PercentType: ' + state);
return state.floatValue();
}
else if(state instanceof QuantityType) {
console.debug('state is a QuantityType, converting to Quantity: ' + state);
return Quantity(state);
}
else {
console.debug('Not numeric, leaving as a string: ' + state);
return state.toString();
}
}
/**
* Determines if the Item is in an alerting state based on the configured comparison.apply
*
* @param {string|float|Quantity} current state of the Item
* @param {Object} record the Item's record of properties and timers
* @param {function(a,b)} operator the comparison operator to use
* @return {boolean} true if current is an alerting state
*/
var isAlertingState = (currState, record) => {
let calc = currState + ' ' + record.operator + ' ' + record.threshold;
if(record.invert) calc = '!(' + calc + ')';
console.debug('Checking if we are in the alerting state: ' + calc);
let rval = record.compare(currState, record.threshold);
rval = (record.invert) ? !rval : rval;
console.debug('Result is ' + rval);
return rval;
}
/**
* Checks the proposed alerting time and adjusts it to occur at the end of the DND
* if the proposed time falls in the DND time.
*
* @param {anything supported by time.toZDT()} timeout proposed time to send the alert
* @param {String} dndStart time the DND starts
* @param {String} dndEnd time the DND ends
* @return {time.ZonedDateTime} adjusted time to send the alert or null if there is a problem
*/
var generateAlertTime = (timeout, dndStart, dndEnd) => {
if(timeout === '' || (!(timeout instanceof time.ZonedDateTime) && !validateDuration(timeout))){
console.debug('Timeout ' + timeout + ' is not valid, using null');
return null;
}
let rval = time.toZDT(timeout.toString());
let start = time.toZDT(dndStart);
let end = time.toZDT(dndEnd);
if(rval.isBetweenTimes(start, end)) {
console.debug('Alert is scheduled during do not distrub time, moving to ' + dndEnd);
rval = end;
if(time.toZDT(rval).isBefore(time.toZDT())) {
console.debug('Moving alert time to tomorrow');
rval = timeUtils.toTomorrow(end);
}
}
return rval;
}
/**
* Calls the rule with the alert info, using the gatekeeper to prevent overloading the
* rule. The rule is called to inforce conditions.
*
* @param {string|float|Quantity} state Item state that is generating the alert
* @param {string} ruleID rule to call
* @param {boolean} isAlerting indicates if the Item is alerting or not
* @param {boolean} isInitialAlert indicates if the Item is just detected as alerting but not yet alerted
* @param {Object} record all the information related to the Item the rule is being called on behalf of
*/
var callRule = (state, ruleID, isAlerting, isInitialAlert, record) => {
if(ruleID == '') {
console.debug('No rule ID passed, ignoring');
return;
}
console.debug('Calling ' + ruleID + ' with alertItem=' + record.name + ', alertState=' + state + ', isAlerting='
+ isAlerting + ', and initial alert ' + isInitialAlert);
var rl = cache.private.get('rl', () => RateLimit());
const throttle = () => {
const gk = cache.private.get('gatekeeper', () => Gatekeeper());
const records = cache.private.get('records');
gk.addCommand(record.gatekeeper, () => {
try {
// If the Item hasn't triggered this rule yet and therefore doesn't have a record, skip it
const allAlerting = items[group].members.filter(item => records[item.name] !== undefined && isAlertingState(stateToValue(item.state), records[item.name]));
const alertingNames = allAlerting.map(item => item.label);
const nullItems = items[group].members.filter(item => item.isUninitialized);
const nullItemNames = nullItems.map(item => item.label);
rules.runRule(ruleID, {'alertItem': record.name,
'alertState': ''+state,
'isAlerting': isAlerting,
'isInitialAlert': isInitialAlert,
'threshItems': allAlerting,
'threshItemLabels': alertingNames,
'nullItems': nullItems,
'nullItemLabels': nullItemNames}, true);
} catch(e) {
console.error('Error running rule ' + ruleID + '\n' + e);
}
console.debug('Rule ' + ruleID + ' has been called for ' + record.name);
});
}
// Only rate limit the alert, always make the end alert call
(isAlerting && record.rateLimit !== '') ? rl.run(throttle, record.rateLimit) : throttle();
}
/**
* Creates the function that gets called by the loopingTimer for a given Item. The generated
* function returns how long to wait for the next call (adjusted for DND) or null when it's
* time to exit.
*
* @param {Object} Object containing alertTimer, endAlertTimer, alerted, alertState
* @return {function} function that takes no arguments called by the looping timer
*/
var sendAlertGenerator = (record) => {
return () => {
const item = items[record.name];
const currState = stateToValue(items[record.name].rawState);
refreshRecord(record);
// We can still get Multithreaded exceptions when calling another rule, this should reduce the occurance of that
console.debug('Alert timer expired for ' + record.name + ' with dnd between ' + record.dndStart + ' and ' + record.dndEnd + ' and reminder period ' + record.remPeriod);
let repeatTime = generateAlertTime(record.remPeriod, record.dndStart, record.dndEnd); // returns null if remPeriod is '', which cancels the loop
// Call alert rule if still alerting
if(isAlertingState(currState, record)) {
console.debug(record.name + ' is still in an alerting state.');
callRule(currState, record.alertRule, true, false, record);
if(!record.alerted) record.alertState = currState;
record.alerted = true;
if(repeatTime === null) record.alertTimer = null; // clean up if no longer looping
console.debug('Waiting until ' + repeatTime + ' to send reminder for ' + record.name);
return repeatTime;
}
// no longer alerting, cancel the repeat, send an alert if configured
else {
console.debug(record.name + ' is no longer in an alerting state but timer was not cancelled, this should not happen');
record.alertTimer = null;
return null;
}
}
}
/**
* Called when the Item event indicates that it's in an alerting state. If there isn't
* already a looping timer running, create one to initially run at alertTime (adjusted
* for DND) and repeat according to remPeriod. If dndStart is after dndEnd, the DND is
* assumed to span midnight.
*
* @param {State|String} state state of the Item that generated the event
* @param {Object} record contains alertTimer, endAlertTimer, and alerted flag
*/
var alerting = (state, record) => {
console.debug(record.name + ' is in the alert state of ' + state);
// Cancel the endAlertTimer if there is one
if(record.endAlertTimer !== null) {
console.debug('Cancelling endAlertTimer for ' + record.name);
record.endAlertTimer.cancel();
record.endAlertTimer = null;
}
// Set a timer for how long the Item needs to be in the alerting state before alerting.
// If one is already set, ignore it
let timeout = generateAlertTime(record.alertDelay, record.dndStart, record.dndEnd);
if(timeout === null) timeout = 'PT0S'; // run now
// First time the Item entered the alert state
if(record.alertTimer === null && !record.isAlerting) {
// Schedule the initAlertTimer first and it will run first
console.debug('Calling the initial alert rule for ' + record.name);
record.initAlertTimer = LoopingTimer();
record.initAlertTimer.loop(() => {
console.debug('Calling init alert rule for ' + record.name);
callRule(state, record.initAlertRule, false, true, record);
record.initAlertTimer = null;
return null;
}, generateAlertTime(time.toZDT(), record.dndStart, record.dndEnd));
// Create the alert timer
console.debug('Creating looping alert timer for ' + record.name + ' at ' + timeout);
record.alertTimer = LoopingTimer();
record.alertTimer.loop(sendAlertGenerator(record), timeout);
}
// Reschedule the alert timer
else if(record.alertTimer !== null && record.reschedule) {
console.debug('Rescheduling the timer for ' + record.name + ' at ' + timeout);
record.alertTimer.timer.reschedule(time.toZDT(timeout));
}
// Do nothing
else {
console.debug(record.name + ' already has an alert timer or has already alerted, ignoring event.');
}
}
/**
* Applies the hysteresis and returns true if the curr value is different enough from the
* alert value or if the calculation cannot be done because the three arguments are not
* compatible.
* @param {string|float|Quantity} curr current state
* @param {string|float|Quantity} alert the state that was alerted
* @param {string|float|hyst} hyst the hysteresis range
* @return {boolean} true if curr is different from alert by hyst or more, or if the arguments are incompatable.
*/
var applyHyst = (curr, alert, hyst) => {
console.debug('Applying hysteresis with: curr = ' + curr + ', alert = ' + alert + ', hyst = ' + hyst);
// Quantity
if(curr.unit !== undefined && alert.unit != undefined && hyst.unit !== undefined) {
try {
const delta = (curr.lessThan(alert)) ? alert.subtract(curr) : curr.subtract(alert);
console.debug('Applying hysteresis using Quantities, delta = ' + delta + ' hystRange = ' + hyst);
return delta.greaterThan(hyst);
} catch(e) {
console.error('Attempting to apply hysteresis with Quantities of incompatable units. Not applying hysteresis');
return true;
}
}
// Number
else if(typeof curr !== 'string' && typeof alert !== 'string' && typeof hyst !== 'string') {
curr = (curr.unit !== undefined) ? curr.float : curr;
alert = (alert.unit !== undefined) ? alert.float : alert;
hyst = (hyst.unit !== undefined) ? hyst.float : float;
const delta = Math.abs(curr - alert);
console.debug('Applying hysteresis using numbers, delta = ' + delta + ' hystRange = ' + hyst);
return delta > hyst;
}
else {
console.debug('Not all values are compatible, skipping hysteresis');
return true;
}
}
/**
* Called when the Item event indicates that it is not in an alerting state.
* Clean up the record a
* @param {string|float|Quantity} state state that generated the event
* @param {Object} record contains alertTimer, endAlertTimer, and alerted flag
*/
var notAlerting = (state, record) => {
console.debug(record.name + "'s new state is " + state + ' which is no longer in the alerting state, previously alerted = ' + record.alerted);
// Skip if we don't pass hysteresis
if(record.alerted && !applyHyst(state, record.threshold, record.hysteresis)) {
console.debug(record.name + ' did not pass hysteresis, remaining in alerting state');
return;
}
// Cancel the initAlertTimer
if(record.initAlertTimer !== null) {
console.debug('Cancelling alert timer for ' + record.name);
record.initAlertTimer.cancel();
record.initAlertTimer = null;
}
// Cancel the alertTimer
if(record.alertTimer !== null) {
console.debug('Cancelling alertTimer for ' + record.name);
record.alertTimer.cancel();
record.alertTimer = null;
}
// Send alert if required
if(record.endRule && record.alerted) {
console.debug('Sending alert that ' + record.name + ' is no longer in the alerting state');
// Schedule a timer if in DND, otherwise run now
const scheduleTime = generateAlertTime(time.toZDT(), record.dndStart, record.dndEnd);
if(record.endAlertTimer === undefined || record.endAlertTimer === null) {
record.endAlertTimer = LoopingTimer();
record.endAlertTimer.loop(() => {
console.debug('Calling end alert rule for ' + record.name);
callRule(stateToValue(items[record.name].rawState), record.endRule, false, false, record);
record.alerted = false;
record.alertState = null;
record.endAlertTimer = null;
return null;
},scheduleTime);
}
else {
record.endAlertTimer.timer.reschedule(scheduleTime);
}
}
else if(!record.endRule && record.alerted) {
console.debug('No end alert rule is configured, exiting alerting for ' + record.name);
record.alerted = false;
record.alertState = null;
}
else if(!record.alerted) {
console.debug('Exiting alerting state but alert was never sent, not sending an end alert for ' + record.name);
}
else {
console.warn('We should not have reached this!');
}
}
/**
* Returns a function that executes the passed in comparison on two operands. If
* both are numbers a numerical comparison is executed. Otherwise the operands are
* converted to strings and a string comparison is done.
*
* @param {string} operator the comparison the that the returned function will execute
* @return {function(a, b)} function tjat executes a number comparison if both operands are numbers, a string comparison otherwise
*/
var generateStandardComparison = (operator) => {
return (a, b) => {
let op = null;
switch(operator) {
case '==':
op = (a, b) => a == b;
break;
case '!=':
op = (a, b) => a != b;
break;
case '<' :
op = (a, b) => a < b;
break;
case '<=':
op = (a, b) => a <= b;
break;
case '>' :
op = (a, b) => a > b;
break;
case '>=':
op = (a, b) => a >=b;
break;
}
if(isNaN(a) || isNaN(b)) {
return op(''+a, ''+b); // convert to strings if both operands are not numbers
}
else {
return op(a, b);
}
}
}
/**
* Creates the custom comparison operator. If both operands are Quantities and they
* have compatible units, the Quantity comparison will be returned. Otherwise a
* standard comparison will be used.
*
* @param {string} operator the comparison operator
* @return {function(a, b)} function that executes the right comparison operation based on the types of the operands
*/
var generateComparison = (operator) => {
// Assume quantity, revert to standard if not or incompatible units
return (a, b) => {
let op = null;
switch(operator) {
case '==':
op = (a, b) => a.equal(b);
break;
case '!=':
op = (a, b) => !a.equal(b);
break;
case '<' :
op = (a, b) => a.lessThan(b);
break;
case '<=':
op = (a, b) => a.lessThanOrEqual(b);
break;
case '>' :
op = (a, b) => a.greaterThan(b);
break;
case '>=':
op = (a, b) => a.greaterThanOrEqual(b);
break;
}
try {
if(a.unit !== undefined && b.unit !== undefined) return op(a, b);
else return generateStandardComparison(operator)(a, b);
} catch(e) {
// Both are Quantities but they have incompatible units
return generateStandardComparison(operator)(a, b);
}
}
}
/**
* Populate/refresh the record with any changes to parameters that may have been
* made to the Item's metadata since the rule was first run.
*
* @param {Object} record contains all the settings and timners for an Item
*/
var refreshRecord = (record) => {
// Metadata may have changed, update record with latest values
console.debug('Populating record from Item metadata or rule defauls');
const md = (items[record.name].getMetadata()[namespace] !== undefined) ? items[record.name].getMetadata()[namespace].configuration : {};
console.debug('Converting threshold to value');
record.threshold = stateToValue(thresholdStr);
if(md['threshold'] !== undefined) record.threshold = stateToValue(md['threshold']);
if(md['thresholdItem'] !== undefined) record.threshold = stateToValue(items[md['thresholdItem']].rawState);
record.operator = (md['operator'] !== undefined) ? md['operator'] : operator;
record.invert = (md['invert'] !== undefined) ? md['invert'] === true : invert;
record.reschedule = (md['reschedule'] !== undefined) ? md['reschedule'] === true : reschedule;
record.compare = generateComparison(record.operator);
record.alertDelay = (md['alertDelay'] !== undefined) ? md['alertDelay'] : defaultAlertDelay;
record.remPeriod = (md['remPeriod'] !== undefined) ? md['remPeriod'] : defaultRemPeriod;
record.alertRule = (md['alertRuleID'] !== undefined) ? md['alertRuleID'] : alertRuleUID;
record.endRule = (md['endRuleID'] !== undefined) ? md['endRuleID'] : endAlertUID;
record.initAlertRule = (md['initAlertRuleID'] !== undefined) ? md['initAlertRuleID'] : initAlertRuleUID
record.gatekeeper = (md['gatekeeperDelay'] !== undefined) ? md['gatekeeperDelay'] : gkDelay;
console.debug('Converting hysteresis to value');
record.hysteresis = stateToValue((md['hysteresis'] !== undefined) ? md['hysteresis'] : hystRange);
record.rateLimt = (md['rateLimit'] !== undefined) ? md['rateLimit'] : rateLimitPeriod;
record.dndStart = (md['dndStart'] !== undefined) ? md['dndStart'] : dndStart;
record.dndEnd = (md['dndEnd'] !== undefined) ? md['dndEnd'] : dndEnd;
console.debug('Processing event for Item ' + record.name + ' with properties: \n'
+ ' Threshold - ' + record.threshold + '\n'
+ ' Operator - ' + record.operator + '\n'
+ ' Invert - ' + record.invert + '\n'
+ ' Reschedule - ' + record.reschedule + '\n'
+ ' Alert Delay - ' + record.alertDelay + '\n'
+ ' Reminder Period - ' + record.remPeriod + '\n'
+ ' Alert Rule ID - ' + record.alertRule + '\n'
+ ' End Alert Rule ID - ' + record.endRule + '\n'
+ ' Init Alert Rule ID - ' + record.initAlertRule + '\n'
+ ' Gatekeeper Delay - ' + record.gatekeeper + '\n'
+ ' Hystersis - ' + record.hysteresis + '\n'
+ ' Rate Limt - ' + record.rateLimt + '\n'
+ ' DnD Start - ' + record.dndStart + '\n'
+ ' DnD End - ' + record.dndEnd);
}
/**
* Process an Item event, checking to see if it is in an alerting state and calling
* the alerting rule with repeats if configured. If it is no longer in an alerting
* state and an alert was sent, call the end alerting rule if configured.
*
* @param {string} name the name of the Item
* @param {string|float|Quantity} state the Item's current state
*/
var procEvent = (name, state) => {
// const value = stateToValue(state);
console.debug('Processing state ' + state + ' from ' + name);
const records = cache.private.get('records', () => { return {}; });
// Initialze the record in timers if one doesn't already exist or it's incomplete
// if(records[name] === undefined
// || records[name].alertTimer === undefined
// || records[name].endAlertTimer === undefined
// || records[name].initAlertTimer == undefined
// || records[name].alerted === undefined) {
// console.debug('Initializing record for ' + name);
// records[name] = {name: name,
// alertTimer: (records[name] === undefined) ? null: records[name].alertTimer,
// endAlertTimer: (records[name] === undefined) ? null: records[name].endAlertTimer,
// initAlertTimer: (records[name] === undefined) ? null: records[name].initAlertTimer,
// alerted: false};
// }
// Initialize the record in timers if one doesn't already exist
if(records[name] === undefined) {
console.debug('Initializing record for ' + name);
records[name] = { name: name,
alertTimer: null,
endAlertTimer: null,
initAlertTimer: null,
alerted: false };
}
const record = records[name];
refreshRecord(record);
// Alerting state, set up an alert timer
if(isAlertingState(state, record)) {
alerting(state, record);
}
// Not alerting, cancel the timer and alert if configured
else {
notAlerting(state, record);
}
}
/**
* @param {string} op candidate operator
* @return {boolen} true if op is one of ==, !=, <, <=, >, >=
*/
var validateOperator = (op) => {
return ['==', '!=', '<', '<=', '>', '>='].includes(op);
}
/**
* @param {string} dur candidate ISO8601 duration
* @return {boolean} true if dur can be parsed into a duration
*/
var validateDuration = (dur) => {
try {
if(dur !== '') time.Duration.parse(dur);
return true
} catch(e) {
console.error('Failed to parse duration ' + dur + ': ' + e);
return false;
}
}
/**
* @param {string} t candidate time
* @return {boolean} true if t can be converted into a valid datetime
*/
var validateTime = (t) => {
try {
const militaryHrRegex = /^(0?[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){1,2}$/;
const meridianHrRegex = /^(0?[0-9]|1[0-2])(:[0-5][0-9]){1,2} ?[a|p|A|P]\.?[m|M]\.?$/;
if(militaryHrRegex.test(t) || meridianHrRegex.test(t)) {
time.toZDT(t);
return true;
} else {
return false;
}
} catch(e) {
console.error('Failed to parse time ' + t + ': ' + e);
return false;
}
}
/**
* @param {string} id candidate rule id
* @return {boolean} true if the rule exists and is enabled
*/
var validateRule = (id) => {
try {
if(!rules.isEnabled(id)) {
console.error('Rule ' + id + ' is disabled');
return false;
}
return true;
} catch(e) {
console.error('Rule ' + id + ' does not exist');
return false;
}
}
/**
* @param {string} h candidate hysteresis
* @return {boolean} true if h is either a number or Quantity
*/
var validateHysteresis = (h) => {
const parsed = stateToValue(h);
return !isNaN(parsed) || parsed.unit !== undefined;
}
/**
* Analyzes the rule properties and Items to verify that the config will work.
*/
var init = () => {
console.debug( 'Rule config defaults:\n'
+ ' Group - ' + group + '\n'
+ ' Threhsold - ' + thresholdStr + '\n'
+ ' Operator - ' + operator + '\n'
+ ' Invert Operator - ' + invert + '\n'
+ ' Reschedule - ' + reschedule + '\n'
+ ' Default Alert Delay - ' + defaultAlertDelay + '\n'
+ ' Default Reminder Duration - ' + defaultRemPeriod + '\n'
+ ' DND Start - ' + dndStart + '\n'
+ ' DND End - ' + dndEnd + '\n'
+ ' Alert Rule - ' + alertRuleUID + '\n'
+ ' End Alert Rule - ' + endAlertUID + '\n'
+ ' Alert Group - ' + group + '\n'
+ ' Alert Items - ' + items[group].members.map(i => i.name).join(', ') + '\n'
+ ' Gatekeeper Delay - ' + gkDelay + '\n'
+ ' Rate Limit - ' + rateLimitPeriod);
var error = false;
var warning = false;
const allItems = items[group].members;
console.info('Cancelling any running timers');
const records = cache.private.get('records');
if(records !== null) {
Object.values(records).forEach(record => {
if(record.alertTimer !== null && record.alertTimer !== undefined){
console.info(record.name + ' has an alert timer scheduled, cancelling now.');
record.alertTimer.cancel();
record.alertTimer = null;
}
if(record.endAlertTimer !== null && record.endAlertTimer !== undefined) {
console.info(record.name + ' has an end alert timer scheduled, cancelling now.');
record.endAlertTimer.cancel();
record.endAlertTimer = null;
}
});
}
// Inform if the threshold is UnDefType
if(thresholdStr === 'UnDefType') {
console.info('Threshold is UnDefType, this will cause Item states of NULL and UNDEF to be converted to UnDefType for comparisons.');
}
// Error if the Group has QuantityTypes but thresholdStr is not
const quantityTypes = allItems.filter(item => item.quantityState !== null);
if(quantityTypes.length > 0 && thresholdStr !== 'UnDefType') {
try {
if(!thresholdStr.includes(' ')) {
warn = true;
console.warn(group + ' contains Quantity states by thresholdStr ' + thresholdStr + ' does not have units, comparison will revert to number or string comparison.');
}
else {
Quantity(thresholdStr);
}
} catch(e) {
warning = true;
console.warn(group + ' contains Quantity states but thresholdStr ' + thresholdStr + ' cannot be converted to a Quantity, comparison will default to a String comparsion');
}
}
// Error if the Group has more than one type of Item
const itemTypes = allItems.filter(item => item.rawItem.class.name === allItems[0].rawItem.class.name);
if(itemTypes.length != allItems.length) {
warn = true;
console.warn(group + ' has a mix of Item types');
}
// Warn if thresholdStr is empty string, there are cases where that could be OK but most of
// the time it isn't.
if(thresholdStr == '') {
warning = true;
console.warn('Alert State is an empty String, is that intended?');
}
// Verify the operator is valid
if(!validateOperator(operator)) {
error = true;
console.error('Invalid operator ' + operator);
}
// Inform that there is no default alert delay configured, there will be no pause before alerting
if(defaultAlertDelay == '') {
console.info('Items without ' + namespace + ' alertDelay metadata will alert immediately');
} else if(!validateDuration(defaultAlertDelay)){
error = true;
console.error('The default alert delay ' + defaultAlertDelay + ' is not a parsable ISO8601 duration string');
}
// Inform that there is no default reminder period configured, there will be no repreated alerts
if(defaultRemPeriod == '') {
console.info('Items without ' + namespace + ' remPeriod metadata will not repeat alerts');
} else if(!validateDuration(defaultRemPeriod)){
error = true;
console.error('The default reminder period ' + defaultRemPeriod + ' is not a parsable ISO8601 duration string');
}
// Inform if both of the DND times are not set
// Error if one but not the other is defined
// Error if either cannot be parsed into a ZDT
if(dndStart == '' && dndEnd == '') {
console.info('DND Start and End are empty, no DND period will be applied');
}
else if(dndStart == '' && dndEnd != '') {
error = true;
console.error('DND Start is defined but DND End is not.');
}
else if(dndStart != '' && dndEnd == '') {
error = true;
console.error('DND Start is not defined but DND End is.')
}
else if(dndStart != '' && !validateTime(dndStart)) {
error = true;
console.error('DND Start ' + dndStart + ' is not parsable into a time');
}
else if(dndEnd != '' && !validateTime(dndEnd)) {
error = true;
console.error('DND End ' + dndEnd + ' is not parsable into a time');
}
// Error is no alert rule is configured or the rule is disabled or does not exist
if(alertRuleUID == '') {
error = true;
console.error('No alert rule is configured!');
} else if(!validateRule(alertRuleUID)) {
error = true;
console.error('Alert rule ' + alertRuleUID + ' is disabled or does not exist');
}
// Inform if the end alert rule is not configured
// Error if it is configured but it is disabled or does not exist
if(endAlertUID == '') {
console.info('No end alert rule configured, no rule will be called when alerting ends');
}
else if(!validateRule(endAlertUID)) {
error = true;
console.error('End alert rule ' + endAlertUID + ' is diabled or does not exist');
}
// Inform if the initial alert rule is not configured
// Error if it is configured but it is disabled or does not exist
if(initAlertRuleUID == '') {
console.info('No initial alert rule configured, no rule will be called when the alert state is first detected');
}
else if(!validateRule(initAlertRuleUID)) {
error = true;
console.error('Initial alert rule ' + initAlertRuleUID + ' is disable or does not exist');
}
// Validate hysteresis
if(hystRange !== '' && !validateHysteresis(hystRange)) {
error = true;
console.error('Hysteresis ' + hystRange + ' is not a number nor Quantity');
}
// Warn if there are QuantityTypes but hystRange does not have units
if(hystRange !== '' && quantityTypes.length > 0) {
if(!hystRange.includes(' ')) {
warning = true;
console.warn(group + ' has QuantityTypes but hystRange ' + hystRange + ' does not have units.');
}
else {
try {
Quantity(hystRange);
} catch(e) {
warning = true;
console.warn(group + ' has QuantityTypes by hystRange ' + hystRange + ' cannot be parsed into a Quantity.');
}
}
}
// Validate rateLimit Period and warn if less than gkDelay
if(rateLimitPeriod !== '' && !validateDuration(rateLimitPeriod)) {
error = true;
console.error('A rate limit was provided but ' + rateLimitPeriod + ' is not a parsable ISO8601 duration string.');
}
if(rateLimitPeriod !== '' && gkDelay) {
if(time.Duration.parse(rateLimitPeriod).lessThan(time.Duration.ofMillis(gkDelay))) {
warning = true;
console.warn('Both rate limit and gatekeeper delay are defined but gatekeeper delay is greater than the rate limit. The rate limit should be significantly larger.');
}
}
// Inform if none of the Items have namespace metadata
const noMetadata = allItems.filter(item => item.getMetadata()[namespace] === undefined);
if(noMetadata.length > 0) {
console.info('These Items do not have ' + namespace + ' metadata and will use the default properties defined in the rule: '
+ noMetadata.map(i => i.name).join(', '));
}
// Validate Item metadata
allItems.filter(item => item.getMetadata()[namespace]).forEach(item => {
const md = item.getMetadata()[namespace].configuration;
console.debug('Metadata for Item ' + item.name + '\n' + Object.keys(md).map(prop => prop + ': ' + md[prop]).join('\n'));
// Anything is OK for threshold
if(md['thresholdItem'] !== undefined) {
const threshItem = md['thresholdItem'];
if(md['threshold'] !== undefined) {
error = true;
console.error('Item ' + item.name + ' has both thershold and thresholdItem defined in ' + namespace + ' metadata');
}
if(items[threshItem] === undefined || items[threshItem] === null) {
error = true;
console.error('Item ' + item.name + ' defines thresholdItem ' + threshItem + ' which does not exist');
} else if(item.quantityState !== null && items[threshItem].quantityState === null){
error = true;
console.error('Item ' + item.name + ' is a Quantity but thresholdItem ' + threshItem + ' is not');
} else if(item.quantityState === null && items[threshItem].quantityState !== null) {
error = true;
console.error('Item ' + item.name + ' is not a Quantity but thresholdItem ' + threshItem + ' is');
} else if(item.numericState !== null && items[threshItem].numericState === null) {
error = true;
console.error('Item ' + item.name + ' is a number but thresholdItem ' + threshItem + ' is not');
} else if(item.numericState === null && items[threshItem].numericState !== null) {
error = true;
console.error('Item ' + item.name + ' is not a number but thresholdItem ' + threshItem + ' is');
}
}
if(md['operator'] !== undefined && !validateOperator(md['operator'])) {
error = true;
console.error('Item ' + item.name + ' has an invalid operator ' + md['operator'] + ' in ' + namespace + ' metadata');
}
if(md['invert'] !== undefined && (md['invert'] !== true && md['invert'] !== false)) {
error = true;
console.error('Item ' + item.name + ' has an unparsible invert ' + md['invert'] + ' in ' + namespace + ' metadata');
}
if(md['reschedule'] !== undefined && (md['reschedule'] !== true && md['reschedule'] !== false)) {
error = true;
console.error('Item ' + item.name + ' has an unparsible reschedule ' + md['reschedule'] + ' in ' + namespace + ' metadata');
}
if(md['alertDelay'] !== undefined && !validateDuration(md['alertDelay'])) {
error = true;
console.error('Item ' + item.name + ' has an unparsable alertDelay ' + md['alertDelay'] + ' in ' + namespace + ' metadata');
}
if(md['remPeriod'] !== undefined && !validateDuration(md['remPeriod'])) {
error = true;
console.error('Item ' + item.name + ' has an unparsable remPeriod ' + md['remPeriod'] + ' in ' + namespace + ' metadata');
}
if(md['alertRuleID'] !== undefined && !validateRule(md['alertRuleID'])) {
error = true;
console.error('Item ' + item.name + ' has an invalid alertRuleID ' + md['alertRuleID'] + ' in ' + namespace + ' metadata');
}
if(md['endRuleID'] !== undefined && !validateRule(md['endRuleID'])) {
error = true;
console.error('Item ' + item.name + ' has an invalid endRuleID ' + md['endRuleID'] + ' in ' + namespace + ' metadata');
}
if(md['initAlertRuleID'] !== undefined && !validateRule(md['initAlertRuleID'])) {
error = true;
console.error('Item ' + item.name + ' has an invalid initAlertRuleID ' + md['initAlertRuleID'] + ' in ' + namespace + ' metadata');
}
if(md['gatekeeperDelay'] !== undefined && isNaN(md.gatekeeperDelay)) {
error = true;
console.error('Item ' + item.name + ' has a non-numerical gatekeeperDelay ' + md['gatekeeperDelay'] + ' in ' + namespace + ' metadata');
}
if(md['gatekeeperDelay'] !== undefined && md['gatekeeperDelay'] < 0) {
warning = true;
console.warn('Item ' + item.name + ' has a negative gatekeeperDelay ' + md['gatekeeperDelay'] + ' in ' + namespace + ' metadata');
}
if(md['hysteresis'] !== undefined && !validateHysteresis(md['hysteresis'])) {
error = true;
console.error('Item ' + item.name + ' has a non-numerical nor Quantity hystersis ' + md['hysteresis'] + ' in' + namespace + ' metadata');
}
else if(md['hysteresis'] && item.quantityState !== null && stateToValue(md['hysteresis']).unit === undefined) {
warn = true;
console.warn('Item ' + item.name + ' has a Quantity but hystereis ' + md['hysteresis'] + ' is not a Quantity');
}
else if(md['hysteresis'] && item.quantityState !== null && stateToValue(md['hysteresis']).unit !== undefined) {
try {
item.quantityState.equal(stateToValue(md['hysteresis']));
} catch(e) {
error = true;
console.error('Item ' + item.name + ' has a hysteresis ' + md['hysteresis'] + ' with units incompatible with ' + item.quantityState.unit);
}
}
if(md['rateLimit'] !== undefined && !validateDuration(md['rateLimit'])) {
error = true;
console.error('Item ' + item.name + ' has an unparsable rateLimit ' + md['rateLimit'] + ' in ' + namespace + ' metadata');
}
if(md['dndStart'] !== undefined && !validateTime(md['dndStart'])) {
error = true;
console.error('Item ' + item.name + ' has an unparsable dndStart time ' + md['dndStart'] + ' in ' + namespace + ' metadata');
}
if(md['dndEnd'] !== undefined && !validateTime(md['dndEnd'])) {
error = true;
console.error('Item ' + item.name + ' has an unparsable dndEnd time ' + md['dndEnd'] + ' in ' + namespace + ' metadata');
}
if((md['dndStart'] === undefined && md['dndEnd'] !== undefined)
|| (md['dndStart'] !== undefined && md['dndEnd'] === undefined)) {
error = true;
console.error('Item ' + item.name + ' one but not both of dndStart and dndEnd defined on ' + namespace + ' metadata');
}
// Warn if metadata is present but none of the known configuration parameters are
if(!['threshold', 'thresholdItem', 'operator', 'invert', 'alertDelay', 'remPeriod', 'alertRuleID', 'endRuleID', 'gatekeeperDelay', 'hysteresis', 'rateLimit', 'dndStart', 'dndEnd']
.some(name => md[name] !== undefined)) {
warning = true;
console.warn('Item ' + item.name + ' has ' + namespace + ' metadata but does not have any known configuration property used by this rule');
}
});
if(error) console.error('Errors were found in the configuration, see above for details.')
else if(warning) console.warn('Warnings were found in the configuration, see above for details. Warnings do not necessarily mean there is a problem.')
else console.info('Threshold Alert configs check out as OK')
}
//~~~~~~~~~~~~~ Body
// If triggered by anything other than an Item event, check the config
// Otherwise process the event to see if alerting is required
if(this.event === undefined) {
console.debug('Rule triggered without an event, checking config.');
init();
}
else {
switch(event.type) {
case 'ItemStateEvent':
case 'ItemStateUpdatedEvent':
case 'ItemStateChangedEvent':
console.loggerName = loggerBase+'.'+event.itemName;
console.debug('Processing an Item event');
procEvent(event.itemName, stateToValue(event.itemState));
break;
default:
console.info('Rule triggered without an Item event, ' + event.type + ' checking the rule config');
//cache.private.clear();
init();
}
}