Threshold Alert and Open Reminder [4.0.0.0;4.9.9.9]

Problem Statement
Frequently one wants to do something when one or more Items exceed or fall below a certain threshold, matches a given state for a given amount of time, or fails to change state (or update) for a certain amount of time. Put simply: “When an Item remains in a given state for a certain amount of time, do something.”

For example:

  • When the temperature remains above 75 °F for five minutes, turn on the air conditioner, turn it off when the temp reaches or falls below 70 °F.

  • When motion is detected turn off a light and then turn it off five minutes after the last motion is detected.

  • Send an alert when a door or window is left open for more than an hour.

  • Set a sensor Item to UNDEF if it doesn’t receive an update for fifteen minutes.

Note: This is a complete rewrite of the previous Threshold Alert and Open Reminder rule templates, combining the two into a single rule template. In addition to merging the behaviors, the behaviors are slightly different from both of these predecessors. Please read carefully if you are coming from OH 3.

See Threshold Alert and Open Reminder [4.0.0.0;4.9.9.9] - #2 by rlkoshak for some configuration examples with explanation. This template can do a lot of different things depending on the parameters configured because the use case is really common…

What the Rule Does
This rule is triggered by changes to the members of a Group. The Group should have members that are all of the same type of Item. At this time it does not support a mix of compatible Item types (e.g. Switches, Dimmers and Color Items) except in the case where it’s looking for NULL or UNDEF as the alerting state.

When the rule triggers, it compares the state of the Item with a threshold state using a user selected comparison.

  • If the comparison evaluates to true (or false if invert is enabled), an optional user defined rule is called.
  • After a user defined delay an optional alert rule is called.
  • When the Item leaves this alerting state, the rule will optionally call an end alert rule, but only if the threshold alert rule was called.
  • Any call to any rule that occurs during a user defined Do Not Disturb (DND) period are delayed to the end of that period. Only the most recent call gets made.
  • Finally, an optional reminder period can be defined to call the alert rule repeatedly while the Item continues to meet the threshold comparison. These reminders also follow the do not disturb period.

The initial alert rule, alert rule, and end alert rule will receive these parameters passed to them when called:

