JRuby OpenHAB Rules System

I use rubocop routinely. There’s not really anything you need to specifically customize for openHAB, besides adjusting the TargetRubyVersion for the version of JRuby you’re running (2.6 for openHAB 3.4, 3.1 for 4.0 and 4.1). I started making an extension to rubocop for making suggestions for better usage of the helper library (rubocop-openhab-scripting), but it’s been low priority and not touched in years, so can’t really recommend it at this point. Overall it’s really up to your personal preferences how you configure rubocop. If you’re not familiar enough with Ruby to develop your own preferences, the Rubocop defaults are a reasonable place to start. You could also look at standardrb or the helper library’s configuration.

My one thought is to be careful of the Style/NumericPredicate cop. I have many instances of received_command rules that compare the command to 0, but I can’t use #zero? because it might also be say REFRESH or STOP which don’t respond to #zero?, so the cop’s recommendation is wrong.

Thanks for your reply.

For now I’m using rubocopy with the defaults.
The most suggestions are very helpfull. I learnd a few things.

But sometimes its a little strange. Example:

next if light.off?

will often replaced by

if light.off?
  next
end

I would prefer the first version… And I haven’t figured out, in which case he replaces the first…

Thanks for the hint with the Style/NumericPredicate.
I even saw the suggestion a few times, I will be careful with it.

https://docs.rubocop.org/rubocop/cops_style.html#styleifunlessmodifier

Maybe it relates to
https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength

The default is set to 120 so it should be plenty.

This is my conf/.rubocop.yml file:

AllCops:
  NewCops: enable
  # TargetRubyVersion: 2.6
  TargetRubyVersion: 3.1
  # SuggestExtensions: false
  Exclude:
   - "automation/lib/ruby/gem_home/**/*"

Style/ClassVars:
  Enabled: false

Metrics:
  Enabled: false

Style/Documentation:
  Enabled: false

Style/HashSyntax:
  EnforcedStyle: ruby19_no_mixed_keys
Style/PercentQLiterals:
  EnforcedStyle: upper_case_q
Style/PerlBackrefs:
  Enabled: false
Style/RescueStandardError:
  EnforcedStyle: implicit
Style/SpecialGlobalVars:
  Enabled: false
Style/StringLiterals:
  EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
  EnforcedStyle: double_quotes
Style/TernaryParentheses:
  EnforcedStyle: require_parentheses_when_complex
Style/Alias:
  EnforcedStyle: prefer_alias_method

I have just gotten around to upgrading to OH4 and JRuby 5. I have one last issue I can’t sort out.

Some of my rules use persistence items such as:

avg_solar_power = EnphaseDetails_EnphaseProductionNow.average_since(15.minutes, :influxdb) || QuantityType.new("0 W")
avg_grid_export = EnphaseDetails_EnphaseNetConsumptionNow.average_since(15.minutes, :influxdb) || QuantityType.new("0 W")
avg_consumption = EnphaseDetails_EnphaseConsumptionNow.average_since(15.minutes, :influxdb)

When this rule loads, I get the following error in the logs:

2024-02-16 08:45:00.447 [ERROR] [mation.jrubyscripting.rule.pool.rb:4] - undefined method `to_zoned_date_time' for PT15M:Java::JavaTime::Duration (NoMethodError)
uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/delegate.rb:87:in `method_missing'
/openhab/conf/automation/jsr223/personal/pool.rb:9:in `block in <main>'

I am at a bit of a loss at the moment. any help would be great.

You need to use 15.minutes.ago, not 15.minutes.

1 Like

Thanks heaps :slight_smile: - simple fix, much appreciated

This was one of the breaking changes going into 5.x

See: Release v5.0.0 · openhab/openhab-jruby · GitHub

Full change logs see File: CHANGELOG — openHAB JRuby

Hey there! I have some trouble with JSON-Path in ruby.

I have this JSON String, I receive it via execute_command_line within a rule.

{"inverters":[],"total":{"Power":{"v":0,"u":"W","d":0},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":0}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}

In the rule I want to have the Power value from the JSONPath. But I dont get it out of the whole JSON string.

I tried with:

power_value = transform(:JSONPATH, '$.total.Power.v', json_string)

Am I doing something wrong? Is there an elegant way to extract the values from the JSON string?

I want to use it in the rule directly for now.

There’s nothing wrong that I can see. What was the error (message)?

Use lowercase, by convention. The uppercase symbol still works though.
power_value = transform(:jsonpath, '$.total.Power.v', json_string)

In any case, it works for me

json_string = '{"inverters":[],"total":{"Power":{"v":0,"u":"W","d":0},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":0}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}'
power_value = transform(:jsonpath, "$.total.Power.v", json_string)

logger.info power_value

You may also want to consider taking into account the provided unit, something like this:

json_string = '{"inverters":[],"total":{"Power":{"v":0,"u":"W","d":0},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":0}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}'
power_value = transform(:jsonpath, "$.total.Power.v", json_string)
power_unit = transform(:jsonpath, "$.total.Power.u", json_string)
power = QuantityType.new("#{power_value} #{power_unit}")

logger.info power

Alternatively you can use Ruby’s JSON parser

require "json"

json_string = '{"inverters":[],"total":{"Power":{"v":0,"u":"W","d":0},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":0}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}'
power = JSON.parse(json_string).dig("total", "Power")
power = power["v"] | power["u"]

logger.info power

Just FYI: Executing a command line in Ruby

Thanks a lot again. It pushed me in the right direction.

My fault was to use execute_command_line. It returns not only the JSON-string.

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   202  100   202    0     0   1311      0 --:--:-- --:--:-- --:--:--  1320
{"inverters":[],"total":{"Power":{"v":0,"u":"W","d":0},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":0}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}

But using send_http_get_request instead of execute_command_line leads to success.

I will take a closer look to Ruby’s JSON parser, havent knew it before.

Alrighty, I tell the truth now… I have spent 45 minutes setting up RuboCop and I have absolutely no clue what to do. :smiley: I thought I only need to install it as VSC addon together with Ruby LSP and all is set but nope… It looks like it is a bit more complicated.
Hope someone can help me out.

What OS is your openhab running on?
How do you edit your rules:

  • using vscode + ssh extension (so you’re working “locally” on the openhab server), or
  • accessing them via samba shares, so you’re working on your computer. If this is the case, what OS is your computer?

openHAB runs on openHABian LXC. rules/items I access via a samba share and edit them on my PC using VSC. OS of the PC is Windows 10.

I access the samba as user “openhabian” if this is of any interest.

I don’t have any experience using this in Windows, but several suggestions:

  • Install rbenv (or chruby or any other ruby version manager of your choice)

    • Try accessing the conf folder via Remote - SSH vscode extension. This way you are working on linux / openhab directly. If this is the case, then install rbenv on linux. Probably easiest using the git method, followed by installing ruby-build as plugin.

    • Otherwise, if you choose to continue using samba, you’ll be working on Windows. Perhaps you’d prefer this if you aren’t familiar with Linux. In this case install rbenv for windows

  • Then using rbenv, install ruby 3.1 rbenv install 3.1.4

  • Create a file called Gemfile and add:

    source "https://rubygems.org"
    gem "rubocop", "~> 1.60"
    
  • Then type bundle install

  • Then in your vscode, restart ruby LSP

Let me know how you go.

At this point rubocop should work I think, and ruby lsp will auto format your code and warn you of any errors. You would next create a .rubocop.yml file to tweak your rubocop settings.

This is mine (not necessary an exemplary one, but it works for me)

AllCops:
  NewCops: enable
  TargetRubyVersion: 3.1
  Exclude:
   - "automation/ruby/.gem/**/*"

Style/ClassVars:
  Enabled: false

Metrics:
  Enabled: false

Style/Documentation:
  Enabled: false

Style/HashSyntax:
  EnforcedStyle: ruby19_no_mixed_keys
Style/PercentQLiterals:
  EnforcedStyle: upper_case_q
Style/PerlBackrefs:
  Enabled: false
Style/RescueStandardError:
  EnforcedStyle: implicit
Style/SpecialGlobalVars:
  Enabled: false
Style/StringLiterals:
  EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
  EnforcedStyle: double_quotes
Style/TernaryParentheses:
  EnforcedStyle: require_parentheses_when_complex
Style/Alias:
  EnforcedStyle: prefer_alias_method

So, for now I’d like to stick to the samba way. If I can’t get it running at all then we shall have a deeper look into the Remote SSH way.

So here is what I did and what worked:

  • Install rbenv for Windows
  • Install ruby 3.1 with rbenv
  • Installed the Gems
  • Added rbenv to the PATH variable

Now, when I go into VSC and restart the Ruby LSP I receive errors that neither rbenv nor ruby are known commands (although in Powershell I can type these commands). I did several restarts of VSC. Some (German) screenshots:

image

I’m not too familiar with windows anymore. How did you add the command to the PATH variable? Might need to log out / log back in to windows for this to take effect for VSC?

Hey. I’m total lost with the configuration of an HTTP action…

I’m trying to set a value, therefore I have done the following:

    headers = {"Authorization" => "\"[USER]:[PASSWORD]\""}
    HTTP.send_http_post_request("http://192.168.XXX.YYY/api/power/config", "application/json", "\"data={\"serial\":\"#{converter_id}\",\"power\":\"0\"}\"", headers, 5000) 

If the rule is executed, I receive:

08:12:58.868 [ERROR] [utomation.jrubyscripting.rule.test:12] - wrong number of arguments (given 4, expected 1…3) (ArgumentError)
/etc/openhab/automation/jsr223/ruby/test.rb:16:in `block in ’

