Is Notification of an Event using a Dummy Switch a good method?

I want to get a Single Notification on certain events… say when my freezer is too warm and one again when it is cold enough. The following seems to work well for this example but before I replicate it out for other Items to monitor I just want to check if there is a “better” way?

Items

Number Freezer_Temp "Freezer Temperature [%.1f °C]" <temperature> { channel="zwave:device:cc323ca9:node21:sensor_temperature" }
Switch Freezer_Alarm

Rules

rule "Freezer Temp Alarm Part 1"
when
	Item Freezer_Temp changed
then
	if (Freezer_Temp.state > -10)
		postUpdate(Freezer_Alarm, ON)  // Toggle ON the Alarm
	else
		postUpdate(Freezer_Alarm, OFF)  // Toggle OFF the Alarm
end
rule "Freezer Temp Alarm Part 2"
when
	Item Freezer_Alarm changed  //  Send a Notification with Details when the Alarm Turns ON or OFF
then
	logInfo("notifications", "Sending Freezer Alarm: " + Freezer_Alarm.state + ", Current Temp is " + Freezer_Temp.state)
	...
end

Thanks
Nathan

I can stay right off the bat without even looking at the code that there is a better way than replicating the same two rules over and over.

First, let’s consider the alerting part. Actually, you’ve stumbled upon a fairly elegant way to keep getting the same alert over and over as long as the freezer is too warm. If this were your only sensor that you need to alert in this way I would absolutely leave it as written.

However, you’ve more than one of these to report on and it’s generally not the best idea to have a lot of duplicated code. So let’s rework it. In fact I actually just rewrote my own code that does this for some of the services and machines my home automation depends upon. Unfortunately, my code only works with Switches and it’s written in Python. I’ll post it below for the curious.

The general approach is to set a flag when you send the alert that the temp is above the threshold. When that flag is true, you know that you’ve already sent the alert that the temp is too high. When the temp drops again, send the alert and set the flag to false. As long as the temp stays below the threshold and the flag is false don’t sent the alert.

var freezer_alert_sent = false

rule "Freezer Temp Alarm"
when
    Item Freezer_Temp changed
then
    var temp_high= Freezer_Temp.state > -10

    if(temp_high && !freezer_alert_sent){
        logInfo("notifications", "Sending Freezer Alarm: " + Freezer_Alarm.state + ", Current Temp is " + Freezer_Temp.state)
        freezer_alert_sent = true
    }
    else if(!temp_high && freezer_alert_sent){
        logInfo("notifications", "Cancelling Freezer Alarm")
        freezer_alert_sent = false
    }
end

So in the above we figure out whether the temp is too high. If so and we haven’t already alerted, we generate the alert and set the flag to true. If the temp is not too high and we’ve previously alerted, we send the cancelling message and set the alerted flag to false.

But what if we can make this even more generic. What if we use String Items for the alert Item and we move the alerting checking into one central place?

Why a String? Because that will let us have a unique message for each sensor. It also lets us use the transform Profile which we could use with the JavaScript transformation or the Scale transformation to eliminate the Rule that checks the temp.

  1. Create a Group to hold all the Alarm Items. Group:String Alarms

  2. Change Freezer_Alarm to be a String and add a JS transform.

String Freezer_Alarm { channel="zwave:device:cc323ca9:node21:sensor_temperature"[profile="transform:JS", function="freezer_alert.js"] }
  1. Create freezer_alert.js:
(function(i) {
    if(isNaN(i)) return "alarm";
    if(i > -10) return "alarm";
    else "normal"
})(input)

This moves the freezer check out of the Rule. Of course, this could also be done in the Rule. I just wanted to show an alternative approach. Neither is better or worse as an approach.

  1. Make a generic alerting Rule.
import java.util.Map

val Map<String, Boolean> alerted_flags = newHashMap

rule "Alerting"
when
    Member of Alarms changed to "alarm" or
    Member of Alarms changed to "normal"
then
    // We don't care about changes from UNDEF and NULL
    if(oldState == NULL || oldState == UNDEF) return;

    var alerted = alerted_flags.get(triggeringItem.name)
    if(alerted == null) flag = false 
    val device = triggeringItem.name.split("_").get(0)

    if(newState == "alarm" && !alerted){
        logInfo("notifications", "Sending " + device + " Alarm!")
        alerted_flags.put(triggeringItem.name, true)
    }
    else if(newState == "normal" && alerted){
        logInfo("notifications", "Cancelling " + device + " Alarm")
        alerted_flags.put(triggeringItem.name, false)
    }
end

It’s also possible to include the sensor reading in the alert message itself using Design Pattern: Associated Items which I’ll leave as an exercise.

Here is my Python code. It works on Switches but the overall concept is the same. I do use access to Item metadata to convert the name of my Item to something more human friendly (see Design Pattern: Human Readable Names in Messages and Design Pattern: Using Item Metadata as an Alternative to Several DPs).

from core.rules import rule
from core.triggers import when
from core.metadata import get_key_value, set_metadata
from personal.util import send_info, get_name

@rule("Offline Alert",
      description="Triggers when a status Switch Item changes state",
      tags=["admin"])
