Skip to content

Commit

Permalink
Support offset in at and every :day, at: triggers
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Sep 8, 2024
1 parent d9c465f commit 89785f5
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 44 deletions.
7 changes: 7 additions & 0 deletions lib/openhab/core/items/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ def members
__getobj__.members
end

# @return [String]
def to_s
return name if __getobj__.nil?

__getobj__.to_s
end

# @return [String]
def inspect
return super unless __getobj__.nil?
Expand Down
49 changes: 38 additions & 11 deletions lib/openhab/dsl/rules/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,10 @@ def cron(expression = nil, attach: nil, **fields)
# If `value` is `:day`, `at` can be a {Core::Items::DateTimeItem DateTimeItem}, and
# the trigger will run every day at the (time only portion of) current state of the
# item. If the item is {NULL} or {UNDEF}, the trigger will not run.
# @param [Duration, Integer, nil] offset Offset the execution time from the time specified in the item.
# This can be a negative value to execute the rule before the time specified in the item.
# A Duration will be converted to its total length in seconds, with any excess precision information dropped.
# @since openHAB 4.3
# @param [Object] attach Object to be attached to the trigger
# @return [void]
#
Expand All @@ -1322,12 +1326,6 @@ def cron(expression = nil, attach: nil, **fields)
# run { Light.on }
# end
#
# @example Trigger at the time portion of a DateTime Item
# rule "Every day at sunset" do
# every :day, at: Sunset_Time
# run { logger.info "It's getting dark" }
# end
#
# @example Specific day of the week
# rule "Weekly" do
# every :monday, at: '5:15'
Expand Down Expand Up @@ -1372,8 +1370,21 @@ def cron(expression = nil, attach: nil, **fields)
# run { logger.info "It's the weekend!" }
# end
#
def every(*values, at: nil, attach: nil)
# @example Trigger at the time portion of a DateTime Item
# rule "Every day at sunset" do
# every :day, at: Sunset_Time
# run { logger.info "It's sunset!" }
# end
#
# @example Using DateTime trigger with offset
# rule "Every day before sunset" do
# every :day, at: Sunset_Time, offset: -20.minutes
# run { logger.info "It's almost sunset!" }
# end
#
def every(*values, at: nil, offset: nil, attach: nil)
raise ArgumentError, "Missing values" if values.empty?
raise ArgumentError, "Offset can only be used when 'at' is given a DateTimeItem" if offset && !at.is_a?(Item)

if Cron.all_dow_symbols?(values)
@ruby_triggers << [:every, values.join(", "), { at: at }]
Expand All @@ -1389,15 +1400,18 @@ def every(*values, at: nil, attach: nil)
value = values.first
value = java.time.MonthDay.parse(value.to_str) if value.respond_to?(:to_str)

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

if value == :day && at.is_a?(Item)
# @!deprecated OH 3.4 - attachments are supported in OH 4.0+
if Core.version <= Core::V4_0 && !attach.nil?
raise ArgumentError, "Attachments are not supported with dynamic datetime triggers in openHAB 3.x"
end

return trigger("timer.DateTimeTrigger", itemName: at.name, timeOnly: true, attach: attach)
offset ||= 0
offset = offset.to_i # Duration#to_i converts it to seconds, but we also want to convert float/string to int
@ruby_triggers.last[2][:offset] = offset
return trigger("timer.DateTimeTrigger", itemName: at.name, timeOnly: true, offset: offset, attach: attach)
end

cron_expression = case value
Expand Down Expand Up @@ -1797,6 +1811,10 @@ def event(topic, source: nil, types: nil, attach: nil)
# The `event` passed to run blocks will be a {Core::Events::TimerEvent}.
#
# @param [Item, String, Symbol] item The item (or its name)
# @param [Duration, Integer, nil] offset Offset the execution time from the time specified in the item.
# This can be a negative value to execute the rule before the time specified in the item.
# A Duration will be converted to its total length in seconds, with any excess precision information dropped.
# @since openHAB 4.3
# @return [void]
#
# @see every
Expand All @@ -1817,9 +1835,18 @@ def event(topic, source: nil, types: nil, attach: nil)
# end
# end
#
def at(item)
# @example Using an offset
# rule "Turn on lights 15 minutes before sunset" do
# at Sunset_Time, offset: -15.minutes
# run { Lights.on }
# end
#
def at(item, offset: nil)
item = item.name if item.is_a?(Item)
trigger("timer.DateTimeTrigger", itemName: item.to_s)
offset ||= 0
offset = offset.to_i if offset.is_a?(Duration)
@ruby_triggers << [:at, item, { offset: offset }]
trigger("timer.DateTimeTrigger", itemName: item.to_s, offset: offset)
end

#
Expand Down
47 changes: 18 additions & 29 deletions lib/openhab/dsl/rules/name_inference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,6 @@ module Rules
# Contains helper methods for inferring a rule name from its triggers
# @!visibility private
module NameInference
# Trigger Type UIDs that we know how to generate a name for
KNOWN_TRIGGER_TYPES = [
"core.ChannelEventTrigger",
"core.GenericEventTrigger",
"core.GroupCommandTrigger",
"core.GroupStateChangeTrigger",
"core.GroupStateUpdateTrigger",
"core.ItemCommandTrigger",
"core.ItemStateChangeTrigger",
"core.ItemStateUpdateTrigger",
"core.SystemStartlevelTrigger",
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)
Expand All @@ -33,24 +18,14 @@ def infer_rule_id_from_block(block)

