openhab-jrubyscripting 5.0.0.rc9 → 5.0.0.rc11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab/core/actions/audio.rb +47 -0
  3. data/lib/openhab/core/actions/ephemeris.rb +39 -0
  4. data/lib/openhab/core/actions/exec.rb +51 -0
  5. data/lib/openhab/core/actions/http.rb +80 -0
  6. data/lib/openhab/core/actions/ping.rb +30 -0
  7. data/lib/openhab/core/actions/transformation.rb +32 -0
  8. data/lib/openhab/core/actions/voice.rb +36 -0
  9. data/lib/openhab/core/actions.rb +23 -120
  10. data/lib/openhab/core/{events → dto}/item_channel_link.rb +1 -4
  11. data/lib/openhab/core/{events → dto}/thing.rb +10 -12
  12. data/lib/openhab/core/dto.rb +11 -0
  13. data/lib/openhab/core/entity_lookup.rb +1 -1
  14. data/lib/openhab/core/events/abstract_event.rb +1 -0
  15. data/lib/openhab/core/events/abstract_item_registry_event.rb +36 -0
  16. data/lib/openhab/core/events/abstract_thing_registry_event.rb +40 -0
  17. data/lib/openhab/core/events/item_command_event.rb +1 -1
  18. data/lib/openhab/core/events/item_state_changed_event.rb +6 -6
  19. data/lib/openhab/core/events/item_state_event.rb +6 -6
  20. data/lib/openhab/core/events/thing_status_info_event.rb +8 -6
  21. data/lib/openhab/core/items/generic_item.rb +2 -1
  22. data/lib/openhab/core/items/persistence.rb +52 -18
  23. data/lib/openhab/core/items/player_item.rb +1 -1
  24. data/lib/openhab/core/items/proxy.rb +20 -14
  25. data/lib/openhab/core/items/registry.rb +2 -0
  26. data/lib/openhab/core/items.rb +3 -3
  27. data/lib/openhab/core/profile_factory.rb +3 -1
  28. data/lib/openhab/core/proxy.rb +125 -0
  29. data/lib/openhab/core/things/links/provider.rb +1 -1
  30. data/lib/openhab/core/things/proxy.rb +8 -0
  31. data/lib/openhab/core/types/date_time_type.rb +2 -1
  32. data/lib/openhab/core/types/decimal_type.rb +1 -1
  33. data/lib/openhab/core/types/un_def_type.rb +2 -2
  34. data/lib/openhab/core/value_cache.rb +1 -1
  35. data/lib/openhab/core_ext/ephemeris.rb +53 -0
  36. data/lib/openhab/core_ext/java/class.rb +1 -1
  37. data/lib/openhab/core_ext/java/duration.rb +25 -1
  38. data/lib/openhab/core_ext/java/local_date.rb +2 -0
  39. data/lib/openhab/core_ext/java/month_day.rb +2 -0
  40. data/lib/openhab/core_ext/java/zoned_date_time.rb +85 -0
  41. data/lib/openhab/core_ext/ruby/date.rb +2 -0
  42. data/lib/openhab/core_ext/ruby/date_time.rb +1 -0
  43. data/lib/openhab/core_ext/ruby/time.rb +1 -0
  44. data/lib/openhab/dsl/debouncer.rb +259 -0
  45. data/lib/openhab/dsl/items/builder.rb +4 -2
  46. data/lib/openhab/dsl/items/timed_command.rb +31 -13
  47. data/lib/openhab/dsl/rules/automation_rule.rb +28 -21
  48. data/lib/openhab/dsl/rules/builder.rb +357 -37
  49. data/lib/openhab/dsl/rules/guard.rb +12 -54
  50. data/lib/openhab/dsl/rules/name_inference.rb +11 -0
  51. data/lib/openhab/dsl/rules/property.rb +3 -4
  52. data/lib/openhab/dsl/rules/terse.rb +4 -1
  53. data/lib/openhab/dsl/rules/triggers/conditions/duration.rb +5 -6
  54. data/lib/openhab/dsl/rules/triggers/cron/cron.rb +1 -0
  55. data/lib/openhab/dsl/rules/triggers/cron/cron_handler.rb +19 -31
  56. data/lib/openhab/dsl/rules/triggers/watch/watch.rb +1 -0
  57. data/lib/openhab/dsl/rules/triggers/watch/watch_handler.rb +22 -30
  58. data/lib/openhab/dsl/things/builder.rb +1 -1
  59. data/lib/openhab/dsl/thread_local.rb +1 -0
  60. data/lib/openhab/dsl/version.rb +1 -1
  61. data/lib/openhab/dsl.rb +224 -3
  62. data/lib/openhab/rspec/hooks.rb +5 -2
  63. data/lib/openhab/rspec/karaf.rb +7 -0
  64. data/lib/openhab/rspec/mocks/instance_method_stasher.rb +22 -0
  65. data/lib/openhab/rspec/mocks/space.rb +23 -0
  66. data/lib/openhab/rspec/openhab/core/actions.rb +16 -4
  67. data/lib/openhab/rspec/openhab/core/items/proxy.rb +1 -13
  68. data/lib/openhab/rspec/suspend_rules.rb +1 -14
  69. data/lib/openhab/yard/base_helper.rb +19 -0
  70. data/lib/openhab/yard/code_objects/group_object.rb +9 -3
  71. data/lib/openhab/yard/coderay.rb +17 -0
  72. data/lib/openhab/yard/handlers/jruby/base.rb +10 -1
  73. data/lib/openhab/yard/handlers/jruby/java_import_handler.rb +3 -0
  74. data/lib/openhab/yard/html_helper.rb +49 -15
  75. data/lib/openhab/yard/markdown_helper.rb +135 -0
  76. data/lib/openhab/yard.rb +6 -0
  77. metadata +36 -4
