Skip to content
This repository has been archived by the owner on Sep 6, 2023. It is now read-only.

Commit

Permalink
feat(rules): improve rule name inference
Browse files Browse the repository at this point in the history
 * push it down into rule itself, not just terse, but catch rules
   that only have a single inferrable trigger
 * support duration on item triggers
 * support "every" triggers
 * support channel triggers
 * support multiple things for many parameters
  • Loading branch information
ccutrer committed Jul 27, 2022
1 parent a7c0c09 commit a0b30ec
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 49 deletions.
133 changes: 133 additions & 0 deletions lib/openhab/dsl/rules/name_inference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# frozen_string_literal: true

module OpenHAB
module DSL
module Rules
# Contains helper methods for inferring a rule name from its triggers
module NameInference
# Trigger Type UIDs that we know how to generate a name for
KNOWN_TRIGGER_TYPES = [
'core.ChannelEventTrigger',
'core.GroupCommandTrigger',
'core.GroupStateChangeTrigger',
'core.GroupStateUpdateTrigger',
'core.ItemCommandTrigger',
'core.ItemStateChangeTrigger',
'core.ItemStateUpdateTrigger',
Triggers::Cron::CRON_TRIGGER_MODULE_ID
].freeze
private_constant :KNOWN_TRIGGER_TYPES

class << self
# get the block's source location, and simplify to a simple filename
def infer_rule_id_from_block(block)
file = File.basename(block.source_location.first)
"#{file}:#{block.source_location.last}"
end

# formulate a readable rule name such as "TestSwitch received command ON" if possible
def infer_rule_name(config) # rubocop:disable Metrics
known_triggers, unknown_triggers = config.triggers.partition do |t|
KNOWN_TRIGGER_TYPES.include?(t.type_uid)
end
return nil unless unknown_triggers.empty?

cron_triggers = known_triggers.select { |t| t.type_uid == 'jsr223.jruby.CronTrigger' }
ruby_every_triggers = config.ruby_triggers.select { |t| t.first == :every }

# makes sure there aren't any true cron triggers cause we can't format them
return nil unless cron_triggers.length == ruby_every_triggers.length
return nil unless config.ruby_triggers.length == 1

infer_rule_name_from_trigger(*config.ruby_triggers.first)
end

private

# formulate a readable rule name from a single trigger if possible
def infer_rule_name_from_trigger(trigger, items, kwargs)
case trigger
when :every
infer_rule_name_from_every_trigger(items, **kwargs)
when :channel
infer_rule_name_from_channel_trigger(items, **kwargs)
when :changed, :updated, :received_command
infer_rule_name_from_item_trigger(trigger, items, kwargs)
end
end

# formulate a readable rule name from an item-type trigger
def infer_rule_name_from_item_trigger(trigger, items, kwargs) # rubocop:disable Metrics
kwargs.delete(:command) if kwargs[:command] == [nil]
return unless items.length <= 3 &&
(kwargs.keys - %i[from to command duration]).empty?
return if kwargs.values_at(:from, :to, :command).compact.any? do |v|
next false if v.is_a?(Array) && v.length <= 4 # arbitrary length
next false if v.is_a?(Range)

v.is_a?(Proc) || v.is_a?(Enumerable)
end

trigger_name = trigger.to_s.tr('_', ' ')
item_names = items.map do |item|
if item.is_a?(GroupItem::GroupMembers)
"#{item.group.name}.members"
else
item.name
end
end
name = "#{format_beginning_of_sentence_array(item_names)} #{trigger_name}"

name += " from #{format_inspected_array(kwargs[:from])}" if kwargs[:from]
name += " to #{format_inspected_array(kwargs[:to])}" if kwargs[:to]
name += " #{format_inspected_array(kwargs[:command])}" if kwargs[:command]
name += " for #{kwargs[:duration]}" if kwargs[:duration]
name.freeze
end

# formulate a readable rule name from an every-style cron trigger
def infer_rule_name_from_every_trigger(value, at:)
name = "Every #{value}"
name += " at #{at}" if at
name
end

# formulate a readable rule name from a channel trigger
def infer_rule_name_from_channel_trigger(channels, triggers:)
triggers = [] if triggers == [nil]
name = "#{format_beginning_of_sentence_array(channels)} triggered"
name += " #{format_inspected_array(triggers)}" unless triggers.empty?
name
end

