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
There will be times when one has data that is associated with a specific Item. This data could be configuration data (e.g. the brightness for a light at certain times of day). It might be a flag indicating that some activity has occurred on that Item or needs to be done on that Item. It might be a different version of the Item’s name for use in logs or alert messages.
Concept
Relatively recently (OH 2.4, maybe earlier) OH has added the ability to set metadata on an Item. The metadata can be set statically or dynamically defined. When it is statically defined you either need to apply it to the Item using the REST API or in your .items file. Dynamic definition can occur from Rules.
Sorry Rules DSL folks, there is no way to access and modify metadata from Rules.
NOTE: if metadata is defined in .items file, all other metadata will be removed upon a reload of the .items file.
Metadata consists of three main parts:
- Namespace: the root category, can contain multiple Values
- Value: Single value for the namespace
- Key/value pairs: an array of keys and their values
Here is an example.
Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
<network> (gSensorStatus, gResetExpire, sensorReporters)
{ expire="2m,command=OFF",
Static="meta"[name="cerberos sensorReporter", timeout=10] }
In the above:
-
Static
= Namespace -
"meta"
= Value -
[name="cerberos sensorReporter"]
= array of key/value pairs, only one pair in this example
If you only have one value for a given namespace, you can just define a value. If you have multiple key value pairs, you must define both the value and the list of key value pairs as shown in the example above.
A version of that Item with just the value defined would be
Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
<network> (gSensorStatus, gResetExpire, sensorReporters)
{ expire="2m,command=OFF",
name="cerberos sensorReporter" }
If just defining a single value, you can only store Strings. When using the list of key value pairs, you can use numbers as well. In Python, you will get a Java Number when you extract the key value so if you need a primitive int, call .intValue()
.
See https://openhab-scripters.github.io/openhab-helper-libraries/Python/Core/Packages%20and%20Modules/metadata.html for a list of all of the metadata functions supported by the JSR223 Helper Libraries.
Python Example
See Design Pattern: Human Readable Names in Messages for the theory of operation of these Rules and Items and the Rules DSL equivalent. The tl;dr is when a service or device goes offline the Rule generates an alert. When an alert is generated we set a flag indicating we have sent the alert. We also use that DP to convert the Item name to something a little more human friendly.
We will replace the Human Readable Names in Messages DP part and the Design Pattern: Associated Items with metadata on the Item.
Items
Group:Switch:AND(ON, OFF) gSensorStatus "Sensor's Status [MAP(admin.map):%s]"
<network>
Group:Switch gOfflineAlerted
Switch vNetwork_cerberos "cerberos Network [MAP(admin.map):%s]"
<network> (gSensorStatus, gResetExpire)
{ channel="network:servicedevice:cerberos:online",
expire="2m",
name="cerberos" }
Switch vCerberos_SensorReporter_Online "Cerberos sensorReporter [MAP(admin.map):%s]"
<network> (gSensorStatus, gResetExpire, sensorReporters)
{ expire="2m,command=OFF",
name="cerberos sensorReporter" }
Notice how we are defining the human friendly name for the Item using statically defined metadata. Also notice how we do not have the associated _Alerted Item defined. We will be using metadata instead. For completeness, the Alerted metadata uses a full namespace,"value",["key":value]
but since it’s only one value for the namespace a simple value would have worked.
Python
from core.rules import rule
from core.triggers import when
from core.metadata import get_value, get_key_value, set_metadata
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:
send_info("{} is now {}".format(name, Transformation.transform("MAP", "admin.map", str(items[itemName]))), status_alert.log)
set_metadata(itemName, "Alert", { "alerted" : "ON"}, overwrite=False)
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 = get_key_value(event.itemName, "Alert", "alerted") or "OFF"
name = get_value(event.itemName, "name") 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()
Theory of Operation
When a device changes it’s online status we make sure it isn’t NULL or UNDEF. If it isn’t we then extract the alerted flag and the human friendly name from the metadata. Notice how we use a default value for both if they don’t exist.
If a timer exists we cancel it and log that the device is flapping.
If not and the alerted flag matches the Item’s new state we may need to send an alert. Create a timer.
If the Timer wasn’t cancelled, we generate the alert message and set the alerted flag to “ON” in the metadata.
Focusing on the metadata we have two namespaces we are working with. One, Static
has the human friendly name for the Item which we use to produce nice and easy to read alerts and log messages. The second is the alerted flag which we use to keep track of whether we have generated an alert when the device went offline so we know to generate an alert when the device goes back online.
Previously, the name mapping was stored in a .map file and the transform Action used to get the Item’s human friendly name. Similarly, the alerted flag was a separate Item which we accessed and updated using the Associated Item DP.
Advantages and Disadvantages
Advantages:
- allows for a reduction in Items to store configuration data and flags
- lets one store metadata and configuration data with the Item reducing the number of places that this sort of information is defined
- if using dynamically created metadata, the metadata is restored on OH restart (only for Items stored in JSONDB)
Disadvantages:
- metadata is not accessible in Rules DSL
- the REST API endpoints for metadata are not very robust making access and setting metadata through that mechanism awkward
- PaperUI has no support for metadata
Related Design Patterns
Design Pattern | How Used |
---|---|
Design Pattern: Human Readable Names in Messages | The above is an alternative approach to this DP |
Design Pattern: Associated Items | In some cases, the above is an alternative approach to this DP |
Edit: Added a bit more discussion about the metadata format and how it can be used.