@@ -21,11 +21,11 @@ module OpenHAB
21
21
  # Undef State
22
22
 
23
23
  # @!method null?
24
- # Check if `self == NULL`
24
+ # Check if `self` == {NULL}
25
25
  # @return [true,false]
26
26
 
27
27
  # @!method undef?
28
- # Check if `self == UNDEF`
28
+ # Check if `self` == {UNDEF}
29
29
  # @return [true,false]
30
30
  end
31
31
  end
@@ -26,7 +26,7 @@ module OpenHAB
26
26
  #
27
27
  # @note Only the {OpenHAB::DSL.shared_cache sharedCache} is exposed in Ruby.
28
28
  # For a private cache, simply use an instance variable. See
29
- # {file:docs/ruby-basics.md#Variables Instance Variables}.
29
+ # {file:docs/ruby-basics.md#variables Instance Variables}.
30
30
  #
31
31
  # @note Because every script or UI rule gets it own JRuby engine instance,
32
32
  # you cannot rely on being able to access Ruby objects between them. Only
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenHAB
4
+ module CoreExt
5
+ #
6
+ # Forwards ephemeris helper methods to `#to_zoned_date_time` provided by
7
+ # the mixed-in class.
8
+ #
9
+ # @note openHAB's built-in holiday definitions are based on _bank_
10
+ # holidays, so may give some unexpected results. For example, 2022-12-25
11
+ # is _not_ Christmas in England because it lands on a Sunday that year,
12
+ # so Christmas is considered to be 2022-12-26. See
13
+ # [the source](https://github.com/svendiedrichsen/jollyday/tree/master/src/main/resources/holidays)
14
+ # for exact definitions. You can always provide your own holiday
15
+ # definitions with {OpenHAB::DSL.holiday_file holiday_file} or
16
+ # {OpenHAB::DSL.holiday_file! holiday_file!}.
17
+ #
18
+ # @see https://www.openhab.org/docs/configuration/actions.html#ephemeris Ephemeris Action
19
+ # @see Core::Actions::Ephemeris.holiday_name Ephemeris.holiday_name
20
+ #
21
+ module Ephemeris
22
+ # (see Java::ZonedDateTime#holiday)
23
+ def holiday(holiday_file = nil)
24
+ to_zoned_date_time.holiday(holiday_file)
25
+ end
26
+
27
+ # (see Java::ZonedDateTime#holiday?)
28
+ def holiday?(holiday_file = nil)
29
+ to_zoned_date_time.holiday?(holiday_file)
30
+ end
31
+
32
+ # (see Java::ZonedDateTime#next_holiday)
33
+ def next_holiday(holiday_file = nil)
34
+ to_zoned_date_time.next_holiday(holiday_file)
35
+ end
36
+
37
+ # (see Java::ZonedDateTime#weekend?)
38
+ def weekend?
39
+ to_zoned_date_time.weekend?
40
+ end
41
+
42
+ # (see Java::ZonedDateTime#in_dayset?)
43
+ def in_dayset?(set)
44
+ to_zoned_date_time.in_dayset?(set)
45
+ end
46
+
47
+ # (see Java::ZonedDateTime#days_until)
48
+ def days_until(holiday, holiday_file = nil)
49
+ to_zoned_date_time.days_until(holiday, holiday_file)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -21,7 +21,7 @@ module OpenHAB
21
21
  #
