openhab-jrubyscripting 5.0.0.rc9 → 5.0.0.rc11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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