Design Pattern: Human Readable Names in Messages

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for details on what a DP is and how to use them.

Problem Statement

Sometimes one wants to create a message or log statement that references certain Items. A clear example of this is to generate an alert message to be sent when one or more sensors change state. We have a nice way to provide a human readable version of an Item’s name on the UIs (i.e. the label) but from a rule this becomes more difficult.

Concept


Create a .map file in the transform folder that maps your Item names to a human-friendly version of that Item’s name. Then use the transform action to convert the Item’s name to this friendlier name.

Simple Example using MAP

admin.map

MyItem=My Human Redable Item's Name

JSR223 Python

from core.actions import Transformation
...
name = Transformation.transform("MAP", "admin.map", event.itemName) or event.itemName

Rules DSL

var name = transform("MAP", "admin.map", MyItem.name)
if(name == "") name = MyItem.name // in case MyItem isn't in the map file

Complex Example using MAP

This example comes from my currently running rules. I won’t include the full set of Items and just focus on the Rules as that is where the design pattern is used. Look for the !!!

admin.map

ON=online
OFF=offline
NULL=unknown
-=unknown

vNetwork_Cerberos=cerberos
vCerberos_SensorReporter_Online=cerberos sensorReporter

# one entry per Online Item Name

Items

Group:Switch:AND(ON, OFF) gSensorStatus "Sensor's Status [MAP(admin.map):%s]"
  <network>
  
Group:Switch gOfflineAlerted

// Example Online and Alerted Items
Switch vNetwork_cerberos "cerberos Network [MAP(admin.map):%s]"
  <network> (gSensorStatus, gResetExpire)
  { channel="network:servicedevice:cerberos:online",
    expire="2m" }

Switch vNetwork_cerberos_Alerted (gOfflineAlerted)

Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
    <network> (gSensorStatus, gResetExpire, sensorReporters)
    { expire="2m,command=OFF" }

Switch vCerberos_SensorReporter_Online_Alerted (gOfflineAlerted)
// lots more Item pairs

JSR223 Python

from core.rules import rule
from core.triggers import when
import personal.util
reload(personal.util)
from personal.util import send_info
from threading import Timer
from core.actions import Transformation

# Python Timers for online alerts
alertTimers = {}

def alert_timer_expired(itemName, name, origState):
    status_alert.log.debug("Status alert timer expired for {} {} {}".format(name, origState, items[itemName]))
    del alertTimers[itemName]
    if items[itemName] == origState:
        # !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
        send_info("{} is now {}".format(name, Transformation.transform("MAP", "admin.map", str(items[itemName])) or origState), status_alert.log)
        events.postUpdate(itemName+"_Alerted", "ON")
    else:
        status_alert.log.warn("{} is flapping!".format(itemName))

@rule("Device online/offline", description="A device we track it's online/offline status changed state", tags=["admin"])
@when("Member of gSensorStatus changed")
def status_alert(event):
    status_alert.log.info("Status alert for {} changed to {}".format(event.itemName, event.itemState))

    if isinstance(event.oldItemState, UnDefType):
        return

    alerted = items[event.itemName+"_Alerted"] or "OFF"

    # !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
    name = Transformation.transform("MAP", "admin.map", event.itemName) or event.itemName

    #If the Timer exists and the sensor changed the sensor is flapping, cancel the Timer
    if event.itemName in alertTimers:
        alertTimers[event.itemName].cancel()
        del alertTimers[event.itemName]
        status_alert.log.warning(name +  " is flapping!")
        return

    '''
    If alerted == "OFF" and event.itemName == OFF than sensor went offline and we have not yet alerted
    If alerted == "ON" and event.itemName == ON then the sensor came back online after we alerted that
    it was offline
    '''
    status_alert.log.debug("Looking to see if we need to create a Timer: {} {}".format(alerted, event.itemState))
    if alerted == str(event.itemState):
        # Wait one minute before alerting to make sure it isn't flapping
        alertTimers[event.itemName] = Timer(60, lambda: alert_timer_expired(event.itemName, name, event.itemState))
        alertTimers[event.itemName].start()

