JRuby OpenHAB Rules System

Thanks @pacive. Rookie error! It shows I haven’t really touched OH for quite a while and just recently started upgrading/migrating.
Using the InfluxDB persistence made the error go away. So, Ruby-wise it is ok now, Although the error is a bit misleading as it refers the a method called state.

@broconne From my tests it seems previous_state is just the current state. I tested it with InfluxDB persistence. There are a few persisted states in the database:

$ influx query 'from(bucket:"openhab") |> range(start:-1d)' | grep -i light_scene_office
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:04:34.964000000Z      OFF
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:04:37.881000000Z  EVENING
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:04:43.948000000Z      OFF
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:09:30.533000000Z  EVENING
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:10:25.995000000Z      OFF
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:21:12.067000000Z  EVENING
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:21:18.004000000Z      OFF
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:21:30.059000000Z  EVENING
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:42:26.109000000Z      OFF
2023-01-03T15:42:35.220953448Z  2023-01-04T15:42:35.220953448Z   value  Light_Scene_office  Light_Scene_office  2023-01-04T15:42:29.999000000Z  EVENING

The item:

String Light_Scene_office "Lights Office" <light> (gOffice,gHist0)

The test rule:

rule 'Hue Dimmer Switch Key triggered' do
    channel [
        'dim_office:dimmer_switch_event',
        # ... etc ...
        ],
        thing: 'hue:0820:0017582dc5b3',
        triggered: ['1000.0', '1002.0']
    run do |event|
        lightScene = items['Light_Scene_' + event.channel.to_s.split(':')[3].split('_')[1]]
        logger.info "Hue #{lightScene.name} state is #{lightScene} with trigger #{event.get_event}"
        logger.info "Previous state: #{lightScene.previous_state}"
    end
end

Triggering event 1000.0 logs this:

[INFO ] [ort.loader.AbstractScriptFileWatcher] - Loading script '/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb'
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is EVENING with trigger 1000.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Previous state: EVENING

Both .state and .previous_state are the current state, so it seems. Is InfluxDB supported as persistence service for this use case? Or should I use another persistence service?

UPDATE: I removed all persistence from the Light_Scene_office item and get a correct value for .state and .previousState after changing the state a few times between ‘OFF’ and ‘EVENING’ from the Karaf shell.