22
22
  # `self`, all superclasses and interfaces, recursively.
23
23
  #
24
- # @return [Array<java.reflect.Type>]
24
+ # @return [Array<java.lang.reflect.Type>]
25
25
  #
26
26
  def generic_ancestors
27
27
  ancestors.flat_map do |klass|
@@ -5,13 +5,37 @@ module OpenHAB
5
5
  module Java
6
6
  Duration = java.time.Duration
7
7
 
8
- # Extensions to Duration
8
+ # Extensions to {java.time.Duration Java Duration}
9
9
  class Duration
10
10
  include Between
11
11
  # @!parse include TemporalAmount
12
12
 
13
+ #
14
+ # Convert to integer number of seconds
15
+ #
16
+ # @return [Integer]
17
+ #
13
18
  alias_method :to_i, :seconds
14
19
 
20
+ #
21
+ # @!method zero?
22
+ # @return [true,false] Returns true if the duration is zero length.
23
+ #
24
+
25
+ #
26
+ # @!method negative?
27
+ # @return [true,false] Returns true if the duration is less than zero.
28
+ #
29
+
30
+ unless instance_methods.include?(:positive?)
31
+ #
32
+ # @return [true, false] Returns true if the duration is greater than zero.
33
+ #
34
+ def positive?
35
+ self > 0 # rubocop:disable Style/NumericPredicate
36
+ end
37
+ end
38
+
15
39
  #
16
40
  # Convert to number of seconds
17
41
  #
@@ -11,6 +11,7 @@ module OpenHAB
11
11
  class LocalDate
12
12
  include Time
13
13
  include Between
14
+ include Ephemeris
14
15
 
15
16
  class << self # rubocop:disable Lint/EmptyClass
16
17
  # @!attribute [r] now
@@ -66,6 +67,7 @@ module OpenHAB
66
67
  Date.new(year, month_value, day_of_month)
67
68
  end
68
69
 
70
+ # @return [Month]
69
71
  alias_method :to_month, :month
70
72
 
71
73
  # @return [MonthDay]
@@ -10,6 +10,7 @@ module OpenHAB
10
10
  # Extensions to MonthDay
11
11
  class MonthDay
12
12
  include Between
13
+ include Ephemeris
13
14
 
14
15
  class << self
15
16
  #
@@ -76,6 +77,7 @@ module OpenHAB
76
77
  year.at_month_day(self)
77
78
  end
78
79
 
80
+ # @return [Month]
79
81
  alias_method :to_month, :month
80
82
 
81
83
  # @param [Date, nil] context
@@ -17,7 +17,10 @@ module OpenHAB
17
17
  # @return [ZonedDateTime]
18
18
  end
19
19
 
20
+ # @return [LocalTime]
20
21
  alias_method :to_local_time, :toLocalTime
