Reducing Item updates (and thus log entries) caused by a Modbus TCP energy meter

Doh! Another typeo. We have to pass in the key on the call to put.

Though this raises an important thing. If you don’t understand what this code does and how it works beware adopting it. You won’t be able to maintain it going into the future and at some point it’ll break and you won’t be able to fix it.

(function(i) {
  const lastValue = cache.private.get('last', () => -999999); // initialize it so if there isn't a lastValue the delta is large
  const delta = Math.abs(lastValue - parseFloat(input));
  if(delta > 1) {
    cache.private.put('last', parseFloat(input));
    return input; // pick a reasonable threshold
  }
  return null; // if the delta isn't large enough return null, suppressing the event
})(input)

It’s working now. Actually I’m used to DSL, because even this takes half a minute to compile on my Raspberry. What an issue is, is that the transformation can be used only once due to cache.private.get(‘last’).
Could this ‘last’ be replaced by something like the itemName? Or maybe we could directly compare it with the item value without cache.private.

I have seen that input provides a input.lastValue, but I guess a Modbus data thing has no lastValue.

In Rules DSL the transform would be

val lastValue = privateCache.get('last', () => -999999)
val delta = Math.abs(lastValue - Float::parse(input))
if(delta > 1) {
    privateCache.put('last', Float::parse(input))
    input
}
null

That should work. I’ve never done a DSL Script transform so can’t be certain but based on the docs, and assuming no typos it should work.

The private cache is limited to that specific instance of the transform. If you use that transform on another profile for another Item, it will get it’s own private cache so there will be no problem.

But let’s assume there was a problem…

The item name isn’t available in the script. Something would need to be hard coded.

However, the Script transformation supports passing arguments to the transform (works for any languages including Rules DSL).

Let’s pass the itemName and the threshold as variables.

Passing variables works the same as passing values to a form in a URL: ?var1=value&var2=value2. So assuming the transform is in the file filter.js you’d use filter.js?itemName=MyItem&threshold=5. I think for Rules DSL it would be filter.dsl?itemName=MyItem&threshold=5. If you defined the transform in the UI just append ?itemName=MyItem&threshold=5 after selecting the transform.

The variables will be injected as a String, same as input.

Then the JS transform would become:

(function(input, itemName, threshold) {
  const lastValue = cache.private.get(itemName, () => -999999); // initialize it so if there isn't a lastValue the delta is large
  const currValue = parseFloat(input);
  if(currValue === NaN) return input; // handle NULL and UNDEF
  const delta = Math.abs(lastValue - currValue);

  if(delta > parseFloat(threshold)) {
    cache.private.put(itemName, currValue);
    return input; // pick a reasonable threshold
  }
  return null; // if the delta isn't large enough return null, suppressing the event
})(input, itemName, threshold)

Note, I added the ability to handle NULL and UNDEF states without crashing.

The Rules DSL version would become:

try {
    val lastValue = privateCache.get(itemName, () => -999999)
    val currValue = Float:parse(input)
    val delta = Math.abs(lastValue - currValue)
    if(delta > Float.parse(threshold)) {
        privateCache.put(itemName, Float::parse(input))
        input
    }
    null;
} catch(e) { // handle NULL and UNDEF, if the parse fails it's one of these
    logInfo('filter transform', 'Failed to process update to ' + itemName + ' to ' + input + ' with threshold ' + threshold)
    input
}

But again, the private cache is private, limited to this one instance of the transform. So it doesn’t matter if the same key is used.

I don’t know where you’ve seen that. Per the docs:

The input value is injected into the script context as a string variable input .

A Sting doesn’t have a lastValue property.

Thank you very much for your support!

I think JS is better here. Due to the amount of Modbus data CPU load should be lower than with DSL.

According to my testing, I can not confirm this. I’m having one transform which I used in several Modbus data things. Having the same cache name was not working.

This is how I made it working now! :grinning:

Openhab was proposing this in the transformation code editor.

This is true for inline script transform, but not for file-based transform. I believe that each transformation script file is a scriptengine instance, so if you are referring to the same transformation script file from multiple places, they’ll all be using that single scriptengine instance, and therefore the same private cache.

2 Likes

Here’s an implementation using JRuby.