@when("Member of gSensorStatus changed to ON")
@when("Member of gSensorStatus changed to OFF")
def offline_alert(event):

    if isinstance(event.oldItemState, UnDefType):
        offline_alert.log.debug("{} changed to {} from {}, ignoring."
                                .format(get_name(event.itemName), event.itemState,
                                        event.oldItemState))
        return

    alerted = get_key_value(event.itemName, "Alert", "alerted") or "OFF"

    # If we were OFF (alerted == "ON") and we are now ON, or visa versa, alert.
    if str(event.itemState) == alerted:
        on_off_map = { ON: "online", OFF: "offline" }
        send_info("{} is now {}".format(get_name(event.itemName),
                                        on_off_map[items[event.itemName]]),
                  offline_alert.log)
        set_metadata(event.itemName, "Alert",
                    { "alerted": "OFF" if alerted == "ON" else "ON" },
                    overwrite=True)

    # Log a warning if the alerted state doesn't match the Item state.
    else:
        offline_alert.log.warn("Item {} changed to {} but alerted is {}, ignoring."
                               .format(event.itemName, event.itemState, alerted))

@rule("Offline Status Reminder",
      description=("Send a message with all the offline sensors at 08:00 and "
                   "at system start"),
      tags=["admin"])
@when("Time cron 0 0 8 * * ?")
@when("System started")
def offline_status(event):

    nullItems = [i for i in ir.getItem("gSensorStatus").members
                 if isinstance(i.state, UnDefType)]
    nullList = ", ".join(["{}".format(get_name(s.name)) for s in nullItems])

    offItems = [i for i in ir.getItem("gSensorStatus").members
                if i.state == OFF]
    offList = ", ".join(["{}".format(get_name(s.name)) for s in offItems])

    if len(nullItems) == 0 and len(offItems) == 0:
        offline_status.log.info("All sensors are online")

    msg = ""

    if len(nullItems) > 0:
        msg = "The following sensors are in an unknown state: {}".format(nullList)
    if len(offItems) > 0:
        msg = ("{}{}The following sensors are known to be offline: {}"
               .format(msg, "" if len(msg) == 0 else "\n", offList))
        [set_metadata(s.name, "Alert", {"alerted" : "ON"}, overwrite=True)
         for s in offItems]

        send_info(msg, offline_status.log)

personal.util.send_info and personal.util.get_name are functions that implement Design Pattern: Separation of Behaviors and Human Readable Names (link above) respectively. I also posted the rule that runs every day with an alert summarizing the current status of all the devices if there are any in an unknown state or are offline. Instead of tracking a flag in a Map, I set metadata on the Item with the alerted flag.

What is not shown is for some of these offline sensors there can be times when it freaks out and starts flapping on/off/on/off lots of times in a second. To filter those out I use debounce from https://github.com/rkoshak/openhab-rules-tools which, when installed, lets you apply a debounce to an Item just by defining Item metadata.

Contact vNetwork_cerberos "cerberos Network [MAP(admin.map):%s]"
  <network> (gSensorStatus, gResetExpire)
  { expire="2m",
    name="cerberos" }

Contact vNetwork_cerberos_Raw
  { channel="network:servicedevice:cerberos:online",
    debounce="vNetwork_cerberos"[timeout="1s", states="OPEN,CLOSED"] }

The config above will update vNetwork_cerberos only when it remains in an OPEN or CLOSED state for a full second (UNDEF and NULL are forwarded to vNetwork_cerberos immediately).

Note, the expire config above is using openhab-rules-tools’ drop in replacement for Expire 1.x binding.

2 Likes

Love it! I figured I could use a group but did not know how to then extract the details of the one (or several) that caused the alarm.

Thanks for the detailed write up. For me this response is perfect as it helps my understanding of both the required Syntax (to do various things) and the Concepts of what can be done. Next is to implement something alone these lines in my setup :slight_smile:

So here is my next version that:

  • Has one common rule for sending the Alarms regardless of which item triggers them (with Details)
  • Uses a String Item to hold the status of each Alarm item as a part of an Alarm Group
  • Allows you to “Disable, Enable, turn ON / OFF” the alarm from a sitemap
  • Uses custom icons to show on a sitemap if the Alarm is ON (Red), OFF (Green), Enabled but Undefined (Grey), or Disabled (Crossed Out)

Here is an example of the devices I’ve so far setup Alarms & Notifications for:

Alarm & Notifiction Dummy Items

/* Alarm & Notification Items */
String ZWave_Node20_SN_Alarm <my_alarm> (Alarms)
String Freezer_Temp_Alarm <my_alarm> (Alarms)
String ats_Status_Selected_Source_Alarm <my_alarm> (Alarms)
String ups_smart_capacity_Alarm <my_alarm> (Alarms)

Related Items

Number ZWave_Node20_SN "SN [%.1f]" <button> {channel="zwave:device:cc323ca9:node20:scene_number"}
Number Freezer_Temp "Freezer Temperature [%.1f °C]" <temperature> { channel="zwave:device:cc323ca9:node21:sensor_temperature" }
String ats_Status_Selected_Source "ATS Selected Power Source" { channel="snmp:target:cd1869ad:ats_Status_Selected_Source" }
String ups_smart_capacity "Capacity [%d %%]" <battery> { channel="snmp:target:ee7f160e:ups_smart_capacity" }