22
+
23
+ # @return [Month]
21
24
  alias_method :to_month, :month
22
25
 
23
26
  # @param [TemporalAmount, #to_zoned_date_time, Numeric] other
@@ -87,6 +90,88 @@ module OpenHAB
87
90
  self
88
91
  end
89
92
 
93
+ # @group Ephemeris Methods
94
+ # (see CoreExt::Ephemeris)
95
+
96
+ #
97
+ # Name of the holiday for this date.
98
+ #
99
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
100
+ # @return [Symbol, nil]
101
+ #
102
+ # @example
103
+ # MonthDay.parse("12-25").holiday # => :christmas
104
+ #
105
+ def holiday(holiday_file = nil)
106
+ ::Ephemeris.get_bank_holiday_name(*[self, holiday_file || DSL.holiday_file].compact)&.downcase&.to_sym
107
+ end
108
+
109
+ #
110
+ # Determines if this date is on a holiday.
111
+ #
112
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
113
+ # @return [true, false]
114
+ #
115
+ def holiday?(holiday_file = nil)
116
+ ::Ephemeris.bank_holiday?(*[self, holiday_file || DSL.holiday_file].compact)
117
+ end
118
+
119
+ #
120
+ # Name of the closest holiday on or after this date.
121
+ #
122
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
123
+ # @return [Symbol]
124
+ #
125
+ def next_holiday(holiday_file = nil)
126
+ ::Ephemeris.get_next_bank_holiday(*[self, holiday_file || DSL.holiday_file].compact).downcase.to_sym
127
+ end
128
+
129
+ #
130
+ # Determines if this time is during a weekend.
131
+ #
132
+ # @return [true, false]
133
+ #
134
+ # @example
135
+ # Time.now.weekend?
136
+ #
137
+ def weekend?
138
+ ::Ephemeris.weekend?(self)
139
+ end
140
+
141
+ #
142
+ # Determines if this time is during a specific dayset
143
+ #
144
+ # @param [String, Symbol] set
145
+ # @return [true, false]
146
+ #
147
+ # @example
148
+ # Time.now.in_dayset?("school")
149
+ #
150
+ def in_dayset?(set)
151
+ ::Ephemeris.in_dayset?(set.to_s, self)
152
+ end
153
+
154
+ #
155
+ # Calculate the number of days until a specific holiday
156
+ #
157
+ # @param [String, Symbol] holiday
158
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
159
+ # @return [Integer]
160
+ # @raise [ArgumentError] if the holiday isn't valid
161
+ #
162
+ # @example
163
+ # Time.now.days_until(:christmas) # => 2
164
+ #
165
+ def days_until(holiday, holiday_file = nil)
166
+ holiday = holiday.to_s.upcase
167
+ r = ::Ephemeris.get_days_until(*[self, holiday, holiday_file || DSL.holiday_file].compact)
168
+ raise ArgumentError, "#{holiday.inspect} isn't a recognized holiday" if r == -1
169
+
170
+ r
171
+ end
172
+
173
+ # @endgroup
174
+
90
175
  # @return [Integer, nil]
91
176
  def <=>(other)
92
177
  # compare instants, otherwise it will differ by timezone, which we don't want
@@ -5,6 +5,7 @@ require "date"
5
5
  # Extensions to Date
6
6
  class Date
7
7
  include OpenHAB::CoreExt::Between
8
+ include OpenHAB::CoreExt::Ephemeris
8
9
 
9
10
  #
10
11
  # Extends {#+} to allow adding a {java.time.temporal.TemporalAmount TemporalAmount}
@@ -90,5 +91,6 @@ class Date
90
91
  end
91
92
 
92
93
  remove_method :inspect
94
+ # @return [String]
93
95
  alias_method :inspect, :to_s
94
96
  end
@@ -11,6 +11,7 @@ require "forwardable"
11
11
  class DateTime < Date
12
12
  extend Forwardable
13
13
  include OpenHAB::CoreExt::Between
