Skip to content

Commit

Permalink
Support providing and accessing context in rules
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Sep 2, 2023
1 parent 27f019b commit 615e4d9
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 8 deletions.
23 changes: 23 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,29 @@ rules[rule_uid].enable
rules[rule_uid].disable
```

#### Passing Values to Rules <!-- omit from toc -->

A rule/script may be given additional context/data by the caller. This additional data is available
within the rule by referring to the names of the context variable. This is applicable to both
UI rules and file-based rules.

Within the script/rule body (either UI or file rule)

```ruby
script id: "check_temp" do
if CPU_Temperature.state > maxTemperature
logger.warn "The CPU is overheating!"
end
end
```

The above script can be executed, passing it the `maxTemperature` argument from any supported
scripting language, e.g.:

```ruby
rules["check_temp"].trigger(maxTemperature: 80 | "°C")
```

### Gems

[Bundler](https://bundler.io/) is integrated, enabling any [Ruby gem](https://rubygems.org/) compatible with JRuby to be used within rules. This permits easy access to the vast ecosystem of libraries within the Ruby community.
Expand Down
19 changes: 15 additions & 4 deletions lib/openhab/core/rules/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,22 @@ def to_s
# Manually trigger the rule
#
# @param [Object, nil] event The event to pass to the rule's execution blocks.
# @return [void]
#
def trigger(event = nil)
Rules.manager.run_now(uid, false, { "event" => event })
# @param [Boolean] consider_conditions Whether to check the conditions of the called rules.
# @param [kwargs] context The context to pass to the conditions and the actions of the rule.
# @return [Hash] A copy of the rule context, including possible return values.
#
def trigger(event = nil, consider_conditions: false, **context)
begin
event ||= org.openhab.core.automation.events.AutomationEventFactory
.createExecutionEvent(uid, nil, "manual")
rescue NameError
# @deprecated OH3.4 doesn't have AutomationEventFactory
end
context.transform_keys!(&:to_s)
context["event"] = event if event # @deprecated OH3.4 - remove if guard. In OH4 `event` will never be nil
Rules.manager.run_now(uid, consider_conditions, context)
end
alias_method :run, :trigger
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/openhab/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "java"
require "method_source"
require "ruby2_keywords"

require "bundler/inline"

Expand Down Expand Up @@ -970,6 +971,24 @@ def try_parse_time_like(string)

raise exception
end

#
# Provide access to the script context / variables
# see OpenHAB::DSL::Rules::AutomationRule#execute!
#
# @!visibility private
ruby2_keywords def method_missing(method, *args)
return super unless args.empty? && !block_given?
return super unless (context = Thread.current[:openhab_context]) && context.key?(method)

logger.trace("DSL#method_missing found context variable: '#{method}'")
context[method]
end

# @!visibility private
def respond_to_missing?(method, include_private = false)
((context = Thread.current[:openhab_context]) && context.key?(method)) || super
end
end
end

Expand Down
24 changes: 20 additions & 4 deletions lib/openhab/dsl/rules/automation_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def on_removal(listener)

# This method gets called in rspec's SuspendRules as well
def execute!(mod, inputs)
# Store the context in a thread variable. It is accessed through DSL#method_missing
# which is triggered when the context variable is referenced inside the run block
@thread_locals[:openhab_context] = extract_context(inputs)
ThreadLocal.thread_local(**@thread_locals) do
if logger.trace?
logger.trace("Execute called with mod (#{mod&.to_string}) and inputs (#{inputs.inspect})")
Expand All @@ -88,6 +91,8 @@ def execute!(mod, inputs)

@run_context.send(:logger).log_exception(e)
end
ensure
@thread_locals.delete(:openhab_context)
end

def cleanup
Expand Down Expand Up @@ -159,6 +164,17 @@ def extract_event(inputs)
struct_class.new(**inputs)
end

#
# Converts inputs into context hash
# @return [Hash] Context hash.
#
def extract_context(inputs)
inputs&.reject { |key, _| key.include?(".") }
&.to_h do |key, value|
[key.to_sym, value.is_a?(Item) ? Core::Items::Proxy.new(value) : value]
end
end

#
# Get the trigger_id for the trigger that caused the rule creation
#
Expand Down Expand Up @@ -232,7 +248,7 @@ def process_task(event, task)
ThreadLocal.thread_local(**@thread_locals) do
case task
when BuilderDSL::Run then process_run_task(event, task)
when BuilderDSL::Script then process_script_task(task)
when BuilderDSL::Script then process_script_task(event, task)
when BuilderDSL::Trigger then process_trigger_task(event, task)
when BuilderDSL::Otherwise then process_otherwise_task(event, task)
end
Expand Down Expand Up @@ -296,9 +312,9 @@ def process_run_task(event, task)
#
# @param [Script] task to execute
#
def process_script_task(task)
logger.trace { "Executing script '#{name}' run block" }
@run_context.instance_exec(&task.block)
def process_script_task(event, task)
logger.trace { "Executing script '#{name}' run block with event(#{event})" }
@run_context.instance_exec(event, &task.block)
end

#
Expand Down
1 change: 1 addition & 0 deletions lib/openhab/dsl/thread_local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module ThreadLocal

# Keys to persist
KNOWN_KEYS = %i[
openhab_context
openhab_ensure_states
openhab_holiday_file
openhab_persistence_service
Expand Down
58 changes: 58 additions & 0 deletions spec/openhab/dsl/rules/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,17 @@ def self.test_event(trigger)
Switch1.on
expect(item).to be Switch1
end

it "has an implicit `event`", caller: caller do
items.build { switch_item "Switch1" }
item = nil
rule do
send(trigger, Switch1)
run { item = event.item }
end
Switch1.on
expect(item).to be Switch1
end
end

test_event(:changed)
Expand All @@ -1502,6 +1513,21 @@ def self.test_event(trigger)
end
expect(ran).to be 3
end

it "has access to its context variables" do
items.build { switch_item TestSwitch }
received_context = nil
rule do
received_command TestSwitch
run do
# `command` was injected by ItemCommandTriggerHandler
received_context = command
end
end

TestSwitch.on
expect(received_context).to be ON
end
end

describe "#delay" do
Expand All @@ -1517,6 +1543,25 @@ def self.test_event(trigger)
time_travel_and_execute_timers(10.seconds)
expect(executed).to be 2
end

it "has access to its context in subsequent run blocks" do
items.build { switch_item TestSwitch }
received_context = nil
rule do
received_command TestSwitch
run {} # rubocop:disable Lint/EmptyBlock
delay 0.1.seconds
run do
# `command` was injected by ItemCommandTriggerHandler
received_context = command
end
end

TestSwitch.on
expect(received_context).to be_nil
time_travel_and_execute_timers(0.2.seconds)
expect(received_context).to be ON
end
end

describe "triggered" do
Expand Down Expand Up @@ -1706,6 +1751,19 @@ def self.test_combo(only_if_value, not_if_value, result)
expect(this).to be self
end

it "has access to its context variables" do
items.build { switch_item Switch1 }
received_context = nil
rule do
received_command Switch1
# `command` was injected by ItemCommandTriggerHandler
only_if { received_context = command }
run {} # rubocop:disable Lint/EmptyBlock
end
Switch1.on
expect(received_context).to be ON
end

describe "#between" do # rubocop:disable RSpec/EmptyExampleGroup
def self.test_it(range, expected)
it "works with #{range.inspect} (#{range.begin.class})", caller: caller do
Expand Down
20 changes: 20 additions & 0 deletions spec/openhab/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@
rules["testscript"].trigger
expect(triggered).to be true
end

it "can access its context" do
received_context = nil
script id: "testscript" do
received_context = foo
end

rules["testscript"].trigger(nil, foo: "bar")
expect(received_context).to eql "bar"
end
end

describe "#scenes" do
Expand All @@ -94,6 +104,16 @@
rules.scenes["testscene"].trigger
expect(triggered).to be true
end

it "can access its context" do
received_context = nil
scene id: "testscene" do
received_context = foo
end

rules["testscene"].trigger(nil, foo: "bar")
expect(received_context).to eql "bar"
end
end

describe "#store_states" do
Expand Down

0 comments on commit 615e4d9

Please sign in to comment.