Skip to content

Commit

Permalink
DSL: add ensure_timed_commands and ensure_timed_commands!. Make T…
Browse files Browse the repository at this point in the history
…imedCommand return nil when a timer isn't started.

Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Jul 17, 2024
1 parent 1fd56ad commit 20ba360
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 48 deletions.
66 changes: 65 additions & 1 deletion lib/openhab/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ def ensure_states!(active: true)
end

#
# Global method that takes a block and for the duration of the block
# Global method that takes a block and for the duration of the block,
# all commands sent will check if the item is in the command's state
# before sending the command. This also applies to updates.
#
Expand Down Expand Up @@ -717,6 +717,68 @@ def ensure_states
ensure_states!(active: old)
end

#
# Permanently set the _default_ `only_when_ensured` to true for {Items::TimedCommand TimedCommand}s
# for the current thread.
#
# When `only_when_ensured` is true, the timer in a timed command will only be started if the item's
# current state is not the same as the command.
#
# The option `only_when_ensured` can still be overridden by passing the `only_when_ensured` argument to
# a timed command.
#
# @note This method is only intended for use at the top level of rule
# scripts. If it's used within library methods, or hap-hazardly within
# rules, things can get very confusing because the prior state won't be
# properly restored.
#
# @param [Boolean] default Whether to set the default for `only_when_ensured` option to true.
# @return [Boolean] The previous ensure_timed_commands setting.
#
# @example Make `only_when_ensured`: true the default for the rest of the script
# # The default is `only_when_ensured`: false, so the timer will start regardless of the item's current state
# Item.ensure.command 50, for: 5.minutes
#
# ensure_timed_commands!
#
# # From now, the default is `only_when_ensured`: true,
# # so the timer will only start if the item's current state is different
# Item.command 50, for: 5.minutes
#
# # It can still be overridden by passing `only_when_ensured: false`
# Item.command 50, for: 5.minutes, only_when_ensured: false
#
# @see Items::TimedCommand TimedCommand
#
def ensure_timed_commands!(default: true)
old = Thread.current[:openhab_ensure_timed_commands]
Thread.current[:openhab_ensure_timed_commands] = default
old
end

#
# Global method that takes a block and for the duration of the block,
# all timed commands will default to `only_when_ensured`: true
#
# @example
# ensure_timed_commands do
# # `only_when_ensured` defaults to true for all timed commands inside the block
# Item.on for: 5.minutes
#
# # It does not affect non timed-commands
# # so a call to {ensure} still needs to be done when required
# Item2.ensure.on
# end
#
# @see Items::TimedCommand TimedCommand
#
def ensure_timed_commands
old = ensure_timed_commands!
yield
ensure
ensure_timed_commands!(default: old)
end

#
# Sets a thread local variable to set the default persistence service
# for method calls inside the block
Expand Down Expand Up @@ -1088,6 +1150,8 @@ def try_parse_time_like(string)
# 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?
Expand Down
19 changes: 14 additions & 5 deletions lib/openhab/dsl/items/timed_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,17 @@ class << self
# @param [Command] command to send to object
# @param [Duration] for duration for item to be in command state
# @param [Command] on_expire Command to send when duration expires
# @param [true, false] only_when_ensured if true, only start the timed command if the command was ensured
# @param [true, false, nil] only_when_ensured
# - When `true`, only start the timed command if the command was ensured.
# - When `false`, the timed command will be started regardless of the prior state of the item, even when
# {OpenHAB::DSL.ensure_timed_commands ensure_timed_commands} is in effect.
# - When `nil`, the timed command will be started unless
# {OpenHAB::DSL.ensure_timed_commands ensure_timed_commands} is in effect.
# @yield If a block is provided, `on_expire` is ignored and the block
# is expected to set the item to the desired state or carry out some
# other action.
# @yieldparam [TimedCommandDetails] timed_command
# @return [self]
# @return [self, nil] self if the timer was started or extended, nil if the timer was not started.
#
# @example
# Switch.command(ON, for: 5.minutes)
Expand All @@ -116,14 +121,18 @@ class << self
# end
# end
#
def command(command, for: nil, on_expire: nil, only_when_ensured: false, &block)
# @see DSL.ensure_timed_commands
# @see DSL.ensure_timed_commands!
#
def command(command, for: nil, on_expire: nil, only_when_ensured: nil, &block)
duration = binding.local_variable_get(:for)
return super(command) unless duration

on_expire = block if block

create_ensured_timed_command = proc do
on_expire ||= default_on_expire(command)
only_when_ensured ||= Thread.current[:openhab_ensure_timed_commands]
if only_when_ensured
DSL.ensure_states do
create_timed_command(command, duration: duration, on_expire: on_expire) if super(command)
Expand All @@ -134,7 +143,7 @@ def command(command, for: nil, on_expire: nil, only_when_ensured: false, &block)
end
end

TimedCommand.timed_commands.compute(self) do |_key, timed_command_details|
timed_command = TimedCommand.timed_commands.compute(self) do |_key, timed_command_details|
if timed_command_details.nil?
# no prior timed command
create_ensured_timed_command.call
Expand All @@ -160,7 +169,7 @@ def command(command, for: nil, on_expire: nil, only_when_ensured: false, &block)
end
end