14
+ include OpenHAB::CoreExt::Ephemeris
14
15
 
15
16
  # (see Time#plus_with_temporal)
16
17
  def plus_with_temporal(other)
@@ -6,6 +6,7 @@ require "forwardable"
6
6
  class Time
7
7
  extend Forwardable
8
8
  include OpenHAB::CoreExt::Between
9
+ include OpenHAB::CoreExt::Ephemeris
9
10
 
10
11
  #
11
12
  # @!method +(other)
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenHAB
4
+ module DSL
5
+ #
6
+ # Provides the feature for debouncing calls to a given block.
7
+ #
8
+ # The debouncer can filter events and only allow the events on the leading or trailing edge
9
+ # of the given interval. Its behavior can be customized through settings passed to its
10
+ # {initialize constructor}.
11
+ #
12
+ # The following timing diagram illustrates the incoming triggers and the actual executions
13
+ # using various options.
14
+ #
15
+ # ```ruby
16
+ # 1 1 2 2 3 3 4 4
17
+ # 0 5 0 5 0 5 0 5 0 5
18
+ # Triggers : 'X.X...X...X..XX.X.X......XXXXXXXXXXX....X.....'
19
+ # leading: false
20
+ # for:5 : '|....X|....X |....X |....X|....X |....X'
21
+ # leading: true
22
+ # for:5 : 'X.....X......X....X......X....X....X....X.....'
23
+ #
24
+ # more options, leading: false
25
+ # Triggers : 'X.X...X...X..XX.X.X......XXXXXXXXXXX....X.....'
26
+ # for:5 idle:3 : '|....X|......X|......X...|............X.|....X'
27
+ # for:5 idle:5 : '|......................X.|..............X.....'
28
+ # for:5..5 idle:X : '|....X|....X.|....X......|....X|....X...|....X'
29
+ # for:5..6 idle:5 : '|.....X...|.....X.|....X.|.....X|.....X.|....X'
30
+ # for:5..7 idle:5 : '|......X..|......X|....X.|......X|......X.....'
31
+ # for:5..8 idle:5 : '|.......X.|.......X......|.......X|.....X.....'
32
+ # for:5..8 idle:3 : '|....X|......X|......X...|.......X|....X|....X'
33
+ # for:5..8 idle:2 : '|....X|.....X|......X....|.......X|....X|....X'
34
+ # ```
35
+ #
36
+ # Notes:
37
+ # - `|` indicates the start of the debounce period
38
+ # - With `for: 5..5` (a range with begin=end), the `idle_time` argument is irrelevant
39
+ # and be unset/set to any value as it will not alter the debouncer's behavior.
40
+ # - Without an `idle_time`, the range end in `for: X..Y` is irrelevant. It is equivalent to
41
+ # `for: X` without the end of the range.
42
+ #
43
+ class Debouncer
44
+ # @return [Range,nil] The range of accepted debounce period, or nil if debouncing is disabled.
45
+ attr_reader :interval
46
+
47
+ # @return [Duration, nil] The minimum idle time to stop debouncing.
48
+ attr_reader :idle_time
49
+
50
+ #
51
+ # Constructor to create a debouncer object.
52
+ #
53
+ # The constructor sets the options and behaviour of the debouncer when the {#call}
54
+ # method is called.
55
+ #
56
+ # Terminology:
57
+ # - `calls` are invocations of the {#call} method, i.e. the events that need to be throttled / debounced.
58
+ # - `executions` are the actual code executions of the given block. Executions usually occur
59
+ # less frequently than the call to the debounce method.
60
+ #
61
+ # @param [Duration,Range,nil] for The minimum and optional maximum execution interval.
62
+ # - {Duration}: The minimum interval between executions. The debouncer will not execute
63
+ # the given block more often than this.
64
+ # - {Range}: A range of {Duration}s for the minimum to maximum interval between executions.
65
+ # The range end defines the maximum duration from the initial trigger, at which
66
+ # the debouncer will execute the block, even when an `idle_time` argument was given and
67
+ # calls continue to occur at an interval less than `idle_time`.
68
+ # - `nil`: When `nil`, no debouncing is performed, all the other parameters are ignored,
69
+ # and every call will result in immediate execution of the given block.
70
+ #
71
+ # @param [true,false] leading
72
+ # - `true`: Perform leading edge "debouncing". Execute the first call then ignore
73
+ # subsequent calls that occur within the debounce period.
74
+ # - `false`: Perform trailing edge debouncing. Execute the last call at the end of
75
+ # the debounce period and ignore all the calls leading up to it.
76
+ #
77
+ # @param [Duration,nil] idle_time The minimum idle time between calls to stop debouncing.
78
+ # The debouncer will continue to hold until the interval between two calls is longer
79
+ # than the idle time or until the maximum interval between executions, when
80
+ # specified, is reached.
81
+ #
82
+ # @return [void]
83
+ #
84
+ def initialize(for:, leading: false, idle_time: nil)
85
+ @interval = binding.local_variable_get(:for)
86
+ return unless @interval
87
+
88
+ @interval = (@interval..) unless @interval.is_a?(Range)
89
+
90
+ @leading = leading
91
+ @idle_time = idle_time
92
+ @mutex = Mutex.new
93
+ @block = nil
94
+ @timer = nil
95
+ reset
96
+ end
97
+
98
+ #
99
+ # Debounces calls to the given block.
100
+ #
101
+ # This method is meant to be called repeatedly with the same given block.
102
+ # However, if no block is given, it will call and debounce the previously given block
103
+ #
104
+ # @yield Block to be debounced
105
+ #
106
+ # @return [void]
107
+ #
108
+ # @example Basic trailing edge debouncing
109
+ # debouncer = Debouncer.new(for: 1.minute)
110
+ # (1..100).each do
111
+ # debouncer.call { logger.info "I won't log more often than once a minute" }
112
+ # sleep 20 # call the debouncer every 20 seconds
113
+ # end
114
+ #
115
+ # @example Call the previous debounced block
116
+ # debouncer = Debouncer.new(for: 1.minute)
117
+ # debouncer.call { logger.info "Hello. It is #{Time.now}" } # First call to debounce
118
+ #
119
+ # after(20.seconds) do |timer|
120
+ # debouncer.call # Call the original block above
121
+ # timer.reschedule unless timer.cancelled?
122
+ # end
123
+ #
124
+ def call(&block)
125
+ @block = block if block
126
+ raise ArgumentError, "No block has been provided" unless @block
127
+
128
+ return call! unless @interval # passthrough mode, no debouncing when @interval is nil
129
+
130
+ now = ZonedDateTime.now
131
+ if leading?
132
+ leading_edge_debounce(now)
133
+ else
134
+ trailing_edge_debounce(now)
135
+ end
136
+ @mutex.synchronize { @last_timestamp = now }
137
+ end
138
+
139
+ #
140
+ # Executes the latest block passed to the {#debounce} call regardless of any debounce settings.
141
+ #
142
+ # @return [Object] The return value of the block
143
+ #
144
+ def call!
145
+ @block.call
146
+ end
147
+
148
+ #
149
+ # Resets the debounce period and cancels any outstanding block executions of a trailing edge debouncer.
150
+ #
151
+ # - A leading edge debouncer will execute its block on the next call and start a new debounce period.
152
+ # - A trailing edge debouncer will reset its debounce timer and the next call will become the start
153
+ # of a new debounce period.
154
+ #
155
+ # @return [Boolean] True if a pending execution was cancelled.
156
+ #
157
+ def reset
158
+ @mutex.synchronize do
159
+ @last_timestamp = @leading_timestamp = @interval.begin.ago - 1.second if leading?
160
+ @timer&.cancel
161
+ end
162
+ end
163
+
164
+ #
165
+ # Immediately executes any outstanding event of a trailing edge debounce.
166
+ # The next call will start a new period.
167
+ #
168
+ # It has no effect on a leading edge debouncer - use {#reset} instead.
169
+ #
170
+ # @return [Boolean] True if an existing debounce timer was rescheduled to run immediately.
171
+ # False if there were no outstanding executions.
172
+ #
173
+ def flush
174
+ @mutex.synchronize do
175
+ if @timer&.cancel
176
+ call!
177
+ true
178
+ end
179
+ end
180
+ end
181
+
182
+ #
183
+ # Returns true to indicate that this is a leading edge debouncer.
184
+ #
185
+ # @return [true,false] True if this object was created to be a leading edge debouncer. False otherwise.
186
+ #
187
+ def leading?
188
+ @leading
189
+ end
190
+
191
+ private
192
+
193
+ def too_soon?(now)
194
+ now < @leading_timestamp + @interval.begin
195
+ end
196
+
197
+ # @return [true,false] When max interval is not set/required, always returns false,
198
+ # because there is no maximum interval requirement.
199
+ # When it is set, return true if the max interval condition is met, or false otherwise
200
+ def max_interval?(now)
201
+ @interval.end && now >= @leading_timestamp + @interval.end
202
+ end
203
+
204
+ # @return [true,false] When idle_time is not set/required, always returns true,
205
+ # as if the idle time condition is met.
206
+ # When it is set, return true if the idle time condition is met, or false otherwise
207
+ def idle?(now)
208
+ @idle_time.nil? || now >= @last_timestamp + @idle_time
209
+ end
210
+
211
+ def leading_edge_debounce(now)
212
+ @mutex.synchronize do
213
+ next if too_soon?(now)
214
+ next unless idle?(now) || max_interval?(now)
215
+
216
+ @leading_timestamp = now
217
+ call!
218
+ end
219
+ end
220
+
221
+ def start_timer(now)
222
+ @leading_timestamp = now
223
+ @timer = DSL.after(@interval.begin) { @mutex.synchronize { call! } }
224
+ end
225
+
226
+ def handle_leading_event(now)
227
+ @leading_timestamp = now
228
+ @initial_wait ||= [@interval.begin, @idle_time].compact.max
229
+ @timer.reschedule(@initial_wait)
230
+ end
231
+
232
+ def handle_intermediate_event(now)
233
+ execution_time = @leading_timestamp + @interval.begin
234
+
235
+ execution_time = [execution_time, now + @idle_time].max if @idle_time && (@last_timestamp + @idle_time != now)
236
+ if @interval.end
237
+ max_execution_time = @leading_timestamp + @interval.end
238
+ execution_time = max_execution_time if max_execution_time < execution_time
239
+ end
240
+
241
+ if execution_time <= now
242
+ @timer.cancel
243
+ call!
244
+ elsif execution_time > @timer.execution_time
245
+ @timer.reschedule(execution_time)
246
+ end
247
+ end
248
+
249
+ def trailing_edge_debounce(now)
250
+ @mutex.synchronize do
251
+ next start_timer(now) unless @timer
252
+ next handle_intermediate_event(now) if @timer.active?
253
+
254
+ handle_leading_event(now)
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -97,8 +97,10 @@ module OpenHAB
97
97
  def item(*args, **kwargs, &block)