@rule("System status reminder", description="Send a message with a list of offline sensors at 08:00 and System start", tags=["admin"])
@when("0 0 8 * * ? *")
@when("System started")
def status_reminder(event):
    status_reminder.log.info("Generating daily sensor status report")

    numNull = len(filter(lambda item: isinstance(item.state, UnDefType), ir.getItem("gSensorStatus").members))
    if numNull > 0: status_reminder.log.warning("There are {} sensors in an unknown state!".format(numNull))

    offline = filter(lambda item: item.state == OFF, ir.getItem("gSensorStatus").members)
    if len(offline) == 0:
        status_reminder.log.info("All sensors are online")
        return

    status_reminder.log.info("Building message")

    # !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
    name = Transformation.transform("MAP", "admin.map", event.itemName) or event.itemName

    offlineMessage = "The following sensors are known to be offline: {}".format(", ".join(map(lambda sensor: "{}".format(name), sorted(sensor for sensor in offline))))

    status_reminder.log.info("Updating all of the alerted flags")
    for sensor in offline: events.postUpdate(sensor.name+"_Alerted", "ON")    status_reminder.log.info("Sending the message")
    send_info(offlineMessage, status_reminder.log)

Rules DSL

import java.util.Map

val Map<String, Timer> timers = newHashMap

rule "A sensor changed its online state2"
when
    Member of gSensorStatus changed
then
  if(previousState == NULL || previousState == UNDEF) return;

  val alerted = gOfflineAlerted.members.findFirst[ a | a.name == triggeringItem.name+"_Alerted"] as SwitchItem
  if(alerted === null) {
    logError("admin", "Cannot find Item " + triggeringItem.name+"_Alerted")
    aInfo.sendCommand(triggeringItem.name + " doesn't have an alerted flag, it is now " + transform("MAP", "admin.map", triggeringItem.state.toString) + "!")
    return;
  }

  if(alerted.state == triggeringItem.state && timers.get(triggeringItem.name) === null) {
    val currState = triggeringItem.state
    timers.put(triggeringItem.name, createTimer(now.plusSeconds(15), [ |
      if(triggeringItem.state == currState) {
        // !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
        var name = transform("MAP", "admin.map", triggeringItem.name)
        if(name == "") name = triggeringItem.name
        aInfo.sendCommand(name + " is now " + transform("MAP", "admin.map", triggeringItem.state.toString) + "!")
        // !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
        alerted.postUpdate(if(triggeringItem.state == ON) OFF else ON)
      }
      timers.put(triggeringItem.name, null)
    ]))
  }
end

rule "Reminder at 08:00 and system start"
when
	Time cron "0 0 8 * * ? *" or
	System started
then
  val numNull = gSensorStatus.members.filter[ sensor | sensor.state == NULL ].size
  if( numNull > 0) logWarn("admin", "There are " + numNull + " sensors in an unknown state")

  val offline = gSensorStatus.members.filter[ sensor | sensor.state == OFF ]
  if(offline.size == 0) return;

  val message = new StringBuilder 
  message.append("The following sensors are known to be offline: ")
  offline.forEach[ sensor |
    // !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
    var name = transform("MAP", "admin.map", sensor.name)
    if(name == "") name = sensor.name
    // !!!!!!!!!!!   This Design Pattern's Implementation !!!!!!!!!!!
    message.append(name)
    message.append(", ")
    gOfflineAlerted.members.filter[ a | a.name==sensor.name+"_Alerted" ].head.postUpdate(ON)
  ]
  message.delete(message.length-2, message.length)

  aInfo.sendCommand(message.toString)
end 

Theory of Operation

This example provides a generic implementation to detect and send an alert when a sensor stops reporting for a certain amount of time.

I have one Online Item per sensor that gets commanded ON every time the sensor reports and the Expire binding sets it to OFF when the sensor doesn’t report for too long. All the Online Items are members of the gSensorStatus group.

An Associated Item is used to keep track of whether or not an alert has been sent for that sensor going offline so we know when OH comes back online whether or not the sensor went offline prior to OH going offline.

When a member of gSensorStatus changes we first check to see if it changed from NULL or UNDEF, in which case we don’t care and immediately return. Then we use Associated Item to access the Associated Alerted Item to see if we have already alerted. It there is no Associated Item, we log and send an alert.

Now, if the Item’s state matches the alerted state it means the sensor changed state and we have alerted on that change. So we create a Timer to help detect and filter out flapping. For some seconds after the last change to the sensor we generate the alert using this DP to create the message.