# format an array of words that will be the beginning of a sentence
def format_beginning_of_sentence_array(array)
result = format_array(array)
if array.length > 2
result = result.dup
result[0] = 'A'
result.freeze
end
result
end

# format an array of items that need to be inspected individually
def format_inspected_array(array)
return array.inspect if array.is_a?(Range)

array = [array] unless array.is_a?(Array)
format_array(array.map(&:inspect))
end

# format an array of words in a friendly way
def format_array(array)
return array[0] if array.length == 1
return "#{array[0]} or #{array[1]}" if array.length == 2

"any of #{array.join(', ')}"
end
end
end
end
end
end
17 changes: 8 additions & 9 deletions lib/openhab/dsl/rules/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative 'rule_config'
require_relative 'automation_rule'
require_relative 'guard'
require_relative 'name_inference'

module OpenHAB
#
Expand All @@ -34,12 +35,6 @@ class << self

module_function

# get the block's source location, and simplify to a simple filename
def self.infer_rule_id_from_block(block)
file = File.basename(block.source_location.first)
"#{file}:#{block.source_location.last}"
end

#
# Create a new rule
#
Expand All @@ -48,18 +43,22 @@ def self.infer_rule_id_from_block(block)
#
#
def rule(rule_name = nil, id: nil, script: nil, &block) # rubocop:disable Metrics
id ||= Rule.infer_rule_id_from_block(block)
rule_name ||= id
id ||= NameInference.infer_rule_id_from_block(block)
script ||= block.source rescue nil # rubocop:disable Style/RescueModifier

OpenHAB::Core::ThreadLocal.thread_local(RULE_NAME: rule_name) do
@rule_name = rule_name

config = RuleConfig.new(rule_name, block.binding)
config = RuleConfig.new(block.binding)
config.uid(id)
config.instance_exec(config, &block)
config.guard = Guard::Guard.new(run_context: config.caller, only_if: config.only_if,
not_if: config.not_if)

rule_name ||= NameInference.infer_rule_name(config)
rule_name ||= id

config.name(rule_name)
logger.trace { config.inspect }
process_rule_config(config, script)
end
Expand Down
7 changes: 5 additions & 2 deletions lib/openhab/dsl/rules/rule_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class RuleConfig
# @return [Object] object that invoked rule method
attr_accessor :caller

# @return [Array] Of trigger definitions as passed in Ruby
attr_reader :ruby_triggers