Sitemap (note: you could remove the mappings for ON & OFF if you did not want manual control)

Switch item=ups_smart_capacity_Alarm label="UPS Alarm" mappings=[DISABLE="DISABLE",ENABLED="ENABLE",OFF="OFF",ON="ON"]
Switch item=Freezer_Temp_Alarm label="Freezer Alarm" mappings=[DISABLED="DISABLE",ENABLED="ENABLE",OFF="OFF",ON="ON"]
Switch item=ats_Status_Selected_Source_Alarm label="ATS Alarm" mappings=[DISABLE="DISABLE",ENABLED="ENABLE",OFF="OFF",ON="ON"]
Switch item=ZWave_Node20_SN_Alarm label="Test Alarm" mappings=[DISABLED="DISABLE",ENABLED="ENABLE",OFF="OFF",ON="ON"]

Rules

import org.eclipse.smarthome.model.script.ScriptServiceUtil

rule "Test Alarm"  // This is just an Alarm Item for testing
when
	Item ZWave_Node20_SN received update
then
	if (ZWave_Node20_SN_Alarm.state != "DISABLED") {  // Check to see if the Alarm is Disabled
		if (ZWave_Node20_SN.state ==4.0)  // Button 4 Pressed
			postUpdate(ZWave_Node20_SN_Alarm, "ON")  // Toggle ON the Alarm
		else
			postUpdate(ZWave_Node20_SN_Alarm, "OFF")  // Toggle OFF the Alarm
	}
end

rule "Freezer Temp Alarm"
when
	Item Freezer_Temp changed
then
	if (Freezer_Temp_Alarm.state != "DISABLED") {  // Check to see if the Alarm is Disabled
		if (Freezer_Temp.state > -10)
			postUpdate(Freezer_Temp_Alarm, "ON")  // Toggle ON the Alarm
		else
			postUpdate(Freezer_Temp_Alarm, "OFF")  // Toggle OFF the Alarm
	}
end

rule "ATS Offline" // Test for if the ATS becomes unreachable from the SNMP Binding but leaves the Items in their previous state
when
	Thing "snmp:target:cd1869ad" changed from ONLINE to OFFLINE
then
	ats_Status_Selected_Source.postUpdate(UNDEF)  // The SNMP binding keeps the previous values so they need to be reset
	atsInputVoltage1.postUpdate(UNDEF)
	atsInputVoltage2.postUpdate(UNDEF)
	if (ats_Status_Selected_Source_Alarm.state != "DISABLED")  // Check to see if the Alarm is Disabled
		postUpdate(ats_Status_Selected_Source_Alarm, "ON")  // Toggle ON the Alarm
end

rule "ATS Online (selected source is not UNDEF" // The SNMP Binding Flops to Online breifly at times so test for a valid source power
when
	Item ats_Status_Selected_Source changed from UNDEF
then
	if (ats_Status_Selected_Source_Alarm.state != "DISABLED")  // Check to see if the Alarm is Disabled
		postUpdate(ats_Status_Selected_Source_Alarm, "OFF")  // Toggle OFF the Alarm
end

rule "UPS Battery Level under 95%"
when
	Item ups_smart_capacity changed
then
	if (ups_smart_capacity_Alarm.state != "DISABLED") {  // Check to see if the Alarm is Disabled
		val vCap = Integer::parseInt(ups_smart_capacity.state.toString)
		if ( vCap < 95 ) // Don't Alert unless under 95%
			postUpdate(ups_smart_capacity_Alarm, "ON")  // Toggle ON the Alarm
		else
			postUpdate(ups_smart_capacity_Alarm, "OFF")  // Toggle OFF the Alarm
	}
end

rule "Home Power Fail"
when
	Item myHousePower changed
then
	if (myHousePower_Alarm.state != "DISABLED") {  // Check to see if the Alarm is Disabled
		if ( myHousePower.state === UNDEF || myHousePower.state < 100 )
			postUpdate(myHousePower_Alarm, "ON")  // Toggle ON the Alarm
		else
			postUpdate(myHousePower_Alarm, "OFF")  // Toggle OFF the Alarm
	}
end

rule "Alarm Alerting"
when
	Member of Alarms changed to "ON" or //  Send a Notification with Details of what Device set the Alarm ON or
    Member of Alarms changed from "ON" to "OFF" // If already in the ON state to OFF (but not UNDEF to OFF etc)

then
	val device = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name.replace("_Alarm",""))  // This gets the item details with the same name as the alarm
	logInfo("notifications", "Alarm for " + device.label + " changed to " + triggeringItem.state + ", and the current value is " + device.state)
	.....
end

I’ve also attached the custom icons and a Pic of what it looks like on my sitemap.

Any suggestions would be welcome as I’m sure there is still much to improve and I’ve a bunch more to setup.

Thanks
Nathan

my_alarm my_alarm-disabled my_alarm-off my_alarm-on
Alarm Names