98
98
  item = ItemBuilder.new(*args, provider: provider, **kwargs)
99
99
  item.instance_eval(&block) if block
100
- provider.add(item)
101
- Core::Items::Proxy.new(item)
100
+ r = provider.add(item)
101
+ return Core::Items::Proxy.new(r) if r.is_a?(Item)
102
+
103
+ item
102
104
  end
103
105
  end
104
106
 
@@ -17,10 +17,17 @@ module OpenHAB
17
17
  # command. This is available on both the 'command' method and any
18
18
  # command-specific methods, e.g. {SwitchItem#on}.
19
19
  #
20
- # Any update to the timed command state will result in the timer being
21
- # cancelled. For example, if you have a Switch on a timer and another
22
- # rule sends OFF or ON to that item the timer will be automatically
23
- # canceled. Sending a different duration (for:) value for the timed
20
+ # The timer will be cancelled, and the item's state will not be changed
21
+ # to the on_expire state if:
22
+ # - The item receives any command within the timed command duration.
23
+ # - The item is updated to a different state, even if it is then updated
24
+ # back to the same state.
25
+ #
26
+ # For example, if you have a Switch on a timer and another rule sends
27
+ # a command to that item, even when it's commanded to the same state,
28
+ # the timer will be automatically canceled.
29
+ #
30
+ # Sending a different duration (for:) value for the timed
24
31
  # command will reschedule the timed command for that new duration.