#
# Struct holding a run block
#
Expand Down Expand Up @@ -73,10 +76,10 @@ class RuleConfig
# @param [Object] caller_binding The object initializing this configuration.
# Used to execute within the object's context
#
def initialize(rule_name, caller_binding)
def initialize(caller_binding)
@rule_triggers = RuleTriggers.new
@caller = caller_binding.eval 'self'
name(rule_name)
@ruby_triggers = []
enabled(true)
on_start(false)
end
Expand Down
31 changes: 2 additions & 29 deletions lib/openhab/dsl/rules/terse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ module Rules
module TerseRule
%i[changed channel cron every updated received_command].each do |trigger|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
def #{trigger}(*args, name: nil, **kwargs, &block) # def changed(*args, name: nil, **kwargs, &block)
name ||= infer_rule_name(#{trigger.inspect}, args, kwargs) # name ||= infer_rule_name(:changed, args, kwargs)
id = Rule.infer_rule_id_from_block(block) # id = Rule.infer_rule_id_from_block(block)
name ||= id # name ||= id
def #{trigger}(*args, name: nil, id: nil, **kwargs, &block) # def changed(*args, name: nil, id: nil, **kwargs, &block)
id ||= NameInference.infer_rule_id_from_block(block) # id ||= NameInference.infer_rule_id_from_block(block)
script = block.source rescue nil # script = block.source rescue nil
rule name, id: id, script: script do # rule name, id: id, script: script do
#{trigger}(*args, **kwargs) # changed(*args, **kwargs)
Expand All @@ -20,31 +18,6 @@ def #{trigger}(*args, name: nil, **kwargs, &block) # def changed(*arg
module_function #{trigger.inspect} # module_function :changed
RUBY
end

private

# formulate a readable rule name such as "TestSwitch received command ON" if possible
def infer_rule_name(trigger, args, kwargs) # rubocop:disable Metrics
return unless %i[changed updated received_command].include?(trigger) &&
args.length == 1 &&
(kwargs.keys - %i[from to command]).empty?
return if kwargs[:from].is_a?(Enumerable)
return if kwargs[:to].is_a?(Enumerable)
return if kwargs[:command].is_a?(Enumerable)

trigger_name = trigger.to_s.tr('_', ' ')
name = if args.first.is_a?(GroupItem::GroupMembers) # rubocop:disable Style/CaseLikeIf === doesn't work with GenericItem
"#{args.first.group.name}.members #{trigger_name}"
elsif args.first.is_a?(GenericItem)
"#{args.first.name} #{trigger_name}"
end
return unless name

name += " from #{kwargs[:from].inspect}" if kwargs[:from]
name += " to #{kwargs[:to].inspect}" if kwargs[:to]
name += " #{kwargs[:command].inspect}" if kwargs[:command]
name
end
end
end
end
Expand Down
12 changes: 7 additions & 5 deletions lib/openhab/dsl/rules/triggers/changed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ module Triggers
#
def changed(*items, to: nil, from: nil, for: nil, attach: nil)
changed = Changed.new(rule_triggers: @rule_triggers)
Changed.flatten_items(items).map do |item|
logger.trace("Creating changed trigger for entity(#{item}), to(#{to}), from(#{from})")
# for is a reserved word in ruby, so use local_variable_get :for
duration = binding.local_variable_get(:for)

# for is a reserved word in ruby, so use local_variable_get :for
wait_duration = binding.local_variable_get(:for)
flattened_items = Changed.flatten_items(items)
@ruby_triggers << [:changed, flattened_items, { to: to, from: from, duration: duration }]
flattened_items.map do |item|
logger.trace("Creating changed trigger for entity(#{item}), to(#{to}), from(#{from})")

Changed.each_state(from, to) do |from_state, to_state|
changed.trigger(item: item, from: from_state, to: to_state, duration: wait_duration, attach: attach)
changed.trigger(item: item, from: from_state, to: to_state, duration: duration, attach: attach)
end
end.flatten
end
Expand Down
7 changes: 5 additions & 2 deletions lib/openhab/dsl/rules/triggers/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ module Triggers
#
def channel(*channels, thing: nil, triggered: nil, attach: nil)
channel_trigger = Channel.new(rule_triggers: @rule_triggers)
Channel.channels(channels: channels, thing: thing).each do |channel|
[triggered].flatten.each do |trigger|
flattened_channels = Channel.channels(channels: channels, thing: thing)
triggers = [triggered].flatten
@ruby_triggers << [:channel, flattened_channels, { triggers: triggers }]
flattened_channels.each do |channel|
triggers.each do |trigger|
channel_trigger.trigger(channel: channel, trigger: trigger, attach: attach)
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/openhab/dsl/rules/triggers/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def received_command(*items, command: nil, commands: nil, attach: nil)

# Combine command and commands, doing union so only a single nil will be in the combined array.
combined_commands = Command.combine_commands(command: command, commands: commands)
flattened_items = Command.flatten_items(items)
@ruby_triggers << [:received_command, flattened_items, { command: combined_commands }]

Command.flatten_items(items).map do |item|
flattened_items.map do |item|
combined_commands.map do |cmd|
logger.states 'Creating received command trigger', item: item, command: cmd

Expand Down
2 changes: 2 additions & 0 deletions lib/openhab/dsl/rules/triggers/cron/cron.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module Triggers
def every(value, at: nil, attach: nil)
return every(MonthDay.parse(value), at: at, attach: attach) if value.is_a? String

@ruby_triggers << [:every, value, { at: at }]

cron_expression = case value
when Symbol then Cron.from_symbol(value, at)
when Java::JavaTime::Duration then Cron.from_duration(value, at)
Expand Down
4 changes: 3 additions & 1 deletion lib/openhab/dsl/rules/triggers/updated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ module Triggers
#
def updated(*items, to: nil, attach: nil)
updated = Updated.new(rule_triggers: @rule_triggers)
Updated.flatten_items(items).map do |item|
flattened_items = Updated.flatten_items(items)
@ruby_triggers << [:updated, flattened_items, { to: to }]
flattened_items.map do |item|
logger.trace("Creating updated trigger for item(#{item}) to(#{to})")
[to].flatten.map do |to_state|
updated.trigger(item: item, to: to_state, attach: attach)
Expand Down

0 comments on commit a0b30ec

Please sign in to comment.