Design Pattern: Human Readable Names in Messages

designpattern
Tags: #<Tag:0x00007f0152bda100>

(Rich Koshak) #1

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

Rules Code

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

admin.map

MyItem=My Human Redable Item's Name

Complex Example

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 !!!

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)
  { channel="network:device:cerberos:online", expire="2m" }
  
Switch vNetwork_Cerberos_Alerted (gOfflineAlerted)

Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
    <network> (gSensorStatus)
    { mqtt="<[mosquitto:status/sensor-reporters:command:OFF:.*cerberos sensorReporter is dead.*],<[mosquitto:status/cerberos/heartbeat/string:command:ON]", expire="11m,command=OFF" }    

Switch vCerberos_SensorReporter_Online_Alerted (gOfflineAlerted)

// lots more Item pairs

Rules

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) 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 

admin.map

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

vNetwork_Cerberos=cerberos
vCerberos_SensorReporter_Online=cerberos sensorReporter

# one entry per Online Item

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, 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 doesn’t match the alerted state it means the sensor changed state and we haven’t alerted yet on that change. So we create a Timer to help detect and filter out flapping. 15 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.

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).

Mqtt connection status
How to make rules easy?
Inactivity of items // no updates // automatically tracking
Working with groups and Stringbuilder
[SOLVED] Openhab 2 Contact Sensor Time Rule
What are your top 3 automations
Adding custom Java code
Use variable for item
(Check) Rule transition help
Include Itemlabel in notice string
Rule return wrong item
How to find out if binding stopped working?
Rule optimization: Window OPEN reminder
Squeezebox Player WIP (Help Appreciated)
Iterating over a group, want to check an alternate item, sometimes
Generic notification rule
Rules executing from startup state restore or ZWave item population
Cancel timer doesn`t work
Hue lights misbehaving
Best way to create outbound mqtt message with json content?
HowTo: Get notified on empty batteries
Presence rule with reed switch state & time
Trouble with Shutter rules since OH 2.3
How can item metadata be accessed in rules?
A simple scene management: finally a good use of scripts
Transform String „OFF“ to Switch OFF
Possible to use transform in a rule?
Help with RPI Openhab full hang. (WAS Rule for longest ever uptime?)
Structures or Objects to group Items?
Transformation mapping of value with sendmail
(Ben Jones) #2

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.


(Rich Koshak) #3

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!


(Ben Jones) #4

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).


(steve1) #5

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”?


(Rich Koshak) #6

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.


(Udo Hartmann) #7

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


(Scott Rushworth) #8

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


(E. Gerland) #9

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


(Vincent Regaud) #10

@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?


(E. Gerland) #11

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.