Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DSL: add ensure_timed_commands and ensure_timed_commands!. Make TimedCommand return nil when a timer isn't started. #304

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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