Line 16 is the code line with the HTTP action.

Must be something simple, but I don’t see it.

Hint!
I’m able to execute the call with curl from inside the openhab vm and its working:

curl -u “[USER]:[PASSWORD]” http://192.168.XXX.YYY/api/power/config -d “data={“serial”:”[NUMBER]“,“power”:“0”}”

The problem seems to be your quoting. Original:
"\"data={\"serial\":\"#{converter_id}\",\"power\":\"0\"}\""

  • You should probably remove the outer double quotes like this:
    "data={\"serial\":\"#{converter_id}\",\"power\":\"0\"}"

  • Second tip, when you have nested quotes but still need variable interpolation, use Ruby’s percent string syntax
    %Q(data={"serial":"#{converter_id}","power":"0"})

    and if you don’t need interpolation, just use single quotes on the outside and double quotes inside or vice versa.

  • Third tip, perhaps the following code would look cleaner. The difference in my code is power is an integer here because I think it probably should be that way but if it needs to be a string, simply change it to power: "0" in the code below.

More:

  • Headers can use symbolic keys, i.e.: { Authorization: "[USER]:[PASSWORD]" }. The value of Authorization probably doesn’t need those extra quotes either, so it should be just literally USER:PASSWORD
  • When you used curl -u user:password it actually creates a basic authentication header. It is NOT the same as simply adding Authorization: user:password header. See RFC7617.
  • I suspect that your content should be just the plain json, without data=
require "json"
require "base64"

content_type = "application/json"
authorization = "USER:PASSWORD"
headers = { Authorization: "Basic #{Base64.encode64(authorization)}" }
data = { serial: converter_id, power: 0 }
content = data.to_json # or "data=#{data.to_json}" if it needs data=
HTTP.send_http_post_request("http://192.168.XXX.YYY/api/power/config", content_type, content, headers: headers, timeout: 5.seconds)

Thanks a lot for your guidance and your explanations. That helps a lot, but the problem stays.

Im still getting the error from above, saying that the given arguments are not matching the expected at this method.

Here is my rule, tried your code:

require "json"
require "base64"

rule "Testrule" do
  received_command TestSwitch
  run do
    converter_id = "123456789"
    content_type = "json"
    authorization = "USER:PASSWORD"
    headers = { Authorization: "Basic #{Base64.encode64(authorization)}" }
    data = { serial: converter_id, power: 0 }
    content = data.to_json # or "data=#{data.to_json}" if it needs data=
    HTTP.send_http_post_request("http://192.168.XXX.YYY/api/power/config", content_type, data, headers, 5000)
  end
end

The error:

18:15:47.137 [ERROR] [automation.jrubyscripting.rule.test:6] - wrong number of arguments (given 5, expected 1…3) (ArgumentError)
/etc/openhab/automation/jsr223/ruby/test.rb:15:in `block in ’

Line 15 is the line with the HTTP action.

My bad. I’ve updated the code above. See Class: OpenHAB::Core::Actions::HTTP — openHAB JRuby