# formulate a readable rule name such as "TestSwitch received command ON" if possible
def infer_rule_name(config)
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)
infer_rule_name_from_trigger(*config.ruby_triggers.first) if config.ruby_triggers.length == 1
end

# formulate a readable rule name from a single trigger if possible
def infer_rule_name_from_trigger(trigger, items = nil, kwargs = {})
case trigger
when :at
infer_rule_name_from_at_trigger(items, **kwargs)
when :every
infer_rule_name_from_every_trigger(items, **kwargs)
when :channel
Expand Down Expand Up @@ -101,10 +76,18 @@ def infer_rule_name_from_item_trigger(trigger, items, kwargs)
name.freeze
end

# formulate a readable rule name from an at-style cron trigger
def infer_rule_name_from_at_trigger(item, offset:)
name = "At #{item}"
name += format_offset(offset)
name
end

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

Expand Down Expand Up @@ -175,6 +158,12 @@ def format_array(array)

"#{array[0..-2].join(", ")}, or #{array[-1]}"
end

def format_offset(offset)
return "" unless offset&.nonzero?

" #{offset.positive? ? "+" : ""}#{offset.seconds.to_s.downcase[2..]}" # Remove "PT" from the ISO8601 string
end
end
end
end
Expand Down
49 changes: 45 additions & 4 deletions spec/openhab/dsl/rules/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,26 @@ def self.test_cron_fields(expected, description: nil, caller: Kernel.caller, **k
expect(triggered).to be true
end
end

# @deprecated OH 4.1 remove if guard when dropping OH 4.1 support
if OpenHAB::Core.version >= OpenHAB::Core::V4_2
{ "Duration" => 2.hours + 1.seconds, "Integer" => 2.hours.to_i + 1 }.each do |type, offset|
it "supports #{type} offset" do
items.build { date_time_item MyDateTimeItem }

triggered = false
rule do
at MyDateTimeItem, offset: offset
run { triggered = true }
end

MyDateTimeItem.update(2.hours.ago) # without an offset, this would not have triggered the rule
wait(4.seconds) do
expect(triggered).to be true
end
end
end
end
end

describe "#every" do
Expand Down Expand Up @@ -1356,6 +1376,10 @@ def self.generate(name, cron_expression, *every_args, attach: nil, **kwargs)
end.to raise_error(ArgumentError)
end

it "complains when using an offset for non-dynamic `at`" do
expect { every(:day, at: "1am", offset: 2.hours) { nil } }.to raise_error(ArgumentError, /offset/i)
end

context "with dynamic `at`" do
let(:item) { items.build { date_time_item MyDateTimeItem } }

Expand Down Expand Up @@ -1386,12 +1410,29 @@ def self.generate(name, cron_expression, *every_args, attach: nil, **kwargs)
expect(triggered).to be 1
end
end
end

it "complains about dynamic at that's not daily" do
items.build { date_time_item MyDateTimeItem }
it "complains about dynamic at that's not daily" do
items.build { date_time_item MyDateTimeItem }

expect { every :month, at: MyDateTimeItem }.to raise_error(ArgumentError)
end

# @deprecated OH 4.1 remove if guard when dropping OH 4.1 support
if OpenHAB::Core.version >= OpenHAB::Core::V4_2
{ "Duration" => 2.hours + 1.second, "Integer" => 2.hours.to_i + 1 }.each do |type, offset|
it "supports #{type} offset" do
items.build { date_time_item MyDateTimeItem }

expect { every :month, at: MyDateTimeItem }.to raise_error(ArgumentError)
triggered = false
every(:day, at: MyDateTimeItem, offset: offset) { triggered = true }

MyDateTimeItem.update(2.hours.ago) # without an offset, this would not have triggered the rule
wait(4.seconds) do
expect(triggered).to be true
end
end
end
end
end
end

Expand Down
64 changes: 64 additions & 0 deletions spec/openhab/dsl/rules/name_inference_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,68 @@
expect(r.name).to eql "System Start Level reached 30 (states) or 40 (rules)"
end
end

context "with #every" do
it "generates a useful name" do
%i[second
minute
hour
day
week
month
year
monday
tuesday
wednesday
thursday
friday
saturday
sunday].each do |frequency|
r = rules.build { every(frequency) { nil } }
expect(r.name).to eql "Every #{frequency}"
end
end

it "generates a useful name with a duration" do
r = rules.build { every(5.minutes) { nil } }
expect(r.name).to eql "Every PT5M"
end

it "generates a useful name with an at:" do
r = rules.build { every(:week, at: "12:00") { nil } }
expect(r.name).to eql "Every week at 12:00"
end

it "generates a useful name with an offset" do
r = rules.build { every(:day, at: DateTimeItem1, offset: 5.minutes) { nil } }
expect(r.name).to eql "Every day at DateTimeItem1 +5m"

r = rules.build { every(:day, at: DateTimeItem1, offset: -5.minutes) { nil } }
expect(r.name).to eql "Every day at DateTimeItem1 -5m"
end
end

context "with #at" do
it "generates a useful name" do
r = rule do
at DateTimeItem1
run { nil }
end
expect(r.name).to eql "At DateTimeItem1"
end

it "generates a useful name with an offset" do
r = rule do
at DateTimeItem1, offset: 5.minutes
run { nil }
end
expect(r.name).to eql "At DateTimeItem1 +5m"

r = rule do
at DateTimeItem1, offset: -5.minutes
run { nil }
end
expect(r.name).to eql "At DateTimeItem1 -5m"
end
end
end

0 comments on commit 89785f5

Please sign in to comment.