self
Core::Items::Proxy.new(self) if timed_command
end

private
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 @@ -13,6 +13,7 @@ module ThreadLocal
KNOWN_KEYS = %i[
openhab_context
openhab_ensure_states
openhab_ensure_timed_commands
openhab_holiday_file
openhab_persistence_service
openhab_providers
Expand Down
154 changes: 112 additions & 42 deletions spec/openhab/dsl/items/timed_command_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,64 +31,134 @@
expect(item.state).to eq 0
end

it "can activate only when ensured" do
commanded = false
received_command(item) { commanded = true }

if commanded # This won't execute because it's only for self documentation
# First check our assumptions of the behavior without `only_when_ensured`
# Possibly unnecessary because such behavior is already tested in other specs
# but nice to have here for clarity
#
# ********
# first without ensure
item.update 7
item.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be true
it "returns `self` (wrapped in a proxy)" do
expect(item.command(7, for: 1.seconds)).to be item
end

context "with `only_when_ensured` option" do
it "can activate only when ensured" do
commanded = false
time_travel_and_execute_timers(2.seconds)
expect(commanded).to be true
expect(item.state).to eq 0
received_command(item) { commanded = true }

if commanded # This won't execute because it's only for self documentation
# First check our assumptions of the behavior without `only_when_ensured`
# Possibly unnecessary because such behavior is already tested in other specs
# but nice to have here for clarity
#
# ********
# first without ensure
item.update 7
item.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be true

commanded = false
time_travel_and_execute_timers(2.seconds)
expect(commanded).to be true
expect(item.state).to eq 0

# ********
# now with ensure (but still without `only_when_ensured`)
item.update(7)
commanded = false
item.ensure.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be false

commanded = false

# the timed command still executes even though the command was ensured
time_travel_and_execute_timers(2.seconds)
expect(commanded).to be true
expect(item.state).to eq 0
end

# ********
# now with ensure (but still without `only_when_ensured`)
# now try it with `only_when_ensured`
item.update(7)
commanded = false
item.ensure.command(7, for: 1.second, on_expire: 0)
item.command(7, for: 1.second, on_expire: 0, only_when_ensured: true)
expect(commanded).to be false

time_travel_and_execute_timers(2.seconds)
expect(commanded).to be false
# The difference is here: the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7

# ********
# calling ensure explicitly should still work
item.update(7) # not necessary but for clarity
commanded = false
item.ensure.command(7, for: 1.second, on_expire: 0, only_when_ensured: true)
expect(commanded).to be false

# the timed command still executes even though the command was ensured
time_travel_and_execute_timers(2.seconds)
expect(commanded).to be true
expect(item.state).to eq 0
expect(commanded).to be false
# The difference is here: the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7
end

# ********
# now try it with `only_when_ensured`
item.update(7)
commanded = false
item.command(7, for: 1.second, on_expire: 0, only_when_ensured: true)
expect(commanded).to be false
it "returns self when the timer was started" do
item.update(0)
result = item.command(7, for: 1.second, only_when_ensured: true)
expect(result).to be item
end

time_travel_and_execute_timers(2.seconds)
expect(commanded).to be false
# The difference is here: the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7
it "returns nil when the timer was not started" do
item.update(0)
result = item.command(0, for: 1.second, only_when_ensured: true)
expect(result).to be_nil
end
end

# ********
# calling ensure explicitly should still work
item.update(7) # not necessary but for clarity
commanded = false
item.ensure.command(7, for: 1.second, on_expire: 0, only_when_ensured: true)
expect(commanded).to be false
describe "#ensure_timed_commands!" do
around do |example|
ensure_timed_commands!
example.run
ensure
ensure_timed_commands!(default: false)
end

time_travel_and_execute_timers(2.seconds)
expect(commanded).to be false
# The difference is here: the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7
it "makes `only_when_ensured` defaults to true for all timed commands" do
commanded = false
received_command(item) { commanded = true }

item.update(7)
# only_when_ensured is not specified in this call, but it should now defaults to true
item.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be false

time_travel_and_execute_timers(2.seconds)
expect(commanded).to be false
# check that the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7
end
end

describe "#ensure_timed_commands" do
it "only takes effect inside the block" do
commanded = false
received_command(item) { commanded = true }

ensure_timed_commands do
item.update(7)
# only_when_ensured is not specified in this call, but it should now defaults to true
item.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be false

time_travel_and_execute_timers(2.seconds)
expect(commanded).to be false
# check that the timer didn't even start, so the state didn't change to `on_expire` state
expect(item.state).to eq 7
end

# After the block, it shoult not default to true anymore
item.update(7)
commanded = false
item.command(7, for: 1.second, on_expire: 0)
expect(commanded).to be true

time_travel_and_execute_timers(2.seconds)
expect(item.state).to eq 0
end
end

context "with SwitchItem" do
Expand Down

0 comments on commit 20ba360

Please sign in to comment.