Design Pattern: Add custom functionality to things by using groups and triggeringItem

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!

9 Likes

Great examle. Thanks for sharing.

Arno,
I am trying to get your volume limiting rule to work on my Amazon Echo Dots. I am having problems getting it working though. In the logs I get this message when I try to raise the volume:

Rule 'Enforce Maximum Volume': An error occurred during the script execution: Could not invoke method: org.eclipse.smarthome.model.script.lib.NumberExtensions.operator_greaterThan(org.eclipse.smarthome.core.types.Type,java.lang.Number) on instance: null

I get this error message on various lines of the rule in Visual Studio also. It is:
Type mismatch: cannot convert from Item to DimmerItem

Here is the rule:

 var DimmerItem volumeItem = null
 var DimmerItem limitItem = null
 var String itemName = null


  rule "Enforce Maximum Volume"
  when 
        Member of gVolumeLimit changed
  then 
       if ( triggeringItem.name.endsWith("_Volume") ) {   
          volumeItem = triggeringItem
          itemName = triggeringItem.name + "_Limit"
          limitItem = gVolumeLimit.members.filter[ item|item.name == itemName ].head
       }
          else if  ( triggeringItem.name.endsWith("_Volume_Limit") ) { 
            limitItem = triggeringItem
            itemName = triggeringItem.name.replace( "_Volume_Limit", "_Volume")
            volumeItem = gVolumeLimit.members.filter[ item|item.name == itemName ].head
          }

               if (( limitItem === null ) || (volumeItem === null) ){ //abort on items not found
               logWarn("VolLimit", "A corresponding item for volume limiting with " + triggeringItem.name + " could not be found")
               return;
                 }
                    if ( volumeItem.state > limitItem.state ) {
                         volumeItem.sendCommand( limitItem.state )
                    }
        end

Can you see what I’m doing wrong. Thanks for any help.

This seems to be a type mismatch. The filter is finding items with the type “Item” whereas the variables at the beginning of the rule are defined as type “DimmerItem”. As they are different types, Visual Studio is complaining that you are trying to copy something into a type that doesn’t fit.

Apparently, it does give it a try, but the field “state” is not available for an “Item”, so eventually it fails when you are trying to do a “greater than” on a non-existent state.

Have a look here, the eclipse object reference is very helpful for solving these kinds of things:
Item (doesn’t have a “state”)
DimmerItem (does have a “state”)

Are you sure your items are defined right? In the Echo control binding, the volume should also be defined as a Dimmer

Dimmer Echo_Living_Room_Volume "Volume [%.0f %%]" <soundvolume>  (gVolumeLimit) {channel="amazonechocontrol:echo:account1:echo1:volume"}

Your corresponding volume limit item should look like:

Dimmer Echo_Living_Room_Volume_Limit  "Volume Limit [%.0f %%]" (gVolumeLimit)

Also check for typos in your item definition. Make sure there are no items in the group gVolumeLimit that are not a dimmer. If you want to use the code as a template to work with other types of items, you need to adjust the variable definitions at the top of the rule to match the kind of item you are working with.

I’m pretty sure you have to set both vars volumeItem and limitItem to null at start of the rule. Defining them inside the rule would suffice anyway. Don’t narrow down the var type to DimmerItem.

rule "Enforce Maximum Volume"
when
    Member of gVolumeLimit changed
then
    var volumeItem = null
    var limitItem = null
...

Thanks Arno and Udo for your suggestions. Unfortunately I still can’t get it working. I will play around with it a little more over the next couple of days to see if I can get it running.

The suggestion from Udo would make the visual studio warnings go away, but I don’t think they will solve the problem.

Somehow, when the rule searches for the items which belong together in the gVolumeLimit group, it seems to come up with a different kind of item than a dimmer. Right now, the code is written to only accept dimmers, (although a number would also work) with Udo’s modification it would be less picky, but would still fail on item types which do not have a “state” field, or who don’t implement the “greater than” method.

For instance, when I change my item definition to

String   SonosKinderzimmer_Volume_Limit          "Maximum Volume" (gVolumeLimit)

I get the same error as you have, because it needs a dimmer (or a number would also work), but it can’t work with a string.

As far as I understood, there are only Dimmer Items in the group, at least those which will match the filtering.

Would be interesting to get some additional logging:

rule "Enforce Maximum Volume"
when 
    Member of gVolumeLimit changed