[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is EVENING with trigger 1000.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Previous state: OFF
String Light_Scene_office "Lights Office" <light> (gOffice)

Looks like .previous_state comes from the OpenHAB Item object in memory in this case.
However, after toggling the status with:

lightScene << lightScene.previous_state

it changes to the previous state once and stays that way on subsequent Hue channel triggers:

[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is EVENING with trigger 1002.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Previous state: OFF
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is OFF with trigger 1002.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Previous state: OFF
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Hue Light_Scene_office state is OFF with trigger 1000.0
[INFO ] [fice.hue_dimmer_switch_key_triggered] - Previous state: OFF

What’s in the influxdb? previousState will read the latest record in the db

It should’ve been called “lastPersisted”

Would skip_equal: true give you what you want?

OK. That clarifies some of the behaviour. I would argue that this logic does not provide the previous state in the majority of cases; only at system startup and in the 100ms or so after the item is updated while the state is not yet written to the database of the persistence service… or am I missing something?

I do know that when an item state change triggers an event there is a valid previous state available as I use that in some Javascript rules. It might be related to event handling specific logic because an event is probably a separately created object, but still it means that in some flows the real previous state is stored in memory, independent from the persistence services.
This works in JSScripting (have not yet converted this to Ruby):

rules.JSRule({
    name: "HueChangeScene",
    description: "Change lighting in area to the specified scene",
    triggers: [
                triggers.ItemStateChangeTrigger('Light_Scene_Livingroom'),
                triggers.ItemStateChangeTrigger('Light_Scene_Kitchen'),
                triggers.ItemStateChangeTrigger('Light_Scene_Dining'),
                ...
            ],
    execute: (event) => {
        logger.debug("[{}] changed, new state [{}], previous state [{}]", event.itemName, event.newState, event.oldState);

I could fall-back to hacking it like storing the previous state in a label, but I really would rather not and it feels like a feature that should be available in OH…

Bottom line: there is no ‘real’ previousState available for an Item in JRuby? Or in any scripting environment because OH simply relies on the persistence service?

I’m afraid not. I want to implement a scenario where lights turn on and off with the same button and when on it uses the last active light scene (meaning the state it had before turned off).

On a changed event, you have event.was which contains the previous state. That might be what you’re after

The jruby version of that rule:

rule "HueChangeScene" do
  description "Change lighting in area to the specified scene"
  changed Light_Scene_Livingroom, Light_Scene_Kitchen, Light_Scene_Dining
  run do |event|
    logger.debug "[#{event.item.name}] changed, new state [#{event.state}], previous state [#{event.was}]"
  end
end
1 Like

You can save the previous state in the item’s metadata, e.g.

rule "Save previous state" do
  changed Light_Scene_office
  run { |event| Light_Scene_office.metadata["previous_state"] = event.was.to_s } 
end

Note I converted the event.was state to string before storing it into the metadata value, because metadata value can only store a string object.

Then you can use the metadata anywhere you need the previous state, e.g. in your channel trigger rule.

previous_state = Light_Scene_office.metadata["previous_state"].value

You could alternatively save it to an instance variable, if you don’t need it to persist.

@previous_state = nil

rule "Save previous state" do
  changed Light_Scene_office
  run { |event| @previous_state = event.was }
end

Or you can use terse rule syntax:

changed(Light_Scene_office) { |event| @previous_state = event.was }

Note I’m saving the actual state, and didn’t convert it to a string, so use it accordingly.

Then you can use the instance variable anywhere you need the previous state, e.g. in your channel trigger rule.

@previous_state

If you need to keep the previous states of multiple items, it’s easy enough into their corresponding metadata, or if you want to store them in an instance variable, store them as a hash, with the item as the key for the hash. Let me know if you want to see examples for those.

Unfortunately it is a Channel trigger and not an Item trigger.
Having said that, I might have to rethink my approach, but that would probably require a lot of extra Items to link to individual channel events or something similar… not looking forward to that.

I don’t understand it. Are the items that you need previous state of, linked to the triggered channel? Your example item definition didn’t show any channels linked to it.

These Light_Scene_* Items are ‘virtual’ items that just hold the light scene name which are predefined but user adjustable scenes (Items in a widget) that get translated by another rule into the defined brightness and colortemp for the designated light(s)).
The Channels are used to identify Hue key press/release events and trigger the rules to change these Light_Scene_* items based on previous state in the case of ON/OFF.

So the virtual light scene item holds the active scene name:

String Light_Scene_office "Lights Office" <light> (gOffice,gPersist)\

and the actual brightness and colortemp channels of the lights are like this:

Dimmer Light_Dim_Office "Brightness Office" <slider> (gLight_Brightness_office,gOffice) {channel="hue:0220:0017582dc5b3:bulb_office:brightness"}
Dimmer Light_ColorTemp_OfficeCeiling "ColorTemp Office Light [JS(i18n_hue0210_colortemp.js):%s]" <slider> (gLight_ColorTemp_Office,gOffice) {channel="hue:0220:0017582dc5b3:bulb_office:color_temperature"}

and are set by the aforementioned rule whenever the light scene item changes.

I don’t think this is a JRuby thing - if you can do it in the DSL or Python or JS you should be able to do it here. Historical states are part of OH persistence - none of the languages I think provide different built in persistence. All of the DSLs have access to the “previous state” for a changed trigger, but those are tied to the item that is triggering.

I apologize as I a am still missing something when reviewing the use case here. From the description it appears there are virtual items that the user can set via a widget. Then distinct from that there exists channels that get triggered and based on the status of that virtual item execute some action.

What is missing for me is why do you need the previous state of the virtual item? Is there another rule that is updating that state outside of the user widget? Or perhaps I am misunderstanding that when you say “previous state of ON/OFF” are you referring to the virtual item or are you referring to the dimmers or something else?

Do you a working rules in another language that you can share here? I can help with the conversion.

No worries, I was a little brief in my explanation. For full disclosure about this ‘previous state scenario’: I have not implemented the same in Jython or JavaScript (or DSL). The reason for this new scenario is that until recently I owned only original Hue Dimmer Switches that have four buttons labeled (with icons): ON, Brighter, Dim, OFF - a separate ON and OFF labeled button. The new Hue Dimmer Switches of which I acquired a few so far, have different icons as label: ON/OFF, Brighter, Dim, Hue Scenes. This triggered me to go for a ON/OFF toggle scenario for button 1 and use button 4 to change the lighting scenes (user defined scenes like EVENING, READ, WORK, MOVIE, BRIGHT,…). Until now I used repeated presses of the ON button to cycle through scenes and the OFF button for turning the lights off.
(I know I can make the buttons do anything, but following the labeling hugely increases the WAF…)

Let me try to explain the updated scenario I want to implement (only the use of button 1 and 4 is different from what I already have running in Jython and partly in JSScripting):

  1. We have Hue lights throughout the house that are dimmable and can be set to different color temperatures (ignoring a few RGB lights that can also change the color of the light).
  2. 99.9% of the time we only use a few predefined settings for the combination of brightness and color temperature. For instance, the default scene activated when the sun sets is ‘EVENING’. Besides that we can switch to a few other scenes that are partly room dependent, like ‘READ’, ‘WORK’, ‘MOVIE’, ‘BRIGHT’ (full brightness). The active scene for each light is maintained in an Item (e.g. LightScene_office) that has no channel linked.
  3. Each lighting scene exists of a preset color temperature and brightness level, also represented by Items that are linked to channels for the respective lights. I use groups because there will be a few more lights added that should be in sync. Partial example item definitions:
// --- Office light groups
String LightScene_office "Office Lights" (gHist0,gPersist)
Group:Dimmer:AVG gBrightness_office "Office Brightness" (gPersist)
Group:Dimmer gColorTemp_office "ColorTemp Office" (gPersist)
// --- Individual office lights
Dimmer Dim_office "Brightness" <slider> (gBrightness_office) {channel="hue:0220:xxxx:bulb_office:brightness"}
Dimmer ColorTemp_office "ColorTemp Office [JS(i18n_hue0210_colortemp.js):%s]" (gColorTemp_office) {channel="hue:xxxx:bulb_office:color_temperature"}
  1. Whenever a light scene changes, like from EVENING to READ, the corresponding brightness and colortemp for READ is set through a rule that gets triggered when one of the scenes change.
  2. These individual brightness and colortemp can be adjusted through the UI for each light scene. Example items:
// --- Office lighting Scene configuration
Dimmer office_Brightness_EVENING "Office brightness EVENING" (gPersist)
Dimmer office_Brightness_WORK "Office brightness WORK" (gPersist)
Dimmer office_Brightness_BRIGHT "Office brightness BRIGHT" (gPersist)
Dimmer office_ColorTemp_EVENING "Office colortemp EVENING" (gPersist)
Dimmer office_ColorTemp_WORK "Office colortemp WOEK" (gPersist)
Dimmer office_ColorTemp_BRIGHT "Office colortemp BRIGHT"(gPersist)

for references the current rule for changing the lights based on the scene item:

scenes = ["EVENING", "READ", "WORK", "BRIGHT", "MOVIE", "COSY"];

rules.JSRule({
    name: "Set Hue Scene",
    description: "Change lighting in area to the specified scene",
    triggers: [
                triggers.ItemStateChangeTrigger('Light_Scene_Living'),
                triggers.ItemStateChangeTrigger('Light_Scene_Kitchen'),
                triggers.ItemStateChangeTrigger('Light_Scene_Dining'),
                triggers.ItemStateChangeTrigger('Light_Scene_Hall'),
                ...etc...
            ],
    execute: (event) => {
        logger.info("[{}] changed, new state [{}], previous state [{}]", event.itemName, event.newState, event.oldState);
        // Get the area and scene names
        var item = event.itemName.toString();
        var index = item.lastIndexOf("_");
        if (index !== -1)
            var lightArea = item.slice(index+1)
        else {
            log.warn("Item [{}] has no area name, exiting", item);
            return;
        }
        const lightScene = event.newState;

        // Get the brightness light group of this area
        const lightGroup = "gBrightness_" + lightArea;
        logger.info("LightArea [{}], Scene [{}], LightGroup [{}]", lightArea, lightScene, lightGroup);

        // Set ColorTemo and Brights for scene
        if (lightScene == "OFF") {
            items.getItem(lightGroup).sendCommand("OFF");
            if (lightArea == "Living")
                ...something...;
        }
        else if (scenes.some(el => lightScene.includes(el))) {
            const brightnessGroup = "gBrightness_" + lightArea;
            const brightness = items.getItem(lightArea + "Brightness_" + event.newState.toString()).state;
            items.getItem(brightnessGroup).sendCommand(brightness);
            var colorTempGroup = "gColorTemp_" + lightArea;
            var colorTemp = items.getItem(lightArea + "ColorTemp_" + event.newState.toString()).state;
            items.getItem(colorTempGroup).sendCommand(colorTemp);
            if (lightArea == "Living")
                ...something...;
        }
        else {
            logger.warn("Unknown Scene command [{}] received, restore previous state [{}]", lightScene, event.oldState);
            items.getItem(event.itemName).postUpdate(event.oldState);
        }
    }
});
  1. Pressing a button on the Hue Dimmer Switch triggers a channel that will change the light scene item as I showed in earlier posts. This triggers the above rule and ultimately sets the desired colortemp and brightness. And that’s where the problem with previous_state started because only item-related events seem to have a real previous state, otherwise it is just the last persisted state - which usually is the current state of the item and not the previous state.

I know that this scenario mimics some of the built-in Hue bridge functions but with added flexibility. I didn’t want to use the Hue built-in scenes because I cannot easily control and change that from OH.

Hope this clarifies it. I’m sure there are other/better ways but this is what I came up with back in the days when DSL was all the rage. :o).
But of course, I’m always open for better/easier ways…

For complete clarity when you say in item 6 “…triggers a channel that will change the light scene…”, you are referring to this post which is noted as a simplified example? Just making sure I understand because the simplified example doesn’t seem to change the virtual item - it tries to get the previous state.

Correct! :+1:
The new JRuby script is responsible for detecting that a button is pressed on the Hue Dimmer Switch and for now only the first button (ON/OFF) as I am in the process of migrating to Ruby and implementing this new use of the ON/OFF and Hue Scene buttons as explained.
What the JRuby script should do is just swap the light scene item state (e.g. LightScene_office) between OFF and previous scene, which will result in an ON/OFF scenario. The actual changing of the lights is currently handled by the JS script that sets the colortemp and brightness based on the light scene presets per scene as I showed in my previous post.

One of the things that came to mind is to link these button channels to an item and start from there, because than I can use the event.was method. But I am a bit reluctant to add another 160+ items (each of the 4 buttons per switch can send multiple events like initial pressed, long pressed, short released, long released - but it could be an option.

I rewrote your JS scene rule in jruby here. @broconne got any suggestions for improvements?

# frozen_string_literal: true

# this is just to collect all related Scene constants in one tidy place
module Scene
  NAMES = %w[OFF EVENING READ WORK BRIGHT MOVIE COSY].freeze

  # Create each scene name as a constant, e.g. Scene::OFF, Scene::EVENING,...
  NAMES.each { |scene| const_set(scene, scene) }

  SETTINGS = %w[Brightness ColorTemp].freeze
end

module Hue
  ON_PRESSED = "1000.0"
  ON_HOLD = "1001.0"
  ON_SHORT_RELEASED = "1002.0"
  ON_LONG_RELEASED = "1003.0"
  # and so on
end

def apply_scene_to_area(scene, area)
  logger.info "Applying #{scene} Scene to #{area} Area"
  Scene::SETTINGS.each do |setting|
    scene_value = items["#{area}_#{setting}_#{scene}"]&.state
    item_to_set = items["g#{setting}_#{area}"]
    next unless item_to_set && scene_value # item_to_set is nil if it doesn't exist

    if area == "Living"
      # Do something special here?
    end
    item_to_set.ensure.command(scene_value)
  end
end

rule "Set Hue Scene" do
  description "Change lighting in area to the specified scene"
  # You could set these as members of a "gAreaScene" group and trigger on gAreaScene.members instead
  changed Light_Scene_Living, Light_Scene_Kitchen, Light_Scene_Dining, Light_Scene_Hall
  run do |event|
    logger.info "[#{event.item.name}] changed, new state [#{event.state}], previous state [#{event.was}]"

    light_area = event.item.name.split("_").last
    unless light_area # This can be combined with the above line if you prefer
      logger.warn "Item [#{event.item.name}] has no area name, exiting"
      next
    end

    new_scene = event.state.to_s

    unless Scene::NAMES.include?(new_scene)
      logger.warn "Unknown Scene command [#{new_scene}] received, restore previous state [#{event.was}]"
      event.item.update(event.was)
      next
    end

    # Save the last scene if it's not off
    event.item.metadata["last_scene"] = new_scene unless new_scene == Scene::OFF

    # No  need to have a special case for the OFF scene, instead, just have
    # a Scene item for the brightness, which is always set to OFF
    # This way, apply_scene_to_area will pick it up and apply it accordingly
    # e.g. Switch Office_Brightness_OFF
    # You don't need to have other settings (e.g. ColorTemp) for OFF.
    # apply_scene_to_area will work fine if it couldn't find the setting item

    apply_scene_to_area(new_scene, light_area)
  end
end

# You need to create the corresponding items in your .items file
# Switch Living_Brightness_OFF
# Switch Office_Brightness_OFF
# ...
rule "Initialize the OFF scene state" do
  on_start
  run do
    items.select { |item| item.name.end_with?("_Brightness_OFF") }.ensure.off
  end
end

rule "Hue Dimmer Switch Key triggered" do
  channel [
    "dim_office:dimmer_switch_event"
    # ... etc ...
  ],
          thing: "hue:0820:0017582dc5b3",
          triggered: [Hue::ON_PRESSED, Hue::ON_SHORT_RELEASED]
  run do |event|
    area_name = event.channel.to_s.split(":")[3].split("_")[1]
    scene_item = items["Light_Scene_#{area_name}"]
    unless scene_item
      logger.info "Scene item not found for area: #{area_name}"
      next
    end

    logger.info "Hue #{scene_item.name} state is #{scene_item.state} with trigger #{event.get_event}"
    last_scene = scene_item.metadata["last_scene"].value

    logger.info "Previous state: #{last_scene}"

    # We received an "ON" button press. Let's set the scene to its last scene
    scene_item.update(last_scene)
  end
end

EDIT: I’ve also saved the scene value for you here and also handle the ON button press so it would restore the scene to the last scene

2 Likes

I’m not familiar with the Hue binding and how it deals with this. My understanding from glancing at the docs, is that you can’t seem to even do this, and you’d need to use the channel trigger. :frowning_face:.

I have an IKEA Styrbar button and use Zigbee2mqtt

Zigbee2mqtt allows me to capture all the different button presses in a String item. The item will be updated with “on”, “off”, “left”, “right”, and so on. So I never had to deal with channel triggers in my rules.

In this manner, you only need one item for each physical remote control, and that one item can tell you which button is pressed.

BTW I think Zigbee2mqtt can also handle Hue buttons and bulbs? I haven’t looked into it because I don’t use any Hue stuff.

Anyway, event.was is not even relevant here, because you want the scene’s previous value, not the button’s, right?

@CrazyElectron Did @jimtng’s post work for you?

I’m going to test it this weekend. Have to be a bit careful as I already get complaints from my wife that she could not turn lights on or off while I was testing stuff…
In any case, thank you @jimtng and @broconne for your support.

In any case, I need to do some studying on Ruby as I could never have come up with this code. Back in the days (in another century…) I did a lot of programming in assembler, C, Pascal, C++, and several other languages. Nowadays I only do some programming for hobby in Python, JavaScript, Ruby and the like. :grinning:

I did a quick test (while ignore complaints from my wife about the lights) and need to do some more testing/debugging later. When changing from a scene (EVENING) to OFF I get the following log entries:

[INFO ] [on.jruby.hue_sw_office.set_hue_scene] - [Light_Scene_Livingroom] changed, new state [OFF], previous state [EVENING]
[INFO ] [on.jruby.hue_sw_office.set_hue_scene] - Applying OFF Scene to Livingroom Area
[ERROR] [obj.OpenHAB.DSL.Rules.AutomationRule] - undefined method `state' for nil:NilClass
Did you mean?  state? (NoMethodError)
In rule: Set Hue Scene
/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb:38:in `block in apply_scene_to_area'
/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb:37:in `apply_scene_to_area'
/openhab/conf/automation/jsr223/ruby/personal/hue_sw_office.rb:80:in `block in <main>'

The offending line is the 3rd line below:

  def apply_scene_to_area(scene, area)
    logger.info "JRuby Applying #{scene} Scene to #{area} Area"
    Scene::SETTINGS.each do |setting|
      scene_value = items["#{area}_#{setting}_#{scene}"].state

Sorry - I’ve updated the code that I posted above with a slight modification to avoid this.

1 Like