25
32
  #
26
33
  module TimedCommand
@@ -107,7 +114,7 @@ module OpenHAB
107
114
  # no prior timed command
108
115
  on_expire ||= default_on_expire(command)
109
116
  super(command)
110
- create_timed_command(duration: duration, on_expire: on_expire)
117
+ create_timed_command(command, duration: duration, on_expire: on_expire)
111
118
  else
112
119
  timed_command_details.mutex.synchronize do
113
120
  if timed_command_details.resolution
@@ -116,7 +123,7 @@ module OpenHAB
116
123
  # just create a new one
117
124
  on_expire ||= default_on_expire(command)
118
125
  super(command)
119
- create_timed_command(duration: duration, on_expire: on_expire)
126
+ create_timed_command(command, duration: duration, on_expire: on_expire)
120
127
  else
121
128
  # timed command still pending; reset it
122
129
  logger.trace "Outstanding Timed Command #{timed_command_details} encountered - rescheduling"
@@ -138,13 +145,13 @@ module OpenHAB
138
145
  private
139
146
 
140
147
  # Creates a new timed command and places it in the TimedCommand hash
141
- def create_timed_command(duration:, on_expire:)
148
+ def create_timed_command(command, duration:, on_expire:)
142
149
  timed_command_details = TimedCommandDetails.new(item: self,
143
150
  on_expire: on_expire,
144
151
  mutex: Mutex.new)
