Avoid state flickering with JRuby scripts

Hi,

I use openHAB 3.4.5 with the Homekit addon to control several roller shutters. Actually I am quite happy with it but there is one tiny detail which I would like to get rid of and I think JRuby scripts can help me with that. But I am still struggling getting it fully to work. The issue is highlighed in bold below:

The home app has a typical vertical bar to control a shutter’s vertical position:

However, while using this bar openHAB sends every 200-300 ms a new command to the shutter’s controller which I find quite annoying.

I read that JRuby has the ability to wait until an item’s value is stable until it performs an action without explicitly creating a timer. Since I am controller 17 blinds I do not want to manage 17 timers via RuleDSL.

So here is what I did so far with JRuby:

I have created 2 items per roller shutter. A proxy item which is shown in the Homekit and which I use to trigger on with the JRuby rule and a physical item which is bound to the roller shutter controller:

Group:Rollershutter         gJalousien
                            "Alle Jalousien"        

Group:Rollershutter         gJalousien_Proxy
                            "Alle Jalousien Proxies"
(...)
Rollershutter               va_Jalousie_SollPos_Wohnzimmer_KuecheNord
                            "Küche Nord"
                            <rollershutter>
							(gJalousien)
                            ["Blinds"]
                            { channel="shelly:shelly25-roller:<id>:roller#control"}                     

Rollershutter               va_Jalousie_SollPos_Wohnzimmer_KuecheNord_Proxy
                            "Küche Nord"
                            <rollershutter>
							(gJalousien_Proxy)                            
                            { homekit = "WindowCovering" }
(...)

The JRuby rule now does the following:

rule 'Jalousie verfahren' do
  changed gJalousien_Proxy.members, for: 1.seconds
  triggered do |item|

    jalousie_proxy = item
    jalousie_name = jalousie_proxy.name.sub('_Proxy', '')
    jalousie_thing = gJalousien.find { |item| item.name.include? jalousie_name }
    jalousie_thing&.command jalousie_proxy.state

    logger.info("Jalousie verfahren auf neue Position: #{jalousie_name}: #{jalousie_proxy.state}")
  end
end

With one particular roller shutter being controlled manually inside the Home app this works fantastic! openHAB waits up to 1 second until no new user input was given and then the new position is sent to the controller.

However, when controlling multiple roller shutters via scenes inside the home app this does not work. I.e. if I close all 17 roller shutters at once via a “Good night” scene inside the home app only 1 roller shutter really closes. The others remain opened but the proxy items still report that all shutters have been closed. Is what I am trying to do possible and if so, what am I doing wrong?

I don’t use jRuby so this is just a guess, but I suspect the for: 1.seconds part only creates 1 timer for the whole rule instead of one timer per member. All the proxy Items change, the rule gets triggered on each of them and then waits for one second after the last proxy Item’s change triggered the rule and runs the rule only for that last proxy Item.

I don’t think you are going to avoid managing a separate timer for each Item, no matter the language. I’ve written a library of JS Scripting that includes a timerMgr for just such a situation. I’d be surprised if there isn’t something similar for jRuby.

For completeness, using my library the rule would look something like this as a text based rule:

const {timerMgr} = require('openhab_rules_tools');

rules.when().memberOf('gJalousien_Proxy').changed().then(event => {
  const timers = cache.private.get('timers', () => new timerMgr.TimerMgr());
  timers.check(event.itemName, 'PT1S', () => {
    const jalousie_proxy = event.itemName;
    const jalousie_name = jalousie_proxy.replace('_Proxy', '');
    items[jalousie_name].sendCommand(event.newState.toString());
    console.info("Jalousie verfahren auf neue Position: " + jalousie_name + ': ' + event.newState);
  });
}).build('Jalousie verfahren', 'description');

That uses the private cache to store the timers. That call to check will create a timer for one second if one for event.itemName doesn’t already exist, or reschedule the timer if one does already exist. After the one second, the timer goes off and calls the function that commands the Item and logs and then cleans up the timer.

Since this is file based you could probably put the timers variable as a global instead of in the cache but I mostly use UI rules so generally use the cache. As a UI script action it would look like:

const {timerMgr} = require('openhab_rules_tools');
const timers = cache.private.get('timers', () => new timerMgr.TimerMgr());

timers.check(event.itemName, 'PT1S', () => {
  const jalousie_proxy = event.itemName;
  const jalousie_name = jalousie_proxy.replace('_Proxy', '');
  items[jalousie_name].sendCommand(event.newState.toString());
  console.info("Jalousie verfahren auf neue Position: " + jalousie_name + ': ' + event.newState);
});

Thank you for the explanation. It makes sense that the timer is bound to the rule and not the members.

Coming to your example: that looks indeed quite handy! So the whole timer management is happening implicitely?
I do not have experience with JS Scripting rules yet. Do I put your first example into a “rule.js” file and put it under ./automation/js folder and that’s it? And of course install the JavaScript Scripting addon before. That’s all then?

@jabra_the_hut, in jruby when you do changed GroupItem.members, for: 1.second, it should keep track of each individual member separately, so if you have 17 members, it will keep track each of the 17 separately, as you would expect.

I tested this scenario just to be sure, on the latest jruby helper library version 5.5.0. Are you using v4.x or v5.x? Although I believe it should work the same way in 4.x, I just can’t test it.

Something else is not working the way you might think it is.

If your openhab.event.ItemStateUpdatedEvent (or whatever is the equivalent in openhab 3.x) logging is enabled, see if all those items received the state changed.

On a separate note, I think you can completely remove using proxy for this situation, if the whole purpose of proxy is to prevent too many commands being sent in rapid succession. One way I can think of is using a custom profile and perform a debounce in the profile. The Jruby library has a handy method to do this too.

I guess you could say that from the end user perspective. It’s very explicit in the library code.

Yes

No, you’d also need to install openhab_rules_tools.

I think it’s openhab.event.ItemStateEvent.

Does your question refer to the Ruby Gems? There I had openhab-scripting=~>4.0. Changing it to openhab-scripting=~>5.0I cannot get the script to work.
At Require Scripts I have openhab/dsl and my script begins with require 'openhab'.
However, when the rule gets executed I receive in the logs:

2023-08-11 11:31:29.180 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/openhab/conf/automation/jsr223/Jalousien.rb': Error during evaluation of Ruby in org/jruby/RubyKernel.java at line 1017: (LoadError) no such file to load -- openhab

I run openHAB 3.4.5 insider a Docker Container.

Yes, I was referring to the ruby gem.

If you have other scripts already written and working with version 4.x, do not change it to 5.x at this point, until you’ve gone through the v5 changelog and adjust for the breaking changes. Upgrading to 5.x is not necessary to solve this problem, because I believe the 4.x works the same way for changed member item, if I remember correctly.

If on the other hand, you don’t have any other established jruby scripts by all means, start using v5 because that’s where the current development is. In that case, if you are using v5.x, just remove the require 'openhab' line at the top.

The jruby helper library (gem) v5.x is compatible with openhab 3.4


I am simply wondering whether all your 17 items received a state change, because if they did, your rule should work as you intended.

This is my first rule to be developed in JRuby, so I am fine starting with 5.x. :slight_smile:

Removing the require directive the script execution fails due to the “find” command:

2023-08-11 11:31:13.590 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job '<unknown>' failed and stopped
org.jruby.exceptions.NoMethodError: (NoMethodError) undefined method `find' for #<OpenHAB::Core::Items::Proxy:0x1a9949a>
	at RUBY.method_missing(uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/delegate.rb:87) ~[?:?]
	at RUBY.<main>(/openhab/conf/automation/jsr223/Jalousien.rb:9) ~[?:?]
(...)

You wrote above that the Proxy item is not necessary for this usecase. How can that be? I have “wired” the proxy items to the Homekit addon, so that whenever the user changes the desired position of one or multiple rolling shutters inside the “Home” app the proxy item’s state changes. This gets recognized by this rule here and the rule sends it to the actual thing.

Removing the proxy item would mean the rule would fire when the thing’s item was already changed which would mean that the blinds already move when we enter the rule. Isn’t it?

This is due to the difference between 4.x and 5.x.

Your code:

    jalousie_thing = gJalousien.find { |item| item.name.include? jalousie_name }

Needs to change to:

    jalousie_thing = gJalousien.members.find { |item| item.name.include? jalousie_name }

But it is not necessary to use find. It could simply just be:

items[jalousie_name]&.command jalousie_proxy.state

Using item profile on the actual item itself, you can intercept the commands and not pass them through to the thing (shelly binding?) when commands are received too frequently.

Two things that will help you achieve this:

  1. Creating the profile in jruby
  2. Wrapping the callback.handle_command with jruby’s debounce_for so that it only handles the command when there’s at least X amount of time (e.g. 1.second) idle.

Yes, it’s the Shelly binding. OK, now with your tweaks it works. A timer gets created for each roller shutter. But still it does not perfectly. This time it seems to be due to the way how Homekit handles roller shutters.
First the items receive the desired position of 80% and while being closed homekit sends out 100% so the shutters end up being fully closed. Same with bringing it from 100% to 80%. In the middle of the movement of the blinds homekit sends out 0% from nowhere and the blinds are opened in the end. My logs look like this:

2023-08-11 14:03:58.931 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_Ost: 80%
2023-08-11 14:03:58.952 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 80%
(...)

2023-08-11 14:04:02.692 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 100%
2023-08-11 14:04:02.799 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_Ost: 100%
(...)
Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 80%
2023-08-11 14:04:28.913 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 80%
2023-08-11 14:04:28.934 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 0%
2023-08-11 14:04:30.426 [INFO ] [n.jrubyscripting.rule.Jalousien.rb:1] - Jalousie verfahren auf neue Position: va_Jalousie_SollPos_Wohnzimmer_KuecheOst: 0%

This will probably be solved with those profiles you recommend so I will read into it.

If that gets sent from “somewhere”, then your rule will simply pass that through. Even when using a profile, unless you implement some sort of logic to filter that out, but how would you know what’s a real and what’s an undesired command? You’d need to figure out what’s sending that extra command and stop it from doing that.

The profile method (untested), it’s super simple:

Rollershutter               va_Jalousie_SollPos_Wohnzimmer_KuecheNord
                            "Küche Nord"
                            <rollershutter>
							(gJalousien)
                            ["Blinds"]
                            { channel="shelly:shelly25-roller:<id>:roller#control" [profile="ruby:jalousie"] }                     

create a jruby file:

profile(:jalousie) do |event, callback:, command:, item:|
  next true if event != :command_from_item

  debounce_for(1.second, id: item) do
    callback.handle_command(command)    
  end
  false
end

You don’t need the proxy, nor the groups, nor the rule.

1 Like

I wonder how the binding knows. :thinking:
I think it has something to do with the „closing“ state in which the shutters are while travelling down. Without using proxies this is not an issue.

I will try it with your example and give feedback. Thank you for that so long! :pray:t2:

Do I need to install something additionally?
After having stored the script inside a *.rb file I get

2023-08-11 21:21:55.321 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/openhab/conf/automation/jsr223/Jalousien.rb': org.jruby.exceptions.SyntaxError: (SyntaxError) /openhab/conf/automation/jsr223/Jalousien.rb:17: syntax error, unexpected ')'
...bounce_for(id: item, 1.second) do
                             ^

Sorry, wrong argument order on debounce_for. Also next should be used instead of return. I have updated the original post with the corrections.

Ok, the script is now loading. However, it does not seem to have any effect :flushed:

I do not get any errors but the system behaves as if this script would not exist. State changes are immediately propagated to the shellys resulting in a fast relay clicking at the shelly.

I equipped each thing item with the ruby:jalousie profile (and homekit=WindowCovering)

@jabra_the_hut I’ve made another mistake (told ya I haven’t tested it) in my post.

The mistake was how to specify the profile. It should be enclosed in a square bracket, right after the channel= definition, and should not have been separated with a comma.

So the syntax should’ve been:

{ channel="shelly:shelly25-roller:<id>:roller#control" [profile="ruby:jalousie"], homekit="WindowCovering" } 

I’ve also updated the original post with the correct syntax.

All good. It‘s just me who feels super stupid as I currently fully rely on every single line of code from you. ^^

I updated the items. Still the same as this morning. No effect. Shellys immediately receive the new target position and click around.

OK try two things:

  1. Add a log at the top of the profile code
profile(:jalousie) do |event, callback:, command:, item:|
  logger.info("Ruby profile #{event}: command: #{command} -> #{item.name}")

  next true if event != :command_from_item

  debounce_for(1.second, id: item) do
    callback.handle_command(command)    
  end
  false
end
  1. If you can’t see any log, try restarting openhab. Sometimes it doesn’t reload the changes on .things file. I realised that this is in an .items file but worth a try. A less severe step would be to comment out the whole item (to remove the item), then uncomment it to reload it.

As we say in German: Reboot tut gut / reboot does good.

Yup, it now works as expected. :tada::partying_face:
All blinds wait a second and then move. No annoying relay clicking anymore. I can see the log messages.

Thank you very much for your support. Ruby scripts seem quite impressive although not easy to understand when you did 2 years of RuleDSL only. I shall dig deeper now in this.
Thanks again!

1 Like