The second rule simply generates an alert message at system startup and 08:00 with a list of all the sensors that are offline as a reminder.

Complex Example using Item Metadata

This is only an option for JSR223 Rules as Rules DSL does not provide a mechanism to access the metadata of an Item. See Design Pattern: Using Item Metadata as an Alternative to Several DPs for an example of the above complex example using metadata.

Advantages and Disadvantages

Advantages

  • You get nice human readable messages in your alerts and your logs
  • Rules do not require Maps or other data structures to map between the Item names and a nice version of the name
  • No logic required to parse out the Item’s name to remove the _ in place of spaces or something like that

Disadvantages

  • You have to separately maintain a .map file with your Item name to nice name mappings

Related Design Patterns

Design Pattern How Used
Design Pattern: How to Structure a Rule Overall structure of the first complex rule follows this DP
Unbound Item The Assocaited Alerted Item
Assocaited Items Gets the Alerted Item based on the Online Item’s name
Working with Groups in Rules Get the Items around the time the rule was triggered, loop through those Items
Separation of Behaviors How the alerting is implemented in the complex example
Generic Is Alive The complex example is a simplified version of this DP, Generic Is Alive works in more circumstances (i.e. when one has sensors with a binding that doesn’t allow transformations).
Design Pattern: Using Item Metadata as an Alternative to Several DPs Implementation of this DP using Metadata
5 Likes

Just thinking out loud here, but would a new displayname binding make this easier perhaps? So the idea (and this hasn’t been thought thru I am just brain dumping here!) would be that you specify the display name in the item definition via this new binding;

Switch vNetwork_Cerberos "Cerberos Network [MAP(admin.map):%s]" <network> (gSensorStatus) { channel="network:device:cerberos:online", expire="2m", displayname="cerberos" }

The displayname binding is very simple and generates a mapping file in the same format as your manual one above. So adds an entry for each item name --> display name. It is updated whenever an item definition is loaded/updated.

It has a single configuration option - the name of the transform file to generate - defaulting to displayname.map. Then you use them in exactly the same way as you do now, via the transform action. I guess you could add a displayname action which wraps transform and looks up the mapping filename from the displayname config to simplify this even more?

This has the benefit of all item metadata living in the one place. As I say, just an idea that popped into my head while I was reading this post. Great work as usual @rlkoshak.

That could work.

But this doesn’t feel right. For one, it would only work for those who are using .items files and that population is growing smaller by the day. I’m not sure how that would fit into an OH 2 paradigm. But even besides that, it doesn’t really feel like something a binding ought to be doing.

I think the real solution would be for this to be added to the core. Though I’m not sure this use case warrants that. It would be a pretty big change and the use case is rather nitch.

I’m certainly open to ideas and I won’t stop anyone from trying to implement something. I’ve no time to code something up myself.

Maybe there is something that can be done with tags…

Keep the ideas coming!

Yep - to be fair I am still in the dark ages with OH 1.8 and too time poor to invest in upgrading at the moment. I tend to agree that it should probably be in core but I believe this has been discussed before and there were valid reasons for it not to be (not that I can recall what they were).

Just FYI, the JSR223 rules allow access to item labels. Does the Rule DSL not provide access to the item label? Since I don’t use the Rule DSL I wasn’t sure if “becomes more difficult” meant “isn’t possible”?

It does but then you need to parse out the state formatting and the label is not always, and perhaps not even frequently (which is my case) the same thing as what I would want to use in this context.

By more difficult I mainly mean a bunch of tedious code.

I’m curious if Item Metadata would be an option to define a display name?

Do you know how the metadata can be accessed through the rules DSL? I was asking about this here…

Just a short one:
Shouldn’t this be:
if(previousState === NULL) return;

@NCO
No,
There is a difference between NULL an openHAB state and null a Java “value”
The NULL state is when an item state is not defined (at startup for example)
The Java null denotes a variable whose value (and/or type) has not been defined.

So, because previousState is an OH state we use previousState == NULL
But to check if a timer is null for example we use timer === null

Does that make sense?

2 Likes

Hi Vincent,

it does make sense.
I struggled a lot when dealing with json returns of null and handling (blocking) startup messages when OH states have been changing from NULL to ON for instance.
Now I know why :slight_smile:

Thanks for this clarification.