In the Openhab rule language, it can be a challenge to code rules that work in a generic fashion, without coding for each item individually. Since OH 2.3, the triggeringItem variable makes it easier to write generic rules, which work on multiple items without the need to hardcode each item name into the rule.
As an example, here is a rule which you might like, but my son might hate: Volume limiting for any music player. Neither the binding for the player, nor the player itself offers any functionality for limiting volume. Thus I coded it in a generic fashion. A single rule can add functionality to multiple things, by just defining a custom item for each thing as needed.
I define an extra āvolume limitā item for my player thing, and then use a generic rule to define its functionality.
I started off by defining a dummy _Limit item in my .item definitions, and grouping all the _Volume and _Limit items in a group as such (for all my players):
Group:Dimmer gVolumeLimit "Group used to automate volume limiting"
Dimmer SonosKinderzimmer_Volume "Volume" (gVolumeLimit) {channel="sonos:PLAY5:RINCON_000E5850034201400:volume"}
Dimmer SonosKinderzimmer_Limit "Maximum Volume" (gVolumeLimit)
The group āgVolumeLimitā is what I will use as a trigger for my rule:
rule "Enforce Maximum Volume"
//This rule uses dummy items and a group for volume automation
when
Member of gVolumeLimit changed
then
//first, we fill the items we need to use to adjust volume
//we cut all possible endings in our group off the item name
var itemName = triggeringItem.name.replaceFirst( "(_Volume|_Limit)$", "")
//filter the group by the shortened name and the needed ending to construct the needed items
var volumeItem = gVolumeLimit.members.filter[ itm|itm.name == itemName + "_Volume" ].head
var limitItem = gVolumeLimit.members.filter[ itm|itm.name == itemName + "_Limit" ].head
//stop with a warning if something is wrong with the items we want to use
if (limitItem.state == NULL || limitItem.state == UNDEF) {
logWarn("VolLimit", "The limit Item is " + limitItem.state.toString + ", cannot set the volume limit")
return;
}
if (volumeItem.state == NULL || volumeItem.state == UNDEF) {
logWarn("VolLimit", "The volume Item is " + volumeItem.state.toString + ", nothing to do since the volume isn't actually set")
return;
}
//here we do something useful to a real item with the help of our self-made item
if ( volumeItem.state > limitItem.state ) {
volumeItem.sendCommand( limitItem.state )
}
end
This rule will trigger on any item change in the gVolumeLimit group, which contains all the volume items and volume limit items in the system. Thus, whenever someone changes a volume or a volume limit, this rule will run and check whether the volume is not too high.
when
Member of gVolumeLimit changed
then
The code first constructs the names of the items we need to work with, and then uses a lambda to filter out those items from the item group we also use as a trigger (containing all _Volume and _Limit items). The part ā(_Volume|_Limit)$ā is a regular expression, which is a special search syntax. Between the brackets are the search terms, separated by an āORā | operator. The dollar sign is a placeholder for the end of a string, making sure that it will only find occurences at the end of the string, and not in the middle.
//we cut all possible endings in our group off the item name
var itemName = triggeringItem.name.replaceFirst( "(_Volume|_Limit)$", "")
//filter the group by the shortened name and the needed ending to construct the needed items
var volumeItem = gVolumeLimit.members.filter[ itm|itm.name == itemName + "_Volume" ].head
var limitItem = gVolumeLimit.members.filter[ itm|itm.name == itemName + "_Limit" ].head
Then there is an error check, because the search for items might come up empty if the item name has a typo or when the corresponding item is not part of the group. An item might also not be initialized by the binding yet.
if (limitItem.state == NULL || limitItem.state == UNDEF) {
logWarn("VolLimit", "The limit Item is " + limitItem.state.toString + ", cannot set the volume limit")
return;
}
if (volumeItem.state == NULL || volumeItem.state == UNDEF) {
logWarn("VolLimit", "The volume Item is " + volumeItem.state.toString + ", nothing to do since the volume isn't actually set")
return;
}
The actual logic after that is simple: If the volume is higher than the limit, then adjust volume to match the limit.
if ( volumeItem.state > limitItem.state ) {
volumeItem.sendCommand( limitItem.state )
In order to define an initial/static value for the limit, you might add this to your rules:
rule "Populate Volume Limits"
when
System started
then
if (SonosKinderzimmer_Limit.state === null) {
SonosKinderzimmer_Limit.postUpdate(25)
}
end
It is also possible to add a volume limit slider to your user interface.
As another example of this technique, here is a more complicated rule, where I add a color channel to all kinds of lamps, so that my automation does not need to adjust its commands to the kind of lamp it wants to control:
Thanks to @rlkoshak and @Udo_Hartmann for their input making this pattern better!