JRuby OpenHAB Rules System

Jim
I use UI rules not files so the portion of the rule I would use is below correct?
Can I ask a few questions?

is light an item?
is level a variable?
So we start out checking if the light is on

    next if light.on?

What does next do? Does it require the end
Is this not required in UI rule?
then we set level to 0

 after(15.seconds) do |timer|

ok, we are creating a timer here?

timer.reschedule if level < 100 && !timer.cancelled?

now we are resetting the timer if level isn’t 100 yet or the timer got cancelled? How would the timer have gotten cancelled?

      level = level + 1
      light << level

inside the loop, every 15 seconds the level gets incremented by 1 until we reach 100?
sooo… if the light is not on, we set level (a local variable we just created on the fly?) to 0 then loop every 15 seconds until level reaches 100

Sorry, if you could just break it down a little for us noobs. Some times ruby is so succinct it is hard to follow

Yes to both. You’d need to point light (or rename it) to an actual item.

It’s an early exit from the enclosing block. If you’re using UI rule, you’ll need to replace that with return instead.

Yes.

It’s a paranoid failsafe. It will work even without it, but I have been caught with a runaway self repeating timer that keeps executing / rescheduling itself even after being cancelled.

It’s because you can cancel a timer, and then reschedule it again. So if you happen to already be executing inside your timer handler and you cancel the timer from somewhere else, your code keeps going to the next line and the next line…and eventually reschedule itself thus nullifying your cancel call.

Correct.

level is indeed a local variable there.

I’m just a more experienced noob :smiley: but still a noob. Happy to help if I can.

so in UI my rule looks like this?

if light.on?

    level = 0
    after(15.seconds) do |timer|
      next unless light.state.to_i == level
      level = level + 1
      light << level
      timer.reschedule if level < 100 && !timer.cancelled?

end

next is unneeded
so is the end for if or the timer?
Where does the return go?
I think the file example trying to convert it to UI, I think that confuses me as far as the structure

No this will give you the opposite logic. We want the code to run if light is off, not on. Also your code is missing one closing end:
if ... end on the outside,
after(xxx) do ..... end on the inside. The do and end are a pair, and it is called a block in Ruby. Blocks are a fundamental construct in Ruby that is crucial to know/understand.

In this case after(....) is a method that takes a block. The stuff inside the do…end is the block being passed to the after() method

I literally meant: replace next with return like this:
The only thing different to the original is s/next/return/


    return if light.on? # don't do the gradual thing if light is already on

    level = 0

    after(15.seconds) do |timer|
      next unless light.state.to_i == level # Stop the routine if it got changed manually
      # Note to_i above to round the state to integer in case it has fractions

      level = level + 1
      light << level
      timer.reschedule if level < 100 && !timer.cancelled?
    end

PS don’t forget to point light to the actual item.

as the very first line:
light = MyActualLightDimmerItem

This works as intended! (But 5am… hell no :smiley: )

OK, sorry, let me see if I got this straight.

return if light.on?

This is a stand alone statement. It needs no end because this one line is it
It means ‘if the light is on bail out’ early exit, none of the following code runs

    level = 0

This is a local variable being created and set to 0

ok so here we create a block with after

after (<some period of time goes here>) do
        ###do whatever here
end

the end is the completion of the do block

|timer|

does timer have a name? local timer, disappears after code runs?

      next unless light.state.to_i == level

How does this work? level is a local variable right?
How would our item ‘light’ have a state the same as level
and this is the same thing as return, an early exit from the block?
Sorry for dumb questions

|timer| is the argument list for the block. So essentially timer is a local variable with the scope of (only visible in) the block. When a method calls a block, it can pass arguments just like any other method. In this case, Timer yields itself to the block, specifically to allow constructs like this.

Correct, level is a local variable with scope of (in your case) the entire “file” of the UI rule. Ruby is “cool” in that every block is a closure, and “captures” any local variables that are referenced within it, but exist outside it. This is just a really fancy way of saying that even though the block from the timer is executed asynchronously after the rule was initially executed, level is a single variable that will continue to live on, and be a single value - i.e. if you change it within the block, its effects are visible outside the block to anyone else referencing the same value, including multiple executions of the same block.

The logic here is:

  • light has state 0, level is 0
  • set level to 1
  • command light to change to level (1)
  • wait 15 seconds, and repeat (unless we’ve reached 100)

If during that 15 seconds, the light was commanded to something different, like if someone turned it off locally, the state would no longer match level, and we want to stop our “loop” of slowly dimming up. Essentially it was cancelled.