Variable Purpose
alertItem name of the Item that generated the alert
alertState current state of the Item
isAlerting boolean, when true the Item meets the threshold comparison, when false the Item exited the threshold state
isInitialAlert boolean, when true the Item just now met the threshold comparison, when `false, the Item has been in this state for awhile.
threshItems JavaScript Array of JavaScript Item Objects whose state meet the alerting criteria
threshItemLabels JavaScript Array of the Item labels whose state meet the alerting criteria
nullItems JavaScript Array of JavaScript Item Objects whose state is NULL or UNDEF
nullItemLabels JavaScript Array of the Item labels whose state is NULL or UNDEF

To access these attributes in your called rule varies from language to language. Blockly has a context block. JS Scripting ECMAScript 2021 injects them as variables (e.g. alertItem). Nashorn JS Scripting (and other JSR223 rules engines) makes them available via context.getAttribute('alertItem');. Rules DSL does not support passed in values like this.

Rule Properties

Property Format Required Default Purpose
Triggering Group Group Item X Changes to it’s members cause the rule to run.
Threshold State String X The state to compare the members of Triggering Group to using the Comparison Operator. If it’s parsable as a number, a number comparison will be made. If it’s a number with units, a Quantity comparison will be made. UnDefType, NULL and UNDEF are all normalized to UnDefType and treated as the same state. In all other cases a string comparison will be made.
Comparison Operator X The comparison operator to use (e.g. ==)
Invert Comparison Boolean false Invert the result of the comparison.
Reschedule Boolean false When true, if an alert event occurs when an alerting timer already exists, the timer will be rescheduled instead of ignoring the new event. The different is whether the alert occurs based on the first alerting event or the most recent alerting event.
Hysteresis String '' Optional parameter which, when populated, will wait until the Item’s state changes at least that amount from the threshold before disabling the alert. Only applied for states which are Numbers or QuantityTypes. If the comparison is >, the Item will need to drop at least this amount below the threshold before alerting ends, and vice versa for <. If == the value needs to move above or below the threshold by this amount. It doesn’t make sense to use with !=.
Alert Delay ISO8601 duration string (e.g. PT15M) '' How long to wait after the Item meets the threshold comparison before calling the Alert Rule. Leave blank to call the rule immediately.
Reminder Period ISO8601 duration string '' How long to wait after the first call to the alert rule before calling it again as a reminder. The rule will continue to be recalled by this period until the Item no longer meets the threshold comparison.
Metadata Namesapce String thresholdAlert An optional metadata namespace where a per Item override for Alert Delan and Reminder Period can be defined (see below).
Alert Rule Rule UID X Rule called when the Item meets the threshold comparison and repeatedly thereafter if Reminder Period is defined.
End Alert Rule Rule UID '' Rule called when the Item first exits the threshold comparison. This can be the same as Alert Rule or Initial Alert Rule.
Initial Alert Rule Rule UID '' Rule called when the Item first meets the threshold comparison. This can be the same as Alert Rule or the End Alert Rule.
Do Not Disturb Start Time HH:MM '' The start time for the optional DND. If this is after the End DND time, the period will span midnight.
Do Not Disturb End Time HH:MM '' The end time for the optional DND. The most recent call to either Alert Rule or End Alert Rule will be delated until this time.
Gatekeeper Delay Number 0 The minimum amount of time between calls to the alert and end alert rules before the next call will be allowed to occur. If you experience “Multi-threaded” exceptions, increase this time until they stop. The time is in milliseconds. If using a DND period, set this to something between 500 and 1000 depending on how long your alerting rule takes to run.
Rate Limit ISO8601 Duration String '' The minimum amount of time between calls to the alert rule before the next call will be allowed to occur. Unlike Gatekeeper Delay, the rate limit will simply drop any events that occur rather than queueing them up and working them off in sequence. If used with Gatekeeper Delay, Rate Limit should be significantly longer.

Any or all of the above properties except for Triggering Group and Metadata Namespace can be defined at the Item level using Item metadata with the addition of one extra parameter. Instead of threshold, a thresholdItem can be defined which will cause the state of that Item to be used as the threshold instead of the statically defined threshold. In the UI a full set of metadata should look like the following

value: " "
config:
  thresholdItem: OfficeHumidity_Setpoint
  operator: <
  invert: false
  reschedule: false
  hysteresis: 4 %
  alertDelay: PT1M
  remPeriod: PT15M
  alertRuleID: new_humidity_proc
  endRuleID: new_humidity_proc
  initAlertRuleID: new_humidity_proc
  dndStart: 22:01
  dndEnd: 08:02
  gatekeeperDelay: 10
  rateLimit: PT30M

All of these metadata are optional. If not present, the rule’s setting will be used for that Item. These should be used to override a default when one or two Items need one or two different parameters (e.g. one door needs to alert at five minutes when left open instead of one hour).

dndStart and dndEnd must both be present or both be absent and represent a time using either 24 hour (e.g. 22:00) or 12 hour (e.g. 10:00 PM) format.

gatekeeperDelay, when present, must be a positive integer.

Running the rule manually will cause the rule to verify the configuration of the rule parameters and the Item metadata configurations. Watch the logs for warnings and errors.

Requirements

Language: JS Scripting ECMAScript 2021

Dependencies:

  • A script to call when an Item meets the threshold
  • A Group containing all the Items to trigger this rule. All members of the Group must be the same type.
  • openhab-js 4.5+
  • openhab_rules_tools 2.0.3+

TODO:

  • There might be a logic bug somewhere. I went on vacation and after a week I had dozens of timers begin tracked for each Item instead of just the one timer. This needs more investigation.
  • DateTime comparisons (i.e. a DateTime Item’s state is too long ago)
  • Restart alerting on OH startup instead of requiring an event to kick off alerting
  • Support using separate DateTime Items for DND periods (so we can use Astro based times)
  • Option to operate at the semantic model equipment level (alert is called on Equipment, not point Items)
  • Option to ignore events during DND instead of moving them to the end of DND

Changelog

Version 1.4

  • Fixed regression which breaks comparisons to UnDefType.
  • removed stray newline

Version 1.3

  • Fixed an issue where the alert timer uses what ever logger name that was used the last time the rule was run. Since the Item name is part of the logger name this creates logs that look like they are from a different Item.
  • Fixed a potential issue with the alert timer generator. The record Object was passed into the generator but if it’s passed by reference, the generator will use what ever record was last used by the rule. I don’t actually think this was a real error becuase then the rule probably wouldn’t work at all, but pulling the record from the cache from inside the timer is a better idea anyway.

Version 1.2

  • oops, there is no v1.2

Version 1.1

  • fixed a minor bug that occurs when an alert occurs between the DND time and the rule fails to reschedule the alert to the end of the DND time.

Version 1.0

  • promoted to version 1.0
  • fixed a bug with the end alert looping timer being recreated even though it already exists, resulting in many looping timers building up during the DND period for each Item You would only see this if you have a DND period defined and an Item alerts and then ends alerting during the DND period.

Version 0.14

  • Fixed misspelling in log statement
  • Call the initialAlertRule the first time entering the alert state
  • Cancel timers before setting them to null
  • Loop through the values of the records, not the keys, when looking for timers to cancel

Version 0.13

  • Do not pass raw ZonedDateTime Objects to create timers. That’s failing for some reason.

Version 0.12

  • adjusting for changes to OHRT updates.

Version 0.11

  • Various fixes
Function Problem Fix
stateToValue The JS function isNaN returns true for empty string causing problems when hysteresis was not set. Added if clause to handle empty string explicitly.
isAlertingState The ternary operation in the log statement was not working, showing the wrong value in the logs. Calculate the return value and save to a variable instead of recalculating it in the log statement.
notAlerting Testing the wrong timer in record before cancelling initAlertTimer Fixed if statement
refreshRecord It’s not clear which value is being sent to stateToValue for conversion. Added log statements before calls to stateToValue indicating what is being converted (state, threshold, hyst).
procEvent state is already converted to a value before this function is called Use state instead of converting it again.
procEvent record[name] should only be initialized once Don’t reinitialize if one of the timers or alerted do not exist. Treat those cases as an error.
procEvent Code simplicity Let notAlerting handle the case where we are no longer alerting but there is no endAlert rule to call.
refreshRecord Missing newline after “Init Alert Rule ID” in log statement.
Body Missing case for ItemStateUpdatedEvent Added case for ItemStateUpdateEvent to handle cases where the default trigger is changed to received update from changed.
Body It’s not always clear when the rule is triggered manually Change debug log statement to info level when manually triggered.

Version 0.10

  • Handles the new to OH 4.0.0 M2 and later ItemStateUpdatedEvent

Version 0.9

  • give each Item it’s own logger to make tracing logs easier
  • schedule initAlertTimer to the end of DND like the alertTimer and endAlertTimer

Version 0.8

  • normalize UNDEF and NULL to UnDefType so both are treated as the same
  • added an initial alert rule called when the Item first enters the alerting state to make motion sensor light type use cases easier to implement

Version 0.7

  • refresh the Item’s settings inside the looping timer so reminders always run with the latest Item metadata settings
  • cancel any running timers in int()

Version 0.6

  • added a reschedule parameter which causes the rule to call the alert rule alert duration after the most recent alerting event instead of after the first alerting event. This implements alerting when an Item doesn’t change for too long a time, mostion sensor timers, etc…

Version 0.5

  • added ability to define another Item whose state will be used as the threshold (thresholdItem)
  • any and all rule properties can be overridden through Item metadata
  • todo: update the docs above to cover the metadata and throughly test the config checking

Version 0.4

  • added rate limiting option

Version 0.3

  • catch and log exception if the alerting rule no longer exists or is disabled
  • pass a list of all the Items and their labels that are null/undef and those that are alerting to have parity with the old Threshold Alert rule to ease migration.

Version 0.2

  • added support for hysteresis

Version 0.1

  • initial release

Sponsorship

If you want to send a tip my way or sponsor my work you can through Sponsor @rkoshak on GitHub or PayPal. It won’t change what I contribute to OH but it might keep me in coffee or let me buy hardware to test out new things.

Resources

https://raw.githubusercontent.com/rkoshak/openhab-rules-tools/refs/heads/main/rule-templates/thresholdAlert/newThresholdAlert.yaml

7 Likes

Humidity Alerts and Control

Where I live it’s dry and I’ve a number of humidifiers scattered throughout the house. When the tank runs out the humidity drops pretty fast and I want to get a notification.

Create the rule that will generate the notification. This can be a script. Mine looks like this:

var {alerting} = require('rlk_personal');
var logger = log('Humidity Alert');
var location = items[actions.Semantics.getLocation(items[alertItem]).name];

var msg = 'The humidity in ' + location.label + ' is low (' + alertState + ')! Remember to refill the humidifier.'
alerting.sendAlert(msg, logger);

logger.info('There are ' + nullItemLabels.length + ' null Items and ' + threshItemLabels.length + ' Items total in the alerting state');

alerting is a personal library that change how I’m alerted based on the time of day and other factors.

I instantiate an instance of this rule template with the following properties:

Property Value What it does
Triggering Group MinIndoorHumidity Group that holds the Items with the humidity sensor readings
Threshold State 35 % Setpoint for the rule to alert
Comparison Operator < Comparison the Item’s current state is made against the Threshold State. If the comparison evaluates to true, the Item is considered to be in the alerting state
Invert Comparison false If true it would negate the Comparison Operator (e.g. the Item would be considered alerting if it’s >= the Threshold State instead of <).
Reschedule false The alert delay applies from the time where the Item first enters an alerting state.
Hysteresis 3 % After calling the alert Item, wait for the humidity to return to 38 % (35 % + 3%) before considering the Item no longer alerting. In this case, this helps keep the Item from bouncing between the threshold and sending lots of alerts.
Alert Delay '' Don’t wait and alert immediately.
Reminder Period PT8H If the humidity remains below 35 %, repeat the alert every 8 hours.
Metadata Namespace humidityAlert
Alert Rule new_humidity_proc The ID of the rule/script shown above.
End Alert Rule '' Do nothing when the humidity is no longer alerting.
Initial Alert Rule '' Do nothing when first entering the alerting state.
Do Not Disturb Start Time 22:00 Don’t call the alert or end alert rule after 10PM.
Do Not Disturb End Time 08:00 Allow the alert or end alert rule to be called again. If a call to one of the rules was scheduled between the start and end time and the Item is still in that alerting/end alerting state, the rule will be called at this time. That way, even if the humidifiers run out at night, I’ll still get notified.
Gatekeeper Delay 0 Calls to the alerting/end alerting rules will happen no faster than this delay (in milliseconds). This can help if the rule is being called rapidly or it interacts with devices that cannot process commands to fast. In this case we have no delay.
Rate Limit '' If this had a value, this defines a period during which new alerts are ignored after calling the alerting rule. Unlike gatekeeper, it drops the events instead of queueing them to deliver later.

Note that most of the above are the defaults. So what does this rule do? It calls new_humidity_proc immediately when a humidity Item drops below 35%. If it is between 10PM and 8AM the call to the rule is delayed until 8AM. If the Item remains below 35% for an additional 8 hours, the rule is called again. The Item will continue to be considered alerting until it reaches 38% and then won’t become alerting again until it falls below 35% again.

However, there is one sensor where I want the threshold to be 40%. So I’ve added the following humidityAlert metadata for that Item to:

value: " "
config:
  threshold: 40 %

Open Door Reminder

I want to get an alert when a door is left open for too long. However, each door has a different amount of time before I want to get the alert.

I created a rule to process the alert.

var {alerting} = require('rlk_personal');
var logger = log('Open Door');

var item = items.getItem[alertItem];
if(isAlerting) alerting.sendAlert(item.label + ' is now closed', logger);
else alerting.sendAlert(item.label +' has been open for a long time!');

I instantiate an instance of this rule template with the following properties:

Property Value What it does
Triggering Group DoorsStatus Group that holds the Items with the door Contact Items
Threshold State OPEN We want to alert on OPEN
Comparison Operator == Any state other than OPEN is considered not alerting
Invert Comparison false If true it would negate the Comparison Operator (e.g. !=).
Reschedule false The alert period starts when the door first opens.
Hysteresis '' Hysteresis can only be used with numeric states.
Alert Delay PT15M If not overridden in the Item metadata, alert when the door remains OPEN for 15 minutes.
Reminder Period PT1H If not overridden in the Item metadata, repeat the alert every hour.
Metadata Namespace dayDoorAlert I’m not going to show it here, but I’ve another instance of this rule to generate alerts for open doors at night time.
Alert Rule door_reminder_detection The ID of the rule/script shown above.
End Alert Rule door_reminder_detection The ID of the rule/script shown above.
Initial Alert Rule '' Do nothing on first entering the alert state.
Do Not Disturb Start Time 22:00 Don’t call the alert or end alert rule after 10PM.
Do Not Disturb End Time 08:00 Allow the alert or end alert rule to be called again. If a call to one of the rules was scheduled between the start and end time and the Item is still in that alerting/end alerting state, the rule will be called at this time. That way, there is a handoff between the daytime rule and the nighttime rule.
Gatekeeper Delay 0 Calls to the alerting/end alerting rules will happen no faster than this delay (in milliseconds). This can help if the rule is being called rapidly or it interacts with devices that cannot process commands to fast. In this case we have no delay.
Rate Limit '' If this had a value, this defines a period during which new alerts are ignored after calling the alerting rule. Unlike gatekeeper, it drops the events instead of queueing them to deliver later.

Again, most of the above are the default values. This rule will send me an alert when a door remains open for too long and repeats the alert periodically between the hours of 08:00 and 22:00.

Each door where I want to customize the Alert Delay and Reminder have dayDoorAlert metadata. For example, I want to be alerted when my garage door is left open for only five minutes, but don’t remind me again for four hours.

value: " "
config:
  alertDelay: PT5M
  remPeriod: PT4H

On-the-other-hand I want to get an alert only if the back door is left open for more than eight hours and I want no reminders.

value: " "
config:
  alertDelay: PT8H
  remPeriod: PT0S

Motion Sensor Light

This is an example of controlling a light based on the state of a motion sensor Item. The light should turn on upon the first motion detection and turn off five minutes after the last motion is detected. The assumption is that the Switch Item representing the motion sensor does not return to OFF on it’s own, it just gets repeated ON updates on each motion detection.

First we define a rule that gets called on the first motion and five minutes after the last motion to control the light.

var light = items[alertItem.replace('Sensor', 'Light')];
if(isInitialAlert) {
  light.sendCommand('ON');
} else {
  light.sendCommand('OFF');
  items[alertItem].postUpdate('OFF'); // reset the motion sensor
}

This script assumes that the light Item is named the same as the motion sensor only with “Light” instead of “Sensor” (e.g. the motion sensor could be “FrontdoorSensor” and the light could be “FrontdoorLight”).

I instantiate an instance of this rule template with the following properties:

Property Value What it does
Triggering Group MotionSensors Group that holds the motion sensor Items
Threshold State ON We want to alert on ON
Comparison Operator == Any state other than ON is considered not alerting
Invert Comparison false If true it would negate the Comparison Operator (e.g. !=).
Reschedule false The alert period starts when the door first opens.
Hysteresis '' Hysteresis can only be used with numeric states.
Alert Delay PT5M If not overridden in the Item metadata, call the alert rule when the motion sensor Item remains on for 5 minutes without being updated.
Reminder Period '' No reminders
Metadata Namespace motionSensorTimer Metadata namespace, maybe you want a different time per motion sensor.
Alert Rule motion_sensor_light The ID of the rule/script shown above. When called, isAlerting will be true and isInitialAlert will be false.
End Alert Rule '' Do nothing at the end of the alert.
Initial Alert Rule motion_sensor_light The ID of the rule/script shown above. When called, isAlerting will be false (isAlerting remains false until the alert rule is called) and isInitialAlert will be true.
Do Not Disturb Start Time 06:00 Don’t call the rules during the day time.
Do Not Disturb End Time 20:00 Don’t call the rules during the day time.
Gatekeeper Delay 0 Calls to the alerting/end alerting rules will happen no faster than this delay (in milliseconds). This can help if the rule is being called rapidly or it interacts with devices that cannot process commands to fast. In this case we have no delay.
Rate Limit '' If this had a value, this defines a period during which new alerts are ignored after calling the alerting rule. Unlike gatekeeper, it drops the events instead of queueing them to deliver later.

One additional change is to modify the rule trigger to be “Member of Group receives update”.

No Motion Timer (i.e. Item remains in same state for too long)

This is an example of a motion sensor timer where you want something to occur after a given amount of time after the most recent event. This could be to turn off a light, alert that a sensor has been offline for too long, etc.

In my particular case I want an alert if no motion is detected at my dad’s house for eight hours or more with a reminder every eight hours until motion is detected again.

My rule to process the alerts is pretty simple for now. We haven’t found a good place to put it where it detects him and not the dogs so I don’t publish notifications right now.

if(isAlerting) console.log('It has been a long time since dad moved.');
else console.log("Motion detected at dad's house after alerting.');
Property Value What it does
Triggering Group MotionSensors Group that holds the Items with the motion sensors. I’ve just the one but it’s easier to put it into a Group than to modify the trigger and code to handle an individual Item.
Threshold State ON Motion results in an ON update to the Item
Comparison Operator == Any state other than OFF is ignored
Invert Comparison false If true it would negate the Comparison Operator (e.g. !=).
Reschedule true The alert delay starts after the most recent event with the Item still in the alerting state.
Hysteresis '' Hysteresis can only be used with numeric states.
Alert Delay PT8H Alert when the motion sensor doesn’t change for over eight hours.
Reminder Period PT8H Repeat the alert every eight hours.
Metadata Namespace dadMotion I may decide to override things in metadata but for not I’m not using this.
Alert Rule dad_motion_alert The ID of the rule/script shown above.
End Alert Rule ‘dad_motion_alert’ The ID of the rule/script shown above.
Initial Alert Rule '' Do nothing on the initial alert.
Do Not Disturb Start Time 22:00 Don’t call the alert or end alert rule after 10PM.
Do Not Disturb End Time 08:00 Allow the alert or end alert rule to be called again. If a call to one of the rules was scheduled between the start and end time and the Item is still in that alerting/end alerting state, the rule will be called at this time. That way, there is a handoff between the daytime rule and the nighttime rule.
Gatekeeper Delay 0 Calls to the alerting/end alerting rules will happen no faster than this delay (in milliseconds). This can help if the rule is being called rapidly or it interacts with devices that cannot process commands to fast. In this case we have no delay.
Rate Limit '' If this had a value, this defines a period during which new alerts are ignored after calling the alerting rule. Unlike gatekeeper, it drops the events instead of queueing them to deliver later.

I don’t define any overridden parameters at the Item level for this one.

Sensor Online/Offline Alerting

Generate an alert when an Item stops updating for a given amount of time. This can be useful to monitor periodically updating sensor Items and report when they stop reporting. An alternative approach is to use Expire to set the Item to UNDEF or NULL but this could be less work overall.

First define a rule/script to handle the calls.

console.debug('Sensor status proc called with Item ' + alertItem + ', state ' + alertState + ' initialAlert ' + isInitialAlert + ' and alerting ' + isAlerting);
if(isInitialAlert) {
  // you might want to add some book keeping to only call if the alert was sent
  console.info('Sensor ' + alertItem + ' is back online!')
}
else {
  console.info('Sensor ' + alertItem + ' has stopped reporting, updating the Item to UNDEF');
  // send alert here
  const update = (alertItem) => {
    return () => {
      items[alertItem].postUpdate('UNDEF');
    }
  }
  setTimeout(update(alertItem), 250); // helps avoid multi-threaded exceptions.
}

I instantiate an instance of this rule template with the following properties:

Property Value What it does
Triggering Group AllSensors Group that holds the sensor Items
Threshold State UnDefType We want to start tracking the time on valid states.
Comparison Operator == Any state other than ON is considered not alerting
Invert Comparison true Negates the Comparison Operator (e.g. !=) so the timer starts on any non-UnDefType.
Reschedule true Reschedule the alert timer to happen after the last event received.
Hysteresis '' Hysteresis can only be used with numeric states.
Alert Delay PT15M If not overridden in the Item metadata, call the alert rule when the sensor remains unchanged for 15 minutes without being updated.
Reminder Period '' No reminders
Metadata Namespace sensorStatus Metadata namespace, sensors have varying reporting periods.
Alert Rule sensor_status_proc The ID of the rule/script shown above. When called, isAlerting will be true and isInitialAlert will be false.
End Alert Rule '' Do nothing at the end of the alert.
Initial Alert Rule sensor_status_proc The ID of the rule/script shown above. When called, isAlerting will be false (isAlerting remains false until the alert rule is called) and isInitialAlert will be true.
Do Not Disturb Start Time 22:00 Don’t call the rules during the day time.
Do Not Disturb End Time 08:00 Don’t call the rules during the day time.
Gatekeeper Delay 250 There are lots of sensors so the chances of multi-threaded exceptions is high.
Rate Limit '' If this had a value, this defines a period during which new alerts are ignored after calling the alerting rule. Unlike gatekeeper, it drops the events instead of queueing them to deliver later.
2 Likes

i have no luck with your rule templates… after having a running debounce setup i tested treshold alert and once again ran into an error:

my config of the threshold rule:

configuration:
  dndEnd: 00:00
  rateLimit: ""
  invert: false
  initAlertRule: ""
  dndStart: 00:00
  thresholdState: 11 °C
  alertRule: Threshold_Temperatur_Alarm
  endAlertRule: ""
  operator: ">"
  defaultRemPeriod: ""
  hysteresis: ""
  reschedule: false
  namespace: thresholdAlert
  gkDelay: 0
  defaultAlertDelay: ""
  group: gKuehlschrankTemperaturen
triggers:
  - id: "2"
    configuration:
      groupName: gKuehlschrankTemperaturen
    type: core.GroupStateChangeTrigger
2023-08-08 22:00:49.022 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - Processing event for Item TuYaTempHumSensor1_temperature with properties:
  Threshold          - 11 ° C
  Operator           - >
  Invert             - false
  Reschedule         - false
  Alert Delay        -
  Reminder Period    -
  Alert Rule ID      - Threshold_Temperatur_Alarm
  End Alert Rule ID  -
  Init Alert Rule ID -   Gatekeeper Delay   - 0
  Hystersis          - NaN
  Rate Limt          -
  DnD Start          - 00:00
  DnD End            - 00:00
2023-08-08 22:00:49.023 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - Checking if we are in the alerting state: 12 °C > 11 ° C
2023-08-08 22:00:49.024 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - false
2023-08-08 22:00:49.025 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - TuYaTempHumSensor1_temperature is in the alert state of 12 °C
2023-08-08 22:00:49.026 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - Timeout  is not valid, using null
2023-08-08 22:00:49.027 [DEBUG] [hrank.TuYaTempHumSensor1_temperature] - Calling the initial alert rule for TuYaTempHumSensor1_temperature
2023-08-08 22:00:49.058 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: Error: "2023-08-08T22:00:49.028+02:00[SYSTEM]" is an unsupported type for conversion to time.ZonedDateTime
        at <js>.toZDT(/etc/openhab/automation/js/node_modules/openhab/time.js:266) ~[?:?]
        at <js>.createTimer(/etc/openhab/automation/js/node_modules/openhab_rules_tools/helpers.js:16) ~[?:?]
        at <js>.loop(/etc/openhab/automation/js/node_modules/openhab_rules_tools/loopingTimer.js:29) ~[?:?]
        at <js>.alerting(<eval>:241) ~[?:?]
        at <js>.procEvent(<eval>:506) ~[?:?]
        at <js>.:program(<eval>:910) ~[?:?]
        at org.graalvm.polyglot.Context.eval(Context.java:399) ~[?:?]
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458) ~[?:?]
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426) ~[?:?]
        at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:262) ~[java.scripting:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocableAndAutocloseable.eval(DelegatingScriptEngineWithInvocableAndAutocloseable.java:53) ~[?:?]
        at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.eval(InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable.java:78) ~[?:?]
        at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.lambda$0(ScriptActionHandler.java:71) ~[?:?]
        at java.util.Optional.ifPresent(Optional.java:178) ~[?:?]
        at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.execute(ScriptActionHandler.java:68) ~[?:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1188) ~[?:?]
        at org.openhab.core.automation.internal.RuleEngineImpl.runRule(RuleEngineImpl.java:997) ~[?:?]
        at org.openhab.core.automation.internal.TriggerHandlerCallbackImpl$TriggerData.run(TriggerHandlerCallbackImpl.java:87) ~[?:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) ~[?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[?:?]
        at java.lang.Thread.run(Thread.java:833) ~[?:?]
2023-08-08 22:00:49.063 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'TresholdAlertKuhlschrank' failed: org.graalvm.polyglot.PolyglotException: Error: "2023-08-08T22:00:49.028+02:00[SYSTEM]" is an unsupported type for conversion to time.ZonedDateTime

already enabled debug log… can you once again help me??

What version of openhab_rules_tools?
What version of openhab-js?

You can find out by running the following in Scratchpad.

console.info('openhab-js version: ' + utils.OPENHAB_JS_VERSION);
console.info('OHRT version: ' + helpers.OHRT_VERSION);

What setting is enabled in Settings → Automation → JS Scripting → Blue gear icon?

Have you installed openhab-js or are you using the one built into the add-on?

Long story short, there was a bug in openhab-js that got fixed some time ago that made it unable to parse the .toString() from a DateTime Item. It should work with the library that comes with the add-on in 4.0.1 though. But if you’ve installed openhab-js separately, you might have forgotten to update it. Or you may be using it and not realizing it.

if i check via npm:

root@raspi4:/etc/openhab/automation/js# npm list
js@ /etc/openhab/automation/js
├── openhab_rules_tools@2.0.2
└── openhab@4.5.1

if i run your test script:

2023-08-08 23:12:39.531 [INFO ] [penhab.automation.script.ui.testrule] - openhab-js version: 4.5.1
2023-08-08 23:12:39.580 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: ReferenceError: "helpers" is not defined

my js scripting settings:

i do not really understand why it does not see/use the openhab_rules_tools…

i have completly uninstalled the openhab and openhab_rules_tools and reinstalled… does not help

Oh, it is using openhab_rules_tools to some degree. The rule template rule would have failed immediately if you weren’t.

I messed up and forgot the import for helpers above.

var {helpers} = require('openhab_rules_tools');
console.info('openhab-js version: ' + utils.OPENHAB_JS_VERSION);
console.info('OHRT version: ' + helpers.OHRT_VERSION);

The npm versions are both recent enough. 2.0.2 is the latest OHRT and I think 4.5.1 is the latest openhab-js. It’s certainly later than the version I’m running actually so it should have the fix for the parsing problem.

The add-on settings look correct.

What versions are reported with the rule, once you add the import for helpers?

thanks for the help, after running your new test script it started to work

2023-08-09 22:03:40.575 [INFO ] [penhab.automation.script.ui.testrule] - openhab-js version: 4.5.1
2023-08-09 22:03:40.577 [INFO ] [penhab.automation.script.ui.testrule] - OHRT version: 2.0.2

is it possible that you have to “require” the helper script once to make it work??

Yes, you have to require helper because that’s where helper.OHRT_VERSION lives.

I’m not sure what could be wrong. I’m using openhab-js 4.5.0 and OHRT 2.0.2 and it works. I’ll try upgrading to 4.5.1 and see if this is a new regression or something.

I switched over to the built in openhab-js library and now I’m seeing the same error. There is definitely a bug with the library that’s coming with the add-on.

Though even weirder, it does not happen with 4.5.1 installed through npm. As a temporary work around install openhab-js through npm and switch the add-on to not use the built in library. I’ll file an issue on the library.

so after updating the openhab-js to the latest dev version (unreleased fix vom 12. aug), the rules start to work like they should… BUT :slightly_smiling_face:

one again i found a “strange” behaviour… i try to setup an alert rule for my fridge and freezer temperatures… the fridge should alert if temperatur is over 11°C and the freezers if it is over -18 °C

and it seems to work for the fridge (where i test with positive values) but it does not work right with negative values like on the freezer…

my setup:

the rule:

configuration:
  dndEnd: 05:00
  rateLimit: ""
  invert: false
  initAlertRule: Threshold_Temperatur_Alarm_Init
  dndStart: 00:00
  alertRule: Threshold_Temperatur_Alarm
  endAlertRule: TresholdTemperaturAlarmEnd
  thresholdState: 11 °C
  defaultRemPeriod: PT30S
  operator: ">"
  hysteresis: ""
  reschedule: false
  namespace: thresholdAlertTemperatur
  gkDelay: 200
  defaultAlertDelay: PT1S
  group: gTemperaturAlert

overriden metadata on the freezer items:

value: " "
config:
  threshold: -18.0 °C

now i get the following rule output when it SHOULD alert (but does not!):

2023-08-13 17:34:03.470 [INFO ] [penhab.automation.script.ui.testrule] - openhab-js version: 4.5.1

==> /var/log/openhab/events.log <==
2023-08-13 17:34:03.470 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'GefrierkombiTemperatur' changed from -19 °C to -17 °C

==> /var/log/openhab/openhab.log <==
2023-08-13 17:34:03.471 [INFO ] [penhab.automation.script.ui.testrule] - OHRT version: 2.0.2

==> /var/log/openhab/openhab.log <==
2023-08-13 17:34:03.520 [DEBUG] [shold Alert.ThresholdAlertTemperatur] - Starting threshold alert
2023-08-13 17:34:03.523 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing an Item event
2023-08-13 17:34:03.527 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -17 of type object
2023-08-13 17:34:03.529 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a QuantityType, converting to Quantity: -17
2023-08-13 17:34:03.531 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -17 °C of type object
2023-08-13 17:34:03.533 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Not numeric, leaving as a string: -17 °C
2023-08-13 17:34:03.535 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -17 °C from GefrierkombiTemperatur
2023-08-13 17:34:03.537 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Populating record from Item metadata or rule defauls
2023-08-13 17:34:03.559 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state 11 °C of type string
2023-08-13 17:34:03.560 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string: 11 °C
2023-08-13 17:34:03.562 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a Quantity: 11 °C
2023-08-13 17:34:03.565 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -18.0 °C of type string
2023-08-13 17:34:03.566 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string: -18.0 °C
2023-08-13 17:34:03.568 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a Quantity: -18.0 °C
2023-08-13 17:34:03.570 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state  of type string
2023-08-13 17:34:03.572 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string:
2023-08-13 17:34:03.573 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a number:
2023-08-13 17:34:03.576 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing event for Item GefrierkombiTemperatur with properties:
  Threshold          - -18 °C
  Operator           - >
  Invert             - false
  Reschedule         - false
  Alert Delay        - PT1S
  Reminder Period    - PT30S
  Alert Rule ID      - Threshold_Temperatur_Alarm
  End Alert Rule ID  - TresholdTemperaturAlarmEnd
  Init Alert Rule ID - Threshold_Temperatur_Alarm_Init  Gatekeeper Delay   - 200
  Hystersis          - NaN
  Rate Limt          -
  DnD Start          - 00:00
  DnD End            - 05:00
2023-08-13 17:34:03.578 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Checking if we are in the alerting state: -17 °C > -18 °C
2023-08-13 17:34:03.581 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - true
2023-08-13 17:34:03.583 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - GefrierkombiTemperatur is not in an alerting state

and the following output when it SHOULD NOT alert:

2023-08-13 17:46:43.316 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'GefrierkombiTemperatur' changed from -22.7 °C to -22.8 °C
2023-08-13 17:46:43.332 [INFO ] [hab.event.GroupItemStateChangedEvent] - Item 'gTemperature' changed from 16.2 °C to 16.192307692307693 °C through GefrierkombiTemperatur

==> /var/log/openhab/openhab.log <==
2023-08-13 17:46:43.343 [DEBUG] [shold Alert.ThresholdAlertTemperatur] - Starting threshold alert
2023-08-13 17:46:43.346 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing an Item event
2023-08-13 17:46:43.348 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -22.8 of type object
2023-08-13 17:46:43.350 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a QuantityType, converting to Quantity: -22.8
2023-08-13 17:46:43.353 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -22.8 °C of type object
2023-08-13 17:46:43.355 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Not numeric, leaving as a string: -22.8 °C
2023-08-13 17:46:43.357 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -22.8 °C from GefrierkombiTemperatur
2023-08-13 17:46:43.359 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Populating record from Item metadata or rule defauls
2023-08-13 17:46:43.383 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state 11 °C of type string
2023-08-13 17:46:43.384 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string: 11 °C
2023-08-13 17:46:43.386 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a Quantity: 11 °C
2023-08-13 17:46:43.388 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state -18.0 °C of type string
2023-08-13 17:46:43.389 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string: -18.0 °C
2023-08-13 17:46:43.390 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a Quantity: -18.0 °C
2023-08-13 17:46:43.393 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing state  of type string
2023-08-13 17:46:43.394 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a string:
2023-08-13 17:46:43.395 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - state is a number:
2023-08-13 17:46:43.398 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Processing event for Item GefrierkombiTemperatur with properties:
  Threshold          - -18 °C
  Operator           - >
  Invert             - false
  Reschedule         - false
  Alert Delay        - PT1S
  Reminder Period    - PT30S
  Alert Rule ID      - Threshold_Temperatur_Alarm
  End Alert Rule ID  - TresholdTemperaturAlarmEnd
  Init Alert Rule ID - Threshold_Temperatur_Alarm_Init  Gatekeeper Delay   - 200
  Hystersis          - NaN
  Rate Limt          -
  DnD Start          - 00:00
  DnD End            - 05:00
2023-08-13 17:46:43.400 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - Checking if we are in the alerting state: -22.8 °C > -18 °C
2023-08-13 17:46:43.402 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - false
2023-08-13 17:46:43.403 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - GefrierkombiTemperatur is in the alert state of -22.8 °C
2023-08-13 17:46:43.430 [DEBUG] [ertTemperatur.GefrierkombiTemperatur] - GefrierkombiTemperatur already has an alert timer or has already alerted, ignoring event.

I’m not sure I ever had opportunity to test it with negative values so there very well could be a bug there.

I;m not exactly sure what I’m seeing in the log.

The config seems to check out. It definitely registered the -18 override in metadata for that Item for the threshold. But it seems off that it’s still processing the 11. It shouldn’t see 11 at all for this Item.

Then it checks if we’ve exceeded the threshold and it says “true” so it’s weird that it would then decide it’s not an alerting state.

I’m going to have to do some analysis that I don’t have time for right now. It might be a couple days before I get back.


Edit1:

I looked into this a bit and there is a bug in the log statement that tells me the result of the comparison with the invert applied. It is logging exactly the opposite from what it should. So indeed it looks like the comparison is failing somehow. But I’ve not yet ruled out a failure in converting the quantityTypes to. a proper value. Still looking. In the mean time, in the script Action you can change the second half of the isAlerting function to the following:

  let rval = record.compare(currState, record.threshold);
  rval = (record.invert) ? !rval : rval;
  console.debug('Result is ' + rval);
  return rval;

That fixes the log statement.

This part is suspicuious. Where is this blank coming from? Still looking…

This is coming from hysteresis. This might be the problem… It’s showing as NaN in your logs.


Edit 2

I’ve made some changes and fixed a couple of unrelated bugs discovered while looking at the code and reported on github.

Please take to code below and replace everything from the line that says // ~~~~~~~~~~~Functions to the end of the file with it. The rule configuration is captured in the variable above this line.

Changes:

  • stateToValue: isNaN returns false for empty String, handle that case separately
  • isAlertingState: the ternary operator in the log statement wasn’t working, reworked so the log statement is complete and accurate
  • refreshRecord: added some logging statements so we can see which values are being converted by stateToValue
  • procEvent: state has already been converted to a value, don’t convert it again
  • procEvent: only initialize the record with values if the record doesn’t exist, don’t override alerted
  • procEvent: let the case where there is no end alerting rule configured be handled by the call to notAlerting
// ~~~~~~~~~~~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);
  let start = time.toZDT(dndStart);
  let end = time.toZDT(dndEnd);
  if(rval.isBetweenTimes(dndStart, dndEnd)) {
    console.debug('Alert is scheduled during do not distrub time, moving to ' + dndEnd);
    rval = dndEnd;
    if(time.toZDT(rval).isBefore(time.toZDT())) {
      console.debug('Moving alert time to tomorrow');
      rval = timeUtils.toTomorrow(dndEnd);
    }
  }
  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', () => new rateLimit.RateLimit());

  const throttle = () => {
    const gk = cache.private.get('gatekeeper', () => new 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 = new loopingTimer.LoopingTimer();
    record.initAlertTimer.loop(() => {
      console.debug('Calling init alert rule for ' + record.name);
      callRule(state, record.alertRule, false, true, record);
      record.initAlertTimer = 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 = new loopingTimer.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.info('Applying hysteresis with: ' + curr + ', ' + alert + ', ' + hyst);

  // Quantity
  if(curr.unit !== undefined && alert.unit && hyst.unit !== undefined) {
    try {
      const delta = (curr.lessThan(alert)) ? alert.subtract(curr) : curr.subtract(alert);
      console.info('Applying hysteresis, 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') {
    const delta = Math.abs(curr - alert);
    console.info('Applying hysteresis, delta = ' + delta + ' hystRange = ' + hyst);
    return delta > hyst;
  }
  else {
    console.info('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.info(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
    record.endAlertTimer = new loopingTimer.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;
    }, generateAlertTime(time.toZDT(), record.dndStart, record.dndEnd));
  }
  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.keys(records).forEach(record => {
      if(record.alertTimer !== null && record.alertTimer !== undefined){
        console.info(record.name + ' has an alert timer scheduled, cancelling now.');
        record.alertTimer = null;
        record.alertTimer.cancel();
      }
      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) {
    error = true;
    console.error(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 repeate 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.equals(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 'ItemStateChangedEvent':
      console.loggerName = loggerBase+'.'+event.itemName;
      console.debug('Processing an Item event');
      procEvent(event.itemName, stateToValue(event.itemState));
      break;
    default:
      console.debug('Rule triggered without an Item event, checking the rule config');
      cache.private.clear();
      init();
  }
}

just updated my rule with your new code, and it works GREAT! …really great rule template! …i think you can update the template on the marketplace

…thanks for all the help… next step will be the monitoring of my windows :slight_smile:

I didn’t think I had actually fix the problem yet. This is good news. It must have been the problem with parsing hysteresis. It was treating empty string like a number.

Because of the number of changes I’m going to test run it a bit more before updating the template. I’ll come back when I post the fixes.

Thank you for this template. It has allowed me to tidy up many of my rules.

Just a couple of points:

It looks like the Initial Alert Rule is never called.

In the alerting method, the line:

      callRule(state, record.alertRule, false, true, record);

should be

      callRule(state, record.initAlertRule, false, true, record);

and also, in the init method ‘Cancelling any running timers’, should these two lines should be reversed?

        record.alertTimer = null;
        record.alertTimer.cancel();

Although in practice this code never seems to run because record.alertTimer !== undefined is always false, even when there is an alert timer running. It would probably throw anyway.

This bit I do not really understand. I think it’s due to reloading of the script, and the cache being recreated.

Good catch. I use the same rule for both in my instantiations so I never noticed the error.

Probably but I wonder why it’s not throwing errors.

That would explain it. I’ll have to think about what’s really going on there.

Thanks for the bug reports. Stay tuned to the top post for an update when I get them fixed. (I’ll add a new version).

Another minor point. In the top post, the note to modify line 44 should be:
$OH_CONFIG/automation/js/node_modules/openhab_rules_tools/loopingTimer.js

Thanks! I fixed the top post. Stay tuned for the other fixes. It might take a day or to if I can’t get a break to work it this morning.

Some more info on the ‘init’ method

Changing the line

Object.keys(records).forEach(record => {

to

Object.values(records).forEach(record => {

makes ‘cancelling running timers’ work.

However, my JavaScript knowledge is marginal, so please don’t rely on it.

Thanks, I’ll look into that as well. I’ll try to get an updated template published sometime tomorrow or Tuesday but can’t promise anything right now. My time is short right now and it’s hard to find 60 consecutive minutes to devote to this.

Updates for all the little bugs you found are now posted in version 0.14. I’ve done a bunch of testing on my own but would appreciate confirmation that it’s working as expected for you.

That’s because of the other bug you found where we loop though the keys instead of the values.

And there usually won’t be any timers to cancel unless you run the rule manually and it’s been triggered prior to that.

Yes that fixed things - great.

I’ve learned so much looking at your code. Thank you.

1 Like