Number:Power MyPower { channel="xxxx" [ profile="ruby:throttle_updates" ]

Then drop this file in conf/automation/ruby/throttle_updates_profile.rb


profile(:throttle_updates) do |event, callback:, item:, state:|
  next unless event == :state_from_handler

  only_every(60.seconds, id: item) { callback.send_update(state) }
  false
end

But this is not to prevent messages based on the difference between old and new value, but is a time based filter (max once per 60 Seconds)

In my opinion there should be more standard profiles in OH4 without having additional transformation scripts, e.g.

  • delta_filter_abs
  • delta_filter_relative
  • time_filter_debounce

Let’s say you have 100 Modbus values every 3 seconds. I don’t know if cache.private() is fast enough. A native implementation might be better.

1 Like

I tried submitting some additional profiles to core but got told that they should go into an add-on instead.

The private and shared caches are native. IIRC they are implemented as a registry (e.g. same as the ItemRegistry, ThingRegistry, MetadataRegistry, etc.) and ultimately backed by a Map. If there is a performance limitation, it’s likely going to be the overhead from calling a profile at all and not the use of the cache.

But, now that we have the name of the Item passed in we don’t really need the cache. We can pull the Item itself and see what it’s currently set to.

(function(input, itemName, threshold) {
  const newValue = parseFloat(input);
  const currValue = items[itemName].numericState;
  const threshold = parseFloat(threshold);

  if(items[itemName].isUninitialized || newValue === NaN) return input; // always update if the current state or new state is NULL or UNDEF
  if(threshold === NaN) {
    console.error('Transform filter: threshold is not parsable to a number: ' + threshold);
    return null;
  }

  const delta = Math.abs(currValue - newValue);

  if(delta > parseFloat(threshold)) {
    cache.private.put(itemName, currValue);
    return input; // pick a reasonable threshold
  }
  return null; // if the delta isn't large enough return null, suppressing the event
})(input, itemName, threshold)

Right. Is there also a way in the “Thing to Item Transformation” to get the itemName automatically? At the moment I type it in manually.

config:js:delta_filter_abs?itemName=ActivePower&threshold=2

As I said above, no. The only way is to pass it in.

I thought rate-limiting it is a better solution than checking against delta value. But to check for delta value in JRuby is like this, assuming the channel and items are all dealing with quantitytype

Number:Power MyPower { channel="xxxx" [ profile="ruby:filter_out_small_changes", threshold="1 W" ]
profile(:filter_out_small_changes) do |event, item:, state:, configuration:|
  next true unless event == :state_from_handler && item.state?

  threshold = QuantityType.new(configuration["threshold"])
  delta = item.state - state
  delta = delta.negate if delta.negative?
  delta >= threshold
end

It’s the nature of modbus to get that many values. If Openhab can not handle it via Transformation Profile, standard features like this should be implemented natively or into the binding. My opinion.

The more binding do, the more complex it gets, and harder to maintain. It leads to situations where fix for one person might impact behavior expected by other.

To be clear, I agree that threshold and debounce profiles are needed, I just question if modbus binding is one which should ship these.

1 Like

You are fully right! Threshold and debounce profiles should be part of the Openhab core. Some days ago I even didn’t know that there are Transformations available at the market. The problem here is mainly you don’t know what the are doing and what parameters they have. Best would be to have natively more standard profiles available, especially when they are performance critical.

I have done some corrections to your code. It should work now, can you please review.

  • renamed local variable threshold to thresholdFloat ; no double definition
  • Removed line cache.private.put(itemName, currValue);
  • Changed “delta > parseFloat(threshold)” to “delta > thresholdFloat”
(function(input, itemName, threshold) {
  const newValue = parseFloat(input);
  const currValue = items[itemName].numericState;
  const thresholdFloat = parseFloat(threshold);

  if(items[itemName].isUninitialized || newValue === NaN) return input; // always update if the current state or new state is NULL or UNDEF
  if(thresholdFloat === NaN) {
    console.error('Transform filter: threshold is not parsable to a number: ' + threshold);
    return null;
  }

  const delta = Math.abs(currValue - newValue);

  if(delta > thresholdFloat) {
    return input; // pick a reasonable threshold
  }
  return null; // if the delta isn't large enough return null, suppressing the event
})(input, itemName, threshold)

All entries on the marketplace have documentation in the post. They don’t get the published tag without that. If there is one not documented point it it and we can correct that problem.

Again, based on what I’ve seen, the new overhead of calling the profile is the limiting factor. But let’s assume there is a difference. If the argument is “performance” exactly what is causing that performance difference needs to be identified and measured and addressed. Making changes blindly rarely actually fixes anything.

That looks good. It’s challenging to code on my phone. it’s ready to miss things. Thanks for posting a working version.

Hi Jim,
I like the custom profile approach but I would also prefer that these profiles are in the OH core.
For simplicity I started with your rate limiter:

Number:VolumetricFlowRate Lueftung_Airflow "Airflow: [%.0f %unit%]" (gLueftung, gLueftung_Chart_Airflow)  {
	channel="knx:device:bridge:lueftung_fast_poll:Lueftung_Airflow" [ profile="ruby:throttle_updates" ],
	unit="m³/h"
}

I activated “JRuby Scripting” and created that file:

conf/automation/ruby/throttle_updates_profile.rb

profile(:throttle_updates) do |event, callback:, item:, state:|
  next unless event == :state_from_handler

  only_every(300.seconds, id: item) { callback.send_update(state) }
  false
end

I see in the logs:

 No ProfileFactory found which supports profile 'ruby:throttle_updates' for link 'Lueftung_Airflow -> knx:device:bridge:lueftung_fast_poll:Lueftung_Airflow'

Do I miss something?

Thanks
Marco

Which openhab version?

Add a comment line, e.g.

# 1

Then save the file. You should see a log that says the file is reloaded.

Yeah, I tried submitting some more profiles to core but got told that they should go in an add-on. I didn’t want to waste any more time on it since I’m perfectly happy using jruby profile.

I’ve recently made the jruby profile visible and usable by UI items too.