then 
    var volumeItem = null
    var limitItem = null
    var String itemName
    logInfo("VolLimit", "triggeringItem = {}",triggeringItem.name)
    if(triggeringItem.name.endsWith("_Volume")) {
        volumeItem = triggeringItem
        logInfo("VolLimit", "volumeItem set = {}",volumeItem.name)
        itemName = triggeringItem.name + "_Limit"
        logInfo("VolLimit", "itemName set = {}, about to set limitItem",itemName)
        limitItem = gVolumeLimit.members.filter[ m|m.name == itemName ].head
        logInfo("VolLimit", "limitItem set = {}",limitItem.name)
    } else if(triggeringItem.name.endsWith("_Volume_Limit")) {
        limitItem = triggeringItem
        logInfo("VolLimit", "limitItem set = {}",limitItem.name)
        itemName = triggeringItem.name.replace( "_Volume_Limit", "_Volume")
        logInfo("VolLimit", "itemName set = {}, about to set volumeItem",itemName)
        volumeItem = gVolumeLimit.members.filter[ m|m.name == itemName ].head
        logInfo("VolLimit", "volumeItem set = {}",volumeItem.name)
    }
    if(limitItem === null || volumeItem === null) { //abort on items not found
        logWarn("VolLimit", "A corresponding item for volume limiting with " + triggeringItem.name + " could not be found")
        return;
    }
    if(volumeItem.state > limitItem.state) 
        volumeItem.sendCommand(limitItem.state)
end

Udo,
Thanks for helping me out! Here are my logs:

[vent.ItemStateChangedEvent] - Echo_Den_LastVoiceCommand changed from Unknown to turn volume up
[vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 30 to 40
[INFO ] [ipse.smarthome.model.script.VolLimit] - triggeringItem = Echo_Den_Volume
[vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxxxx_xxxxxxxxxxxxxxxx_volume changed from 30 to 40
[INFO ] [ipse.smarthome.model.script.VolLimit] - volumeItem set = Echo_Den_Volume
[INFO ] [ipse.smarthome.model.script.VolLimit] - itemName set = Echo_Den_Volume_Limit, about to set limitItem
[INFO ] [ipse.smarthome.model.script.VolLimit] - limitItem set = Echo_Den_Volume_Limit
[ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Enforce Maximum Volume': 'state' is not a member of 'null'; line 59, column 8, length 16
[vent.ItemStateChangedEvent] - Echo_Den_LastVoiceCommand changed from turn volume up to Unknown

I guess the problem is, that the item substitution with vars is not allowed. So the rule would be a bit different:

rule "Enforce Maximum Volume"
when
    Member of gVolumeLimit changed
then
    var String itemName
    if(triggeringItem.name.endsWith("_Volume")) {
        itemName = triggeringItem.name.replace( "_Volume", "")
    } else if(triggeringItem.name.endsWith("_Volume_Limit")) {
        itemName = triggeringItem.name.replace( "_Volume_Limit", "")
    }
    var Number volume = gVolumeLimit.members.filter[ m|m.name == itemName + "_Volume" ].head.state as Number
    var Number limit  = gVolumeLimit.members.filter[ m|m.name == itemName + "_Volume_Limit" ].head.state as Number
    if(volume > limit)
        gVolumeLimit.members.filter[ m|m.name == itemName + "_Volume" ].head.sendCommand(limit)
end

It kind of worked. It seemed to be doing a lot of extra work though. Also got error message.
Update: I have 5 Echo’s and 2 Sonos speakers. This works rule works great on the Sonos speakers, but for some reason the Echo speakers volume are getting adjusted multiple times. I did change the last lines in the rule to:
if(volume > limit) {
gVolumeLimit.members.filter[ m|m.name == itemName + “_Volume” ].head.sendCommand(limit)
}

[vent.ItemStateChangedEvent] - Echo_Den_LastVoiceCommand changed from stop to turn volume up
2019-01-15 17:56:40.932 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 50 to 60
2019-01-15 17:56:40.944 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxx_xxxxxxxxxxxxxx_volume changed from 50 to 60
2019-01-15 17:56:40.977 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:40.985 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:40.992 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 60 to 25
2019-01-15 17:56:41.017 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 25 to 60
2019-01-15 17:56:41.018 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:41.019 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:41.020 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 60 to 25
2019-01-15 17:56:41.033 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:41.036 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:41.197 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 25 to 60
2019-01-15 17:56:41.203 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:41.208 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:41.210 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 60 to 25
2019-01-15 17:56:41.259 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxx_xxxxxxxxxxxxx_volume changed from 60 to 25
2019-01-15 17:56:41.735 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 25 to 27
2019-01-15 17:56:41.736 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxx_xxxxxxxxxxxxx_volume changed from 25 to 27
2019-01-15 17:56:41.751 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:41.769 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:41.770 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 27 to 25
2019-01-15 17:56:41.956 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxxxxxx_volume changed from 27 to 25

==> /var/log/openhab2/openhab.log <==
2019-01-15 17:56:43.011 [ERROR] [nal.common.AbstractInvocationHandler] - An error occurred while calling method ‘ThingHandler.handleCommand()’ on ‘org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler@7b2615d5’: POST url ‘https://alexa.amazon.com/api/behaviors/preview’ failed: Bad Request
org.openhab.binding.amazonechocontrol.internal.HttpException: POST url ‘https://alexa.amazon.com/api/behaviors/preview’ failed: Bad Request
at org.openhab.binding.amazonechocontrol.internal.Connection.makeRequest(Connection.java:583) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.Connection.executeSequenceNode(Connection.java:1108) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.Connection.executeSequenceCommand(Connection.java:1093) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler.handleCommand(EchoHandler.java:355) ~[?:?]
at sun.reflect.GeneratedMethodAccessor148.invoke(Unknown Source) ~[?:?]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
at org.eclipse.smarthome.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:153) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at org.eclipse.smarthome.core.internal.common.InvocationHandlerSync.invoke(InvocationHandlerSync.java:59) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at com.sun.proxy.$Proxy152.handleCommand(Unknown Source) [273:org.openhab.binding.amazonechocontrol:2.5.0.Beta_01]
at org.eclipse.smarthome.core.thing.internal.profiles.ProfileCallbackImpl.handleCommand(ProfileCallbackImpl.java:75) [109:org.eclipse.smarthome.core.thing:0.10.0.oh240]
at org.eclipse.smarthome.core.thing.internal.profiles.SystemDefaultProfile.onCommandFromItem(SystemDefaultProfile.java:49) [109:org.eclipse.smarthome.core.thing:0.10.0.oh240]
at sun.reflect.GeneratedMethodAccessor147.invoke(Unknown Source) ~[?:?]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
at org.eclipse.smarthome.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:153) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at org.eclipse.smarthome.core.internal.common.Invocation.call(Invocation.java:53) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:?]
at java.lang.Thread.run(Thread.java:748) [?:?]
2019-01-15 17:56:43.775 [ERROR] [nal.common.AbstractInvocationHandler] - An error occurred while calling method ‘ThingHandler.handleCommand()’ on ‘org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler@7b2615d5’: POST url ‘https://alexa.amazon.com/api/behaviors/preview’ failed: Bad Request
org.openhab.binding.amazonechocontrol.internal.HttpException: POST url ‘https://alexa.amazon.com/api/behaviors/preview’ failed: Bad Request
at org.openhab.binding.amazonechocontrol.internal.Connection.makeRequest(Connection.java:583) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.Connection.executeSequenceNode(Connection.java:1108) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.Connection.executeSequenceCommand(Connection.java:1093) ~[?:?]
at org.openhab.binding.amazonechocontrol.internal.handler.EchoHandler.handleCommand(EchoHandler.java:355) ~[?:?]
at sun.reflect.GeneratedMethodAccessor148.invoke(Unknown Source) ~[?:?]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
at org.eclipse.smarthome.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:153) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at org.eclipse.smarthome.core.internal.common.InvocationHandlerSync.invoke(InvocationHandlerSync.java:59) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at com.sun.proxy.$Proxy152.handleCommand(Unknown Source) [273:org.openhab.binding.amazonechocontrol:2.5.0.Beta_01]
at org.eclipse.smarthome.core.thing.internal.profiles.ProfileCallbackImpl.handleCommand(ProfileCallbackImpl.java:75) [109:org.eclipse.smarthome.core.thing:0.10.0.oh240]
at org.eclipse.smarthome.core.thing.internal.profiles.SystemDefaultProfile.onCommandFromItem(SystemDefaultProfile.java:49) [109:org.eclipse.smarthome.core.thing:0.10.0.oh240]
at sun.reflect.GeneratedMethodAccessor147.invoke(Unknown Source) ~[?:?]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:?]
at org.eclipse.smarthome.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:153) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at org.eclipse.smarthome.core.internal.common.Invocation.call(Invocation.java:53) [102:org.eclipse.smarthome.core:0.10.0.oh240]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:?]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:?]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:?]
at java.lang.Thread.run(Thread.java:748) [?:?]

