I’ve only started looking at / using Ruby about 2 years ago when @broconne first introduced his concept jruby helper library here in this very thread (see the first few posts at the beginning!). Prior to that, I had never written a single line of Ruby code. I found that Ruby is really really nice and it is now my most favourite language. I still have a lot to learn.
Hi @jimtng, I’m having some issues with the metadata. Initially, I get the following error:
[INFO ] [ort.loader.AbstractScriptFileWatcher] - Loading script '/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb'
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Event [hue:0820:2:diml_bedroom:dimmer_switch_event, 1002.0]; area [bedroom]; scene item [Scene_bedroom, OFF]
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Scene metadata: {}
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `value' for nil:NilClass (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:119:in `block in <main>'
because there is no metadata yet (that only happens when actually changing scenes in the other rule).
To quickly test this rule, I added metadata to an item via the Karaf console and tried again. This gives the same error:
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Event [hue:0820:1:dim_toilet:dimmer_switch_event, 1002.0]; area [toilet]; scene item [Scene_toilet, OFF]
[INFO ] [itch.hue_dimmer_switch_key_triggered] - Scene metadata: {"last_state"=>["EVENING",{}]}
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `value' for nil:NilClass (NoMethodError)
In rule: Hue Dimmer Switch Key triggered
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:119:in `block in <main>'
The rule handling the key channel trigger:
rule "Hue Dimmer Switch Key triggered" do
channel [
'dim_office:dimmer_switch_event',
'dim_toilet:dimmer_switch_event',
'diml_bedroom:dimmer_switch_event'
# etc ...
],
thing: ['hue:0820:1', 'hue:0820:2'],
triggered: [Hue::ON_SHORT_RELEASED]
run do |event|
area_name = event.channel.to_s.split(":")[3].split("_")[1]
scene_item = items["Scene_#{area_name}"]
logger.info "Event [#{event.channel.to_s}, #{event.get_event}]; area [#{area_name}]; scene item [#{scene_item.name}, #{scene_item}]"
unless scene_item
logger.info "Scene item not found for area: #{area_name}"
next
end
logger.info "Scene metadata: #{scene_item.metadata}"
last_scene = scene_item.metadata["last_scene"].value
logger.info "Scene previous state: #{last_scene}"
scene_item.update(last_scene)
end
end
The offending line is last_scene = scene_item.metadata["last_scene"].value.
Do you have any suggestions?
Thank you for all your support. With a minor tweak, I have it working now. Since it would not turn the lights (it always gets updated with the last_scene state) off, I changed the line that updated the scene state item to:
My next challenge is to use a ‘long press’ off the ON/OFF key to turn more/other lights on/off in a similar fashion. One thing that I could not figure out yet is how to ‘debounce’ the channel triggers for the ON_HOLD event. This event is fired every second (after a short delay) while the ON/OFF key is pressed, resulting in multiple runs of the same rule. In the old days :o) I would have resorted to an expire timer and ignore additional events for 5 or more seconds, but I think there is a more elegant way possible with JRuby. I’ve seen some things for items but not for channels.
For now I have set the trigger to ON_LONG_RELEASE but that means that no action takes place until you release the key, which is not very user friendly because it requires some sense of timing to know how long to keep the key pressed because there is no feedback while pressing the key.
So this rule is supposed to toggle between OFF and the last scene? I had assumed it was just to turn it on, and another rule reacting to a different button would turn the scene to off.
If you just want the ON_HOLD to trigger once and ignore it until ON_LONG_RELEASE, try this: (as usual, I haven’t tested this…)
# Stores the current "hold" state of the channel
@current_state = {}
rule "Hue Dimmer Switch Key triggered" do
channel [
'dim_office:dimmer_switch_event',
'dim_toilet:dimmer_switch_event',
'diml_bedroom:dimmer_switch_event'
# etc ...
],
thing: ['hue:0820:1', 'hue:0820:2'],
triggered: [Hue::ON_HOLD, Hue::ON_LONG_RELEASE]
run do |event|
next if @current_state[event.channel] == event.event
@current_state[event.channel] = event.event
next unless event.event == Hue::ON_HOLD
# operate other lights here?
end
end
With the provided example, I made a lot of progress and most scenario’s work well. Thanks again, @jimtng. One issue that pops up is an error with multi-threading. Sometimes it throws the following error:
[INFO ] [e_switch.hue_dimmer_key_4_triggered] - Event [hue:0820:2:dim_office:dimmer_switch_event, key pressed 4002.0]; switch area [office]
[INFO ] [e_switch.hue_dimmer_key_4_triggered] - Determine next scene from [#<Enumerator:0x69e163ad>], current=EVENING
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - fiber called across threads (FiberError)
In rule: Hall Dimmer Key 4 triggered
uri:classloader:/jruby/kernel/enumerator.rb:95:in `next'
uri:classloader:/jruby/kernel/enumerator.rb:17:in `next'
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:107:in `next_scene'
/openhab/conf/automation/jsr223/ruby/personal/hue_switch.rb:303:in `block in <main>'
Looks like the enumerator module is not thread-safe?
The rule and method/function involved:
module Scene
OFFICE_LIST = %w[EVENING READ BRIGHT].cycle
...
end
def next_scene(current, scenes)
logger.info "Determine next scene from [#{scenes}], current=#{current}"
next_scene = ""
next_scene = scenes.next until current == next_scene
return scenes.next
end
rule "Hue Dimmer Key 4 triggered" do
channel [
'dim_office:dimmer_switch_event',
...
],
thing: ['hue:0820:1', 'hue:0820:2'],
triggered: [Hue::HUE_SHORT_RELEASED, Hue::HUE_LONG_RELEASED]
run do |event|
area = event.channel.to_s.split(":")[3].split("_")[1]
key_pressed = event.get_event
logger.info "Event [#{event.channel.to_s}, key pressed #{key_pressed}]; switch area [#{area}]"
scene = items['Scene_'+area].state
if event.get_event == Hue::HUE_SHORT_RELEASED
unless scene == 'OFF'
new_scene = next_scene(scene, Scene::OFFICE_LIST) if area == 'office'
...
items['Scene_'+area] << new_scene
end
elsif event.event == Hue::HUE_LONG_RELEASED
items['Scene_'+area] << 'EVENING' unless scene == 'OFF'
end
end
end
Line 107 is next_scene = scenes.next until current == next_scene.
@scenes_mutex = Mutex.new
def next_scene(current, scenes)
@scenes_mutex.synchronize do
logger.info "Determine next scene from [#{scenes}], current=#{current}"
next_scene = ""
next_scene = scenes.next until current == next_scene
scenes.next
end
end
And here’s a version that doesn’t involve #cycle nor a mutex
module Scene
OFFICE_LIST = %w[EVENING READ BRIGHT] # Note I removed cycle here, so it's just a simple array
...
end
def next_scene(current, scenes)
logger.info "Determine next scene from [#{scenes}], current=#{current}"
scenes = scenes + [scenes.first]
scenes[scenes.find_index(current) + 1]
end
I also changed your code a bit to (my) preferred style
rule "Hue Dimmer Key 4 triggered" do
channel [
'dim_office:dimmer_switch_event',
...
],
thing: ['hue:0820:1', 'hue:0820:2'],
triggered: [Hue::HUE_SHORT_RELEASED, Hue::HUE_LONG_RELEASED]
run do |event|
area = event.channel.to_s.split(":")[3].split("_")[1]
key_pressed = event.event
logger.info "Event [#{event.channel}, key pressed #{key_pressed}]; switch area [#{area}]"
scene_item = items["Scene_#{area}"]
scene = scene_item.state
next if scene == Scene::OFF # assuming this is defined in the Scene module...
case key_pressed
when Hue::HUE_SHORT_RELEASED
new_scene = next_scene(scene, Scene::OFFICE_LIST) if area == 'office'
...
scene_item << new_scene
when Hue::HUE_LONG_RELEASED
scene_item << Scene::EVENING
end
end
end
module Area
AREAS = %w[office living bedroom].freeze
AREAS.each { |area| const_set(area.upcase, area) }
def self.item_for(area)
items["Scene_#{area}"]
end
end
...
scene_item = Area.item_for(area)
........ if area == Area::OFFICE
While we’re at it, I’d rename Area to Zone
Taking it further, I’d use the semantic model Location instead of using “areas”
I agree that it can be improved a lot. It’s age is showing (originally created a long time ago with DSL rules, moved to Jython at some point and partly to JS).
I’ll try to find some time to restructure the code a bit (and the naming). I already made it more flexible by using hashmaps (dicts) for the different zones and applicable scenes.
Ultimately I would like to make it integrated with Hue scenes and groups so it will be much more interactive and responsive, but that will be quite a task…
@CrazyElectron I was just reading the docs and came across this:
trigger-event-stringprofile: Trigger String This profile can be used to link a trigger channel to a String item. The item’s state will be updated to the string representation of the triggering event (e.g. PRESSED).
So you could convert your channel triggers into item triggers, if you like.
Thanks @jimtng , that sounds promising! I’m going to have a look (and it just might solve the issue where channel trigger events for Hue are sometimes missed).
I’m currently playing with JRuby, since the RulesDSL meanwhile don’t offer me enough options to develop more complex “processes”. It works, but it’s no fun. So I’m looking for an alternative and ended up with JavaScript or JRuby. I really like JRuby, it’s slim and offers a few more options than RulesDSL.
However.
RulesDSL with the openHab Extension for Visual Studio are very comfortable to implement and to maintain/debug.
E.g. if you move the mouse over an item you get a tool tip of the items status.
So I’m wondering, what editor/IDE are you using for your JRuby rules?
I use vscode. I don’t however, utilise the openhab extension for vscode. I usually run the karaf console inside vscode’s terminal window, and would just type status ItemName but I don’t need to do this very often.
What I do more often is have a “test.rb” file that I would frequently wipe clean and just do short scripts to test something, and issue logger.warn / info to tell me various things that I’d like to know during runtime.
Are you trying to get the average value of RCT_Power_Solar within the hour? So if it’s currently 13:25, you only want to know the average for the past 25 minutes, and if it’s 13:01, you only want to know the average for the past 1 minute?
Questions:
I’m curious to know what averageSolarPower / 60 * minute means, given the above.
What is basicPowerConsumption - does it have a unit? Is it Watt, or something else?
What is WeatherLocalHours01SolarHarvest - I assume it’s an item. Is it a plain item, or one with a dimension?
Are you trying to get the average value of RCT_Power_Solar within the hour? So if it’s currently 13:25, you only want to know the average for the past 25 minutes, and if it’s 13:01, you only want to know the average for the past 1 minute?
Yes indeed, that’s my target.
Before I will answer your questions.
The main target of this rule should be to calculate how much power the solar-plant has produced and compare it to the forecasted solar-“harvest”.
I’m curious to know what averageSolarPower / 60 * minute means, given the above.
The result is the energy(Wh) calculated up to 1 hour.
The item
Number WeatherLocalHours01SolarHarvest "Solarharvest in 1 hour [%.0f Wh]" <solarplant>
gives me the forecasted solar-harvest (Wh) for the next hour.
And now I’m able to calculate/compare the two values.
basicPowerConsumption = 300
Is not used yet. You can ignore, but to feed you curiosity: This is round about the basic power consumption of or house.