145
152
 
146
153
  timed_command_details.timer = timed_command_timer(timed_command_details, duration)
147
- cancel_rule = TimedCommandCancelRule.new(timed_command_details)
154
+ cancel_rule = TimedCommandCancelRule.new(command, timed_command_details)
148
155
  unmanaged_rule = Core.automation_manager.add_unmanaged_rule(cancel_rule)
149
156
  timed_command_details.rule_uid = unmanaged_rule.uid
150
157
  Core::Rules::Provider.current.add(unmanaged_rule)
@@ -189,16 +196,27 @@ module OpenHAB
189
196
  #
190
197
  # @!visibility private
191
198
  class TimedCommandCancelRule < org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule
192
- def initialize(timed_command_details)
199
+ def initialize(command, timed_command_details)
193
200
  super()
194
201
  @timed_command_details = timed_command_details
195
202
  # Capture rule name if known
196
203
  @thread_locals = ThreadLocal.persist
197
204
  self.name = "Cancel implicit timer for #{timed_command_details.item.name}"
198
- self.triggers = [Rules::RuleTriggers.trigger(
199
- type: Rules::Triggers::Changed::ITEM_STATE_CHANGE,
200
- config: { "itemName" => timed_command_details.item.name }
201
- )]
205
+ self.triggers = [
206
+ Rules::RuleTriggers.trigger(
207
+ type: Rules::Triggers::Changed::ITEM_STATE_CHANGE,
208
+ config: {
209
+ "itemName" => timed_command_details.item.name,
210
+ "previousState" => timed_command_details.item.format_command(command).to_s
211
+ }
212
+ ),
213
+ Rules::RuleTriggers.trigger(
214
+ type: Rules::Triggers::Command::ITEM_COMMAND,
215
+ config: {
216
+ "itemName" => timed_command_details.item.name
217
+ }
218
+ )
219
+ ]
202
220
  self.visibility = Core::Rules::Visibility::HIDDEN
203
221
  end
204
222