JRuby OpenHAB Rules System

That sounds great actually. This way I could even attach multiple timers (but it could be any other type of objects) to a single item, giving it a name e.g. Item1[“Timer1”], Item1[“Timer2”], Item1[“RealState”] (or whatever).

Yes, if I can figure out how to persist it without leaking memory, etc. :slightly_smiling_face:
But it is just an idea, I haven’t yet considered how to engineer it.

I like .meta

Is there a way to store the objects inside JRuby’s global variable? We don’t need to access them outside JRuby obviously. So when Item1[“objectname”] is referenced, it could just return $global[Item1][“objectname”]

The global variable would result memory leakage. I want to attach it to the Item, but need it to go away if the item ever does. I might be able to just use weak references, which I have never done in Ruby only in Java.

On the topic of metadata access, I have been playing around a bit whilst learning Ruby. Here’s what I came up with:

Item definition:

Switch MyLight  ["Light"] { channel="mqtt:topic:mosquitto:mylight:power", autoupdate="false", ga="Light", AutoOff="20m", gPresenceSimulators="on" [ max_duration="5m", probability="0.05" ] }

jruby code:

    MyLight.meta.each { |namespace, value| logger.info("RUBY metadata #{namespace} = #{value} ")}
    logger.info("RUBY Value #{MyLight.meta["gPresenceSimulators"]}   ")
    logger.info("RUBY config #{MyLight.meta["gPresenceSimulators"].config}   ")
    logger.info("RUBY config['probability'] #{MyLight.meta["gPresenceSimulators"].config["probability"]}   ")

Output:

22:58:19.008 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY metadata ga = Light
22:58:19.009 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY metadata semantics = Point_Control
22:58:19.010 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY metadata AutoOff = 20m
22:58:19.012 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY metadata autoupdate = false
22:58:19.012 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY metadata gPresenceSimulators = on
22:58:19.014 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY Value on
22:58:19.019 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY config {"probability"=>"0.05", "max_duration"=>"5m"}
22:58:19.020 [INFO ] [.OpenHAB::Core::DSL::Rule::RuleConfig] - RUBY config['probability'] 0.05

@broconne is this interface similar to what you had in mind?

I haven’t tested it with nested metadata.

Apparently you could set a metadata with null as the value of the metadata, but still have configuration data. In code (jython)

    set_metadata(
        "TestItem",
        "namespace1",
        {
            "config1": {"a": {"key1": 1, "key2": 2}, "b": {"key1": 3, "key2": 4}, "c": {"xxx": "abc"},},
            "config2": "test2",
            "config3": {"test31": "xxx"},
        },
        overwrite=True,
    )

In Karaf console:

openhab> metadata list TestItem
Metadata [key=namespace1:TestItem, value=null, configuration=[config2=test2, config3={'test31': 'xxx'}, config1={'a': {'key1': 1, 'key2': 2}, 'b': {'key1': 3, 'key2': 4}, 'c': {'xxx': 'abc'}}]]

This presents an issue with the JRuby syntax TestItem.meta["namespace1"] which normally would return the value, would return nil, because although this namespace exists, the value is actually null. I’m not sure how TestItem.meta["namespace1"].config could work in this case.

One alternative I could think of is to have TestItem.metaconfig["namespace1"]

Thank you @JimT for all the documentation PRs!!!

As for the metadata, I had wanted to expose it as a hash backed directly by method calls into OpenHAB so that set would work as well as get and we would get access to important things like dig to handle potential null/missing values. My rough plan was to subclass Hash and override the [] and the []= method… But I hadn’t even started experimenting yet to see if that would work.

Based on what I saw on the layout after the namespace they will need to specify either config or value to get the element they want.

So it would be more like:

MyLight.meta[:gPresenceSimulators][:value] # Get the value for the namespace
MyLight.meta[:gPresenceSimualtors[:config][:max_duration] #Should return 5m

The reason to try and make it all a hash backed by calls to the metadata service is that we would get things like dig, merge!, each, all for free. I haven’t looked at the internals of the hash class to figure out how hard it is change the backend.

So for example if we can make that work then:

MyLight.meta.dig(:gPresenceSimualtors,:config,:max_duration,:foo,:bar) #will return nil rather than throwing an exception

FYI, while working through another item I have discovered a serious issue with how instance variables are handled based on the JRuby JSR223 design and the OpenHAB threading model. Right now they are not playing well together.

Right now the JRuby engine is configured in “threadsafe” mode. However, I believe we need to move that to “singleton” mode otherwise variables cannot be shared between rules because OpenHAB executes each rule in its own thread. See this for details on the various context types.

However, once we move to the singleton instance, every instance variable defined in a rules file ends up being global and shared by all rules files because in Ruby/JRuby when write code outside of a defined class it ends up being attached to the BasicObject class… Which is now shared across all threads. Meaning an instance variable (those with @ in front) are now global.

I have been talking to the JRuby team and there is no real elegant or obvious solution to this problem, however I am continuing to work on it.

We may end up with something like this:

rule_set do

  rule 'foo' do


  end


  rule 'bar' do

  end

end

If we need to share things between rules.

@broconne can you give an example that demonstrates the problem with instance variables?

For a simple metadata without configuration, Item1.meta[:namespace] is a nicer syntax than Item1.meta[:namespace][:value]

I’ve been converting my python rules to jruby, it’s so nice!

Firstly I love the one line require 'openhab' compared to jython where I have several lines of specific imports at the top.

Then my rules became so succinct and much easier to understand at a glance. I love the little touches like every 15.minutes, and between '08:00'..'09:00' instead of using cron expression ‘0 */15 8-9 * * ?’. I also love the guard blocks only_if and not_if.

I also love the elegant comparison when dealing with UoM e.g.

if Outdoor_Temperature < '14 °C'

Sure. without setting the jsr223 context to singleton, the following will fail:

 Scenario: Instance variables set in file are available within rules
    Given a rule
      """
      @foo = 'bar'

      rule 'read variable' do
        on_start
        run do
         logger.info("@Foo is defined: #{ instance_variable_defined?("@foo") }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'Foo is defined: true' within 5 seconds

  Scenario: Instance variables are available between rules
    Given a rule
      """
      rule 'set variable' do
        on_start
        run do
          @foo = 'bar'
          logger.info("@Foo set to #{ @foo }")
        end
      end

      rule 'read variable' do
        on_start
        delay 2.seconds
        run do
         logger.info("@Foo is defined: #{ instance_variable_defined?("@foo") }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'Foo is defined: true' within 5 seconds

In short, there is no way to share data between rules without making that data global. And I want to avoid forcing globally scoped data as it can lead to hard to debug problems.

However, once I set the JSR223 context to singleton, the following scenario fails:

 Scenario: Instance variables are not shared between rules files
    Given a deployed rule:
      """
      rule 'set variable' do
        on_start
        run do
          @foo = 'bar'
          logger.info("@Foo set to #{  @foo  }")
        end
      end
      """
    And a rule:
      """"
      rule 'read variable' do
        on_start
        run do
         logger.info("@Foo is defined: #{ instance_variable_defined?("@foo") }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'Foo is defined: false' within 5 seconds

Meaning ALL instance data is now global. Because all instance variables are being attached to the base object in ruby which is now shared by all threads.

What type of variable is the one declared outside of the rule, without being prepended $ or @? It seemed to work for me:

foo = 'bar'
rule 'read variable' do
  on_start
  run { logger.info("foo = #{foo}") }
end

For a simple metadata without configuration, Item1.meta[:namespace] is a nicer syntax than Item1.meta[:namespace][:value]

I completely agree, but there is no way to differentiate, correct?

In none of my rules do I currently work with metadata, but it seems important to provide ruby hash methods and access to the metadata. For example, if we want to set metadata on a set of items, we could use merge!.

metadata = {
  value: 'on',
  config: {
   max_duration: '5m'
   probability: '0.05'
  }    
}

# Assuming a group of presence simulators called PresenceSimulators

# This would set metadata for the entire group
PresenceSimulators.items.each { |sim| sim.meta[:gPresenceSimulators].merge! metadata }

At which point do we actually intercept the changes and send the metadata changes to openhab, and how can that be done in Ruby?

  • Item.meta[:namespace][:value] = xx
  • Item.meta[:namespace][:config][key] = xx
  • Item.meta[:namespace].merge! () # is this the same as Hash.update() ?

In my experimental implementation, Item.meta is subclassed from Hash. I can load the entire metadata that belongs to the item, into the .meta variable as a hash data. After that, it is a plain simple (subclassed) hash variable, but I don’t know how to intercept the changes to the data and send it to openhab. I can override []=, but what about nested data? It can be done by manually calling Item.meta.set() maybe, but that’s a little awkward. On the other hand, Item.meta.refresh() maybe needed too.

It is a local variable, which will get set (and reset) during the evaluation phase (when OpenHab is reading the script file). This is based on how I currently have JSR223 configured for local variables to be transient.

So this works:

  Scenario: local variables set in file are available within rules
    Given a rule
      """
      foo = 'bar'

      rule 'read variable' do
        on_start
        run do
         logger.info("foo is defined: #{ local_variables.include?(:foo) }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'foo is defined: true' within 5 seconds

But this fails:

  Scenario: local variables are available between rules
    Given a rule
      """
      rule 'set variable' do
        on_start
        run do
          foo = 'bar'
          logger.info("Foo set to #{ foo }")
        end
      end

      rule 'read variable' do
        on_start
        delay 2.seconds
        run do
         logger.info("foo is bar: #{ foo == 'bar' }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'foo is bar: true' within 5 seconds

And if we “shadow” the outer variable, it will also work. Although this should not work for you, only the version I have running locally that doesn’t make every local variable thread specific.

 Scenario: local variables are available between rules
    Given a rule
      """
      foo = 'baz'

      rule 'set variable' do
        on_start
        run do
          foo = 'bar'
          logger.info("Foo set to #{ foo }")
        end
      end

      rule 'read variable' do
        on_start
        delay 2.seconds
        run do
         logger.info("foo is bar: #{ foo == 'bar' }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'foo is bar: true' within 5 seconds

This does work correctly:

 Scenario: Local variables are not shared between rules files
    Given a deployed rule:
      """
      foo = 'baz'

      rule 'set variable' do
        on_start
        run do
          foo = 'bar'
          logger.info("foo set to #{  foo  }")
        end
      end
      """
    And a rule:
      """"
      rule 'read variable' do
        on_start
        run do
         logger.info("foo is defined: #{ local_variables.include?(:foo) }")
        end
      end
      """
    When I deploy the rule
    Then It should log 'foo is defined: false' within 5 seconds

From a ruby perspective not having instance variables working as expected will be weird, I don’t think standard memoization ( @var ||=. ) will be possible and all variables would need to be declared before usage.

I think we would like to avoid set and refresh. I don’t think we want to preload all the data from the metadata service into a hash, rather we want the hash to be fully backed by the metadata service. In other words, I think we want to override [] and []= where [] retrieves the value from the metadata service and []= sets the value.

Since we are getting into a deep design discussion here which others may not want to follow, do we want to move that over to the github issue?

I believe I have solved the variable issue, but it does require placing rules that need to share instance variables within a larger block.

Here is what I have now and works correctly, even with OpenHAB using different threads for every rule:

ruleset do

        rule 'set variable' do
          on_start
          run do
            @foo = 'bar'
            logger.info("@Foo set to #{ @foo }")
          end
        end

        rule 'read variable' do
          on_start
          delay 2.seconds
          run do
           logger.info("@Foo is defined: #{ instance_variable_defined?("@foo") }")
          end
        end

end

You would only need to put rules in a ruleset if they are going to share state within the same file. I am not in love with the name either… Other options I had considered include rule_set, rules, and scope. I am open to other names/ideas.

I think ruleset sounds fine

I agree ruleset is fine, does exactly what it says, a set of rules.

Will there then be a way to create a named set of rules? So one can extend that ruleset with an additional rule that is outside the actual file?

@broconne can we use that nice TimeOfDayRange inside the rule code, say to check whether it’s executing between certain times of day? This is not the same as the between guard, because depending on the time range, different actions may be taken. This saves us from having to create multiple rules if one can only use the between guard outside of the rule.