==> /var/log/openhab2/events.log <==
2019-01-15 17:56:46.753 [vent.ItemStateChangedEvent] - Echo_Den_LastVoiceCommand changed from turn volume up to Unknown
2019-01-15 17:56:46.791 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 25 to 27
2019-01-15 17:56:46.793 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxxxxx_xxxxxxxxxxx_volume changed from 25 to 27
2019-01-15 17:56:46.809 [ome.event.ItemCommandEvent] - Item ‘Echo_Den_Volume’ received command 25
2019-01-15 17:56:46.812 [nt.ItemStatePredictedEvent] - Echo_Den_Volume predicted to become 25
2019-01-15 17:56:46.823 [vent.ItemStateChangedEvent] - Echo_Den_Volume changed from 27 to 25
2019-01-15 17:56:47.162 [vent.ItemStateChangedEvent] - amazonechocontrol_echospot_xxxxxx_xxxxxxxxxxxx_volume changed from 27 to 25

@Udo_Hartmann It works for me as described though, as long as I use it with items that support “greater than” and that have a state. Your programming circumvents that, but part of the purpose of the design pattern for me was to end up with items to work with. Volume controlling was just a maximally simple use case to set an example.

I would like to invite other programmers to form an opinion, I wouldn’t want to promote something that has issues, but right now, I can’t really see where that problem might be. In the first post, I have linked to another piece of code, also pointing to items in vars, and that also works for me. (OH 2.4). Maybe @rlkoshak can take a look?

In your code, cleaning up the name can be more compact as follows:

itemName = triggeringItem.name.replaceFirst( "_Volume(_Limit)?$", "")

Actually I like Udo’s way of constructing the item names better than mine, I might change my code to include it as follows (slightly easier to understand regex)

//shorten item name by the endings we are searching for
var String itemName = triggeringItem.name.replaceFirst( "(_Volume|_Volume_Limit)$", "")
//filter the group by the shortened name and the needed ending to construct the needed items
var DimmerItem volumeItem = gVolumeLimit.members.filter[ item|item.name == itemName + "_Volume" ].head
var DimmerItem limitItem = gVolumeLimit.members.filter[ item|item.name == itemName + "_Volume_Limit" ].head

If changing the naming slightly, it could be even more easy.
“Name part” without any “_”, use this as a separator to the kind of Item, would be like

Dimmer Player1_Vol "Volume Player 1" (gVol)
Dimmer Player1_Lim "Volume Limit Player 1" (gVol)
rule "volume limit"
when
    Member of gVol changed
then
    var String itemName = triggeringItem.name.split("_").get(0)
    var Number volume   = gVol.members.filter[ m|m.name.contains(itemName) && m.name.contains("_Vol" ].head.state as Number
    var Number limit    = gVol.members.filter[ m|m.name.contains(itemName) && m.name.contains("_Lim" ].head.state as Number
    if(volume > limit)
        gVol.members.filter[ m|m.name == itemName + "_Vol" ].head.sendCommand(limit)
end

I must admit when I first saw this post I just skimmed it and didn’t read it that closely.

This is a type error. Most of the time this happens when you have an Item that is NULL or UNDEF and you try to use it as if it were a Number (DecimalType, PercentType).

The error is actually occurring on the greater than operation. If either of the operands are not a Number then it throws a null pointer exception.

This is usually really good advice. Don’t be any more specific when it comes to type than necessary.

Looking back at the OP, check the state of limitItem and volumeItem before the if ( volumeItem.state > limitItem.state ) { to make sure the state isn’t NULL or UNDEF.

    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;
    }
    //this is where the magic happens ;)
    if ( volumeItem.state > limitItem.state ) {
        volumeItem.sendCommand( limitItem.state )
    }

I thought being specific (when knowing what to expect) would be more efficient for the compile, and would generate more specific errors. Thus I considered it to be good coding hygiene, and put it in this tutorial. Of course no problem to leave it out and there is one less thing to customize for other purposes.

Because the Rule DSL is a weakly types language, it is actually more work at parse/compile time when you are more specific than it is if you leave the type determination until runtime. When you are specific the parser then has to go through everything to make sure the types line up. When you are less specific then the parser doesn’t have to do that much checking at all and so can parse/compile the code much faster.

The problem is really bad when using primitives. Using lots of primitives can add minutes to the rules loading on a RPi.

In many languages that would be the case. The Rules DSL has some quirks in this case which makes it a bad idea.

If you are on a non SBC you probably wouldn’t see any problems at all. But on an SBC like an RPi, being less specific can result in a difference of five or ten minutes in the amount of time it takes OH to load your rules.

Thanks for the hints! You must have a lot more rules than I have, on my RPi (3+) my rules load in a matter of seconds.