return returns from the current method (or file, which a UI rule is) immediately. next returns from the current block, but allows the method calling the block to continue executing, and possibly call the block again. This is analogous to continue in C/C++/Java etc., except that it is far more general since it applies to blocks, not just basic loops. In terms of loops, it means “skip this single iteration”. break returns from the current block and the method calling the block, so that it (likely) cannot be called again. For file-based rules, the actual execution part of a rule is just another block, so we use next to skip the execution of the rule, instead of return which would mean “return from the enclosing method” (which would actually raise a LocalJumpError because there usually is no enclosing method, and return at file level cannot be used from inside a block). Using break will also raise an error in a file-based rule, or in your timer case, because break is invalid unless the block is being called immediately from within the method you’re passing it to. For rules, we save the block off and call it when the rule is triggered. For timers, we save the block off and call it when the timer expiries.

I hope all that makes sense. Happy to clarify any points I was too vague or dense on.

Inorite?!?! I swear that guy never sleeps!

1 Like

Sorry to have beat this to death
so here is what I have in UI rule

logger.info("jruby fading rule run")
### BigRedDimmer is a item - a Lifx bulb

return if BigRedDimmer.on? # if the light is on, nevermind, do nothing
level = 0
after(1.seconds) do |timer|
  next unless BigRedDimmer.state.to_i == level
      # they should be the same unless someone/something changed it
  level = level + 1
  BigRedDimmer << level
  timer.reschedule if level < 100 && !timer.cancelled?
end

So I have a rule, if the light is off, it will fade the light on one step a second until 100 unless at some point during that process, someone or something charges the dimmer, then the process just aborts

How do I pass in which light item I want to fade, say if I wanted to make it work on any light? Can I use it in a script to fade any item? (that works like a dimmer)

How about what if it isn’t off and it just fades from what ever it is now to a new setting either up or down?

Precisely.

I normally find the light based on the triggering item and using semantic methods. I.e. event.item.equipment.members.points(Semantics::Switch).first. This assumes the triggering item is in the same “equipment” as the dimmer. I have a “scene” item on my switches that is triggered my multiple taps, so I put that item in the equipment group. So you navigate from the scene item, to the equipment, then on that equipment look for the Point items that are Switches. You could use other methods (directly looking up an item by the triggering items name that has been modified some way, or using tagged, etc.).

Funny you should ask! @jimtng has a PR allowing exactly that! It should be merged and released within the next few days (as soon as I have some time to review it!)

Right now it would just abort if it isn’t off. If you want it to start at the current level, initialize level to item.state instead of 0. If you want to go down or up depending on something else, instead of adding one, use a delta, and assign it 1 or -1 as appropriate at the beginning of the rule.

@Andrew_Rowe, are you after a quick fading effect? Would the delay of 1 second for each iteration be sufficient? You might also need to increase the brightness by 5, or 10% each time instead of 1%, to get quicker transitions. However, the effect would be smoother if the device could do the fading by itself, and you simply set it to the final % that you want - although this may not always be possible depending on the device. Tasmota (I think) and esphome has such feature.

Or alternatively you could put the fading code in a method, save it in a library file, then require that file in your rule, then voila, you can call that method from any rule.

Or better yet, just switch to file-based rules.

It all depends on how you’re going to do this for the multiple items, so if you can explain that part, I’ll try to give a more concrete example. Generally the approach that @ccutrer mentioned above would probably be the best way to do it.

no no, I was just testing the lighting effect
I’ve got one sample set up so at astro:sun:local:civilDusk starts
every 24 seconds starting where it is at that moment and counting down to 1
here is the codez

level = Spotlights_Dimmer.state
after(24.seconds) do |timer|
  level = level - 1
  Spotlights_Dimmer << level
  timer.reschedule if level > 1 && !timer.cancelled?
end

did I get it right?

edit: I have a camera recording time lapse video from local 5:30 pm until 8:30 pm will be a 30 second time lapse. There are a number of other effects that run at that time and the video will capture all

Beware that if the current state is 1.01%, it would set your dimmer to 0.01 which might get rounded down to 0, possibly turning off your light. So I’d change it to if level >= 2
Also if the current level is already 1, you’d be setting it to 0. A check needs to be added before starting the timer.

Alternatively you could also use increase/decrease if it works.

after(24.seconds) do |timer|
  next unless Spotlights_Dimmer.state >= 2

  Spotlights_Dimmer.decrease 
  timer.reschedule unless timer.cancelled?
end
1 Like

so what does decrease do??? increment one?
where does a guy find all these cool things ruby can do?
And I’ve scoured the OH jruby docs, and they are great

ok so, we are acting directly on the Item, sending it a command and because it is a dimmer, decrease is a valid command and because it is ruby it doesn’t have to be DECREASE

Just like Item.on is the shorthand for Item.command(ON), Item.decrease is the shorthand for Item.command(DECREASE).

I admit I don’t know exactly where inside openHAB this DECREASE is actually handled, that’s why I said “if it works for you”, but on my system it reduced the dimmer state by one.

ok, so my next question is what happens if there is code following this block? I assume the next block runs immediately so what if

after(24.seconds) do |timer|
  next unless Spotlights_Dimmer.state >= 2

  Spotlights_Dimmer.decrease 
  timer.reschedule unless timer.cancelled?
end
after(72.seconds) do |timer|
  next unless Someotherlight_Dimmer.state >= 2

  Someotherlight.decrease 
  timer.reschedule unless timer.cancelled?
end

two lights go off on there own little jruby timer thread and do their thing? And nothing else gets blocked?

Correct. after doesn’t block. It simply creates and starts a timer and immediately returns to the caller. In fact, after is just a fancy wrapper for openhab’s ScriptExecution.createTimer

Since your timer code is exactly the same except for the item, you could do it like this (just one of the many other ways)

[[Spotlights_Dimmer, 24.seconds], [Someotherlight_Dimmer, 72.seconds]].each do |item, duration|
  after(duration) do |timer|
    next unless item.state >= 2

    item.decrease
    timer.reschedule unless timer.cancelled?
  end
end

no easy to miss but second block interval changed

Anyhow… point is what if block, then different block, they all go on their merry ways. What if block inside another block

level = Spotlights_Dimmer.state
after(72.seconds) do |timer|
  level = level - 3
  Spotlights_Dimmer << level
  timer.reschedule if level > 4 && !timer.cancelled?
end
other_level = 100
after(2300.seconds) do |timer|
  Spotlights_Color << "orange"
     after(24.seconds) do |timer|
     level = level + 1
     Spotlights_Dimmer << level
     timer.reschedule if level < 100 && !timer.cancelled?
  end
end

sorry, trying to understand, so one rule could set off a whole bunch of timer blocks from one trigger
I would use a cron trigger but this is all stuff that triggers off astro:sun:local:civilDusk START

I know. My example code took that into account. It was meant to give you more examples of what can be done with Ruby.

Yes one rule could set off many timers. Have you also looked into managed timers :D?

In the above code, what’s other_level for?

But yes, you can set another timer from inside a timer, that’s perfectly fine. Although you should use a different name for the inner timer to avoid confusion.

If you want to decrease a dimmer by 3, especially multiple dimmers like this, instead of using an external variable level, you could do

timer.reschedule if Spotlights_Dimmer.state > 4
Spotlights_Dimmer << Spotlights_Dimmer.state - 3

Another note: instead of 2300.seconds you can use 38.33.minutes (.minutes/seconds/hours/etc also apply to floating point), or 38.minutes + 20.seconds, i.e.

after(38.minutes + 20.seconds) do
  ...
end

so level is a block level variable and I can use again?

right so that was where I was going, how reusable, trying to understand the structure
So is there a better way to handle this or is ok?

yeah, I figured but thank you for example so…

after(72.seconds) do |timer|
  level = level - 3
  Spotlights_Dimmer << level
  timer.reschedule if level > 4 && !timer.cancelled?
end
after(38.minutes) do
 Spotlights_Color << "orange"
 after(24.seconds) do |timer2|
   Spotlights_Dimmer.increase
  timer2.reschedule if level <100 && !timer2.cancelled?
 end
end

is there a more elegant way or is good
sorry to belabor
trying to make a fader widget plug in light, increment and step time

In this case, you can keep using timer, not timer2. The This has nothing to do with the previous timer above it. I was suggesting using a different name if the 38 minute timer (i.e. the outer timer) also had a timer variable. In this case you have removed it so it doesn’t exist anymore.

You should install rubocop, it helps a lot in spotting things like these and sometimes even fixing / optimising your code or at the very least warning you about potential issues. If you use vscode let me know, I can make a separate post explaining the steps to do get get it working. I have to say it’s not the easiest for me and I wish someone else would tell me if there’s a better way.

This may not be the best article out there, it’s just the first result from my search but it seems good enough. It explains it in greater detail than I can. Understanding Variable Scope & Binding Objects in Ruby - RubyGuides

You can reuse variables, as long as you know they are no longer used. In the case of timers, because timers run later, they will still be used then, when the timer kicks in, so just need to be aware of that.

It just seems that you’re trying to make some sort of super slow fading effect. The first timer will reduce dimmer by 3% every 72 seconds, starting 72 seconds from now, until it’s 1%. If you started from 100%, that would take over 40 minutes to complete?

Then at minute 38, set the color to “orange” and 24 seconds after that, start increasing the dimmer again by 1% every 24 seconds. Because this will start before the first timer is finished, there’s an overlap of two timers going on I think. During the “overlap” between the first and third timer, they’ll be fighting, timer 1 reduces the brightness by 3% (but every 72 seconds), and timer 2 increases the brightness by 1% every 24 seconds. Eventually timer 1 wins - but that will take a long time. 3 steps back, 2 steps forward.

The whole thing seems pretty complex.

How quick of a fading effect are you trying to achieve? What’s the desired overall period of the effect (time from 100% back to 100%)?