openhab-jrubyscripting 5.0.0.rc10 → 5.0.0.rc12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) 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/date_time_item.rb +3 -2
  22. data/lib/openhab/core/items/generic_item.rb +92 -1
  23. data/lib/openhab/core/items/item.rb +9 -8
  24. data/lib/openhab/core/items/metadata/hash.rb +1 -1
  25. data/lib/openhab/core/items/metadata/namespace_hash.rb +10 -2
  26. data/lib/openhab/core/items/metadata/provider.rb +2 -2
  27. data/lib/openhab/core/items/persistence.rb +99 -21
  28. data/lib/openhab/core/items/player_item.rb +1 -1
  29. data/lib/openhab/core/items/proxy.rb +20 -14
  30. data/lib/openhab/core/items/registry.rb +12 -1
  31. data/lib/openhab/core/items/state_storage.rb +2 -2
  32. data/lib/openhab/core/items.rb +3 -3
  33. data/lib/openhab/core/profile_factory.rb +3 -1
  34. data/lib/openhab/core/proxy.rb +130 -0
  35. data/lib/openhab/core/registry.rb +12 -2
  36. data/lib/openhab/core/rules.rb +1 -1
  37. data/lib/openhab/core/things/links/provider.rb +39 -1
  38. data/lib/openhab/core/things/proxy.rb +8 -0
  39. data/lib/openhab/core/things/registry.rb +4 -0
  40. data/lib/openhab/core/timer.rb +3 -19
  41. data/lib/openhab/core/types/date_time_type.rb +3 -2
  42. data/lib/openhab/core/types/decimal_type.rb +1 -1
  43. data/lib/openhab/core/types/un_def_type.rb +2 -2
  44. data/lib/openhab/core/value_cache.rb +1 -1
  45. data/lib/openhab/core.rb +3 -3
  46. data/lib/openhab/core_ext/ephemeris.rb +53 -0
  47. data/lib/openhab/core_ext/java/class.rb +1 -1
  48. data/lib/openhab/core_ext/java/duration.rb +27 -1
  49. data/lib/openhab/core_ext/java/local_date.rb +17 -7
  50. data/lib/openhab/core_ext/java/local_time.rb +13 -3
  51. data/lib/openhab/core_ext/java/month.rb +1 -1
  52. data/lib/openhab/core_ext/java/month_day.rb +15 -3
  53. data/lib/openhab/core_ext/java/period.rb +1 -1
  54. data/lib/openhab/core_ext/java/temporal_amount.rb +1 -1
  55. data/lib/openhab/core_ext/java/time.rb +5 -1
  56. data/lib/openhab/core_ext/java/zoned_date_time.rb +100 -2
  57. data/lib/openhab/core_ext/ruby/date.rb +4 -2
  58. data/lib/openhab/core_ext/ruby/date_time.rb +1 -0
  59. data/lib/openhab/core_ext/ruby/numeric.rb +6 -1
  60. data/lib/openhab/core_ext/ruby/time.rb +1 -0
  61. data/lib/openhab/dsl/debouncer.rb +259 -0
  62. data/lib/openhab/dsl/items/builder.rb +29 -14
  63. data/lib/openhab/dsl/items/timed_command.rb +31 -13
  64. data/lib/openhab/dsl/rules/automation_rule.rb +30 -44
  65. data/lib/openhab/dsl/rules/builder.rb +404 -39
  66. data/lib/openhab/dsl/rules/guard.rb +12 -54
  67. data/lib/openhab/dsl/rules/name_inference.rb +11 -0
  68. data/lib/openhab/dsl/rules/property.rb +3 -4
  69. data/lib/openhab/dsl/rules/terse.rb +4 -1
  70. data/lib/openhab/dsl/rules/triggers/conditions/duration.rb +5 -6
  71. data/lib/openhab/dsl/rules/triggers/cron/cron.rb +1 -0
  72. data/lib/openhab/dsl/rules/triggers/cron/cron_handler.rb +19 -31
  73. data/lib/openhab/dsl/rules/triggers/watch/watch.rb +1 -0
  74. data/lib/openhab/dsl/rules/triggers/watch/watch_handler.rb +22 -30
  75. data/lib/openhab/dsl/things/builder.rb +1 -1
  76. data/lib/openhab/dsl/thread_local.rb +1 -0
  77. data/lib/openhab/dsl/version.rb +1 -1
  78. data/lib/openhab/dsl.rb +251 -14
  79. data/lib/openhab/rspec/helpers.rb +3 -2
  80. data/lib/openhab/rspec/hooks.rb +6 -2
  81. data/lib/openhab/rspec/karaf.rb +7 -0
  82. data/lib/openhab/rspec/mocks/instance_method_stasher.rb +22 -0
  83. data/lib/openhab/rspec/mocks/space.rb +23 -0
  84. data/lib/openhab/rspec/mocks/timer.rb +33 -0
  85. data/lib/openhab/rspec/openhab/core/actions.rb +16 -4
  86. data/lib/openhab/rspec/openhab/core/items/proxy.rb +1 -13
  87. data/lib/openhab/rspec/suspend_rules.rb +1 -14
  88. data/lib/openhab/rspec.rb +9 -0
  89. data/lib/openhab/yard/base_helper.rb +19 -0
  90. data/lib/openhab/yard/code_objects/group_object.rb +9 -3
  91. data/lib/openhab/yard/coderay.rb +17 -0
  92. data/lib/openhab/yard/handlers/jruby/base.rb +10 -1
  93. data/lib/openhab/yard/handlers/jruby/java_import_handler.rb +3 -0
  94. data/lib/openhab/yard/html_helper.rb +49 -15
  95. data/lib/openhab/yard/markdown_helper.rb +135 -0
  96. data/lib/openhab/yard.rb +6 -0
  97. metadata +36 -4
@@ -7,15 +7,24 @@ module OpenHAB
7
7
  module Java
8
8
  java_import java.time.LocalDate
9
9
 
10
- # Extensions to LocalDate
10
+ # Extensions to {java.time.LocalDate}
11
11
  class LocalDate
12
12
  include Time
13
13
  include Between
14
+ include Ephemeris
14
15
 
15
- class << self # rubocop:disable Lint/EmptyClass
16
- # @!attribute [r] now
17
- # @return [ZonedDateTime]
18
- end
16
+ # @!scope class
17
+
18
+ # @!attribute [r] now
19
+ # @return [LocalDate]
20
+
21
+ # @!method parse(text, formatter=nil)
22
+ # Converts the given text into a LocalDate.
23
+ # @param [String] text The text to parse
24
+ # @param [java.time.format.DateTimeFormatter] formatter The formatter to use
25
+ # @return [LocalDate]
26
+
27
+ # @!scope instance
19
28
 
20
29
  # @param [TemporalAmount, LocalDate, Numeric] other
21
30
  # If other is a Numeric, it's interpreted as days.
@@ -24,11 +33,11 @@ module OpenHAB
24
33
  def -(other)
25
34
  case other
26
35
  when Date
27
- Period.of_days(day_of_year - other.yday)
36
+ self - other.to_local_date
28
37
  when MonthDay
29
38
  self - other.at_year(year)
30
39
  when LocalDate
31
- Period.of_days(day_of_year - other.day_of_year)
40
+ Period.between(other, self)
32
41
  when Duration
33
42
  minus_days(other.to_days)
34
43
  when Numeric
@@ -66,6 +75,7 @@ module OpenHAB
66
75
  Date.new(year, month_value, day_of_month)
67
76
  end
68
77
 
78
+ # @return [Month]
69
79
  alias_method :to_month, :month
70
80
 
71
81
  # @return [MonthDay]
@@ -7,6 +7,8 @@ module OpenHAB
7
7
  module Java
8
8
  java_import java.time.LocalTime
9
9
 
10
+ #
11
+ # Extensions to {java.time.LocalTime}
10
12
  #
11
13
  # @example
12
14
  # break_time = LocalTime::NOON
@@ -36,15 +38,23 @@ module OpenHAB
36
38
  include Between
37
39
  # @!parse include Time
38
40
 
39
- # @!visibility private
40
41
  class << self
42
+ # @!attribute [r] now
43
+ # @return [LocalTime]
44
+
45
+ # @!visibility private
46
+ alias_method :raw_parse, :parse
47
+
41
48
  #
42
- # Parses strings in the form "h[:mm[:ss]] [am/pm]"
49
+ # Parses strings in the form "h[:mm[:ss]] [am/pm]" when no formatter is given.
43
50
  #
44
51
  # @param [String] string
52
+ # @param [java.time.format.DateTimeFormatter] formatter The formatter to use
45
53
  # @return [LocalTime]
46
54
  #
47
- def parse(string)
55
+ def parse(string, formatter = nil)
56
+ return raw_parse(string, formatter) if formatter
57
+
48
58
  format = /(am|pm)$/i.match?(string) ? "h[:mm[:ss][.S]][ ]a" : "H[:mm[:ss][.S]]"
49
59
  java_send(:parse, [java.lang.CharSequence, java.time.format.DateTimeFormatter],
50
60
  string, java.time.format.DateTimeFormatterBuilder.new
@@ -7,7 +7,7 @@ module OpenHAB
7
7
  module Java
8
8
  Month = java.time.Month
9
9
 
10
- # Extensions to Month
10
+ # Extensions to {java.time.Month}
11
11
  class Month
12
12
  include Between
13
13
  # @!parse include Time
@@ -7,9 +7,10 @@ module OpenHAB
7
7
  module Java
8
8
  java_import java.time.MonthDay
9
9
 
10
- # Extensions to MonthDay
10
+ # Extensions to {java.time.MonthDay}
11
11
  class MonthDay
12
12
  include Between
13
+ include Ephemeris
13
14
 
14
15
  class << self
15
16
  #
@@ -38,12 +39,22 @@ module OpenHAB
38
39
 
39
40
  # @return [MonthDay]
40
41
  def +(other)
41
- (LocalDate.of(1900, month, day_of_month) + other).to_month_day
42
+ case other
43
+ when java.time.temporal.TemporalAmount, Numeric
44
+ (LocalDate.of(1900, month, day_of_month) + other).to_month_day
45
+ else
46
+ (to_local_date(other.to_local_date) + other).to_month_day
47
+ end
42
48
  end
43
49
 
44
50
  # @return [MonthDay, Period]
45
51
  def -(other)
46
- d = (LocalDate.of(1900, month, day_of_month) - other)
52
+ d = case other
53
+ when java.time.temporal.TemporalAmount, Numeric
54
+ LocalDate.of(1900, month, day_of_month) - other
55
+ else
56
+ to_local_date(other.to_local_date) - other
57
+ end
47
58
  return d if d.is_a?(java.time.Period)
48
59
 
49
60
  d.to_month_day
@@ -76,6 +87,7 @@ module OpenHAB
76
87
  year.at_month_day(self)
77
88
  end
78
89
 
90
+ # @return [Month]
79
91
  alias_method :to_month, :month
80
92
 
81
93
  # @param [Date, nil] context
@@ -5,7 +5,7 @@ module OpenHAB
5
5
  module Java
6
6
  java_import java.time.Period
7
7
 
8
- # Extensions to Period
8
+ # Extensions to {java.time.Period}
9
9
  class Period
10
10
  # @!parse include TemporalAmount
11
11
 
@@ -5,7 +5,7 @@ module OpenHAB
5
5
  module Java
6
6
  java_import java.time.temporal.TemporalAmount
7
7
 
8
- # Extensions to TemporalAmount
8
+ # Extensions to {java.time.temporal.TemporalAmount}
9
9
  module TemporalAmount
10
10
  # Subtract `self` to {ZonedDateTime.now}
11
11
  # @return [ZonedDateTime]
@@ -50,7 +50,11 @@ module OpenHAB
50
50
  # Convert `other` to this class, if possible
51
51
  # @return [Array, nil]
52
52
  def coerce(other)
53
- [other.send(self.class.coercion_method), self] if other.respond_to?(self.class.coercion_method)
53
+ coercion_method = self.class.coercion_method
54
+ return unless other.respond_to?(coercion_method)
55
+ return [other.send(coercion_method), self] if other.method(coercion_method).arity.zero?
56
+
57
+ [other.send(coercion_method, self), self]
54
58
  end
55
59
  end
56
60
  end
@@ -7,17 +7,33 @@ module OpenHAB
7
7
  module Java
8
8
  ZonedDateTime = java.time.ZonedDateTime
9
9
 
10
- # Extensions to ZonedDateTime
10
+ # Extensions to {java.time.ZonedDateTime}
11
11
  class ZonedDateTime
12
12
  include Time
13
13
  include Between
14
14
 
15
15
  class << self # rubocop:disable Lint/EmptyClass
16
+ # @!scope class
17
+
16
18
  # @!attribute [r] now
17
19
  # @return [ZonedDateTime]
20
+
21
+ # @!method parse(text, formatter = nil)
22
+ # Parses a string into a ZonedDateTime object.
23
+ #
24
+ # @param [String] text The text to parse.
25
+ # @param [java.time.format.DateTimeFormatter] formatter The formatter to use.
26
+ # @return [ZonedDateTime]
27
+ end
28
+
29
+ # @!scope instance
30
+
31
+ # @return [LocalTime]
32
+ def to_local_time(_context = nil)
33
+ toLocalTime
18
34
  end
19
35
 
20
- alias_method :to_local_time, :toLocalTime
36
+ # @return [Month]
21
37
  alias_method :to_month, :month
22
38
 
23
39
  # @param [TemporalAmount, #to_zoned_date_time, Numeric] other
@@ -87,6 +103,88 @@ module OpenHAB
87
103
  self
88
104
  end
89
105
 
106
+ # @group Ephemeris Methods
107
+ # (see CoreExt::Ephemeris)
108
+
109
+ #
110
+ # Name of the holiday for this date.
111
+ #
112
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
113
+ # @return [Symbol, nil]
114
+ #
115
+ # @example
116
+ # MonthDay.parse("12-25").holiday # => :christmas
117
+ #
118
+ def holiday(holiday_file = nil)
119
+ ::Ephemeris.get_bank_holiday_name(*[self, holiday_file || DSL.holiday_file].compact)&.downcase&.to_sym
120
+ end
121
+
122
+ #
123
+ # Determines if this date is on a holiday.
124
+ #
125
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
126
+ # @return [true, false]
127
+ #
128
+ def holiday?(holiday_file = nil)
129
+ ::Ephemeris.bank_holiday?(*[self, holiday_file || DSL.holiday_file].compact)
130
+ end
131
+
132
+ #
133
+ # Name of the closest holiday on or after this date.
134
+ #
135
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
136
+ # @return [Symbol]
137
+ #
138
+ def next_holiday(holiday_file = nil)
139
+ ::Ephemeris.get_next_bank_holiday(*[self, holiday_file || DSL.holiday_file].compact).downcase.to_sym
140
+ end
141
+
142
+ #
143
+ # Determines if this time is during a weekend.
144
+ #
145
+ # @return [true, false]
146
+ #
147
+ # @example
148
+ # Time.now.weekend?
149
+ #
150
+ def weekend?
151
+ ::Ephemeris.weekend?(self)
152
+ end
153
+
154
+ #
155
+ # Determines if this time is during a specific dayset
156
+ #
157
+ # @param [String, Symbol] set
158
+ # @return [true, false]
159
+ #
160
+ # @example
161
+ # Time.now.in_dayset?("school")
162
+ #
163
+ def in_dayset?(set)
164
+ ::Ephemeris.in_dayset?(set.to_s, self)
165
+ end
166
+
167
+ #
168
+ # Calculate the number of days until a specific holiday
169
+ #
170
+ # @param [String, Symbol] holiday
171
+ # @param [String, nil] holiday_file Optional path to XML file to use for holiday definitions.
172
+ # @return [Integer]
173
+ # @raise [ArgumentError] if the holiday isn't valid
174
+ #
175
+ # @example
176
+ # Time.now.days_until(:christmas) # => 2
177
+ #
178
+ def days_until(holiday, holiday_file = nil)
179
+ holiday = holiday.to_s.upcase
180
+ r = ::Ephemeris.get_days_until(*[self, holiday, holiday_file || DSL.holiday_file].compact)
181
+ raise ArgumentError, "#{holiday.inspect} isn't a recognized holiday" if r == -1
182
+
183
+ r
184
+ end
185
+
186
+ # @endgroup
187
+
90
188
  # @return [Integer, nil]
91
189
  def <=>(other)
92
190
  # 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}
@@ -84,11 +85,12 @@ class Date
84
85
  #
85
86
  def coerce(other)
86
87
  return nil unless other.respond_to?(:to_date)
87
- return [other.to_date(self), self] if other.method(:to_date).arity == 1
88
+ return [other.to_date, self] if other.method(:to_date).arity.zero?
88
89
 
89
- [other.to_date, self]
90
+ [other.to_date(self), self]
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)
@@ -151,7 +151,12 @@ module OpenHAB
151
151
  # @return [QuantityType] `self` as a {QuantityType} of the supplied Unit
152
152
  #
153
153
  def |(unit) # rubocop:disable Naming/BinaryOperatorParameterName
154
- unit = org.openhab.core.types.util.UnitUtils.parse_unit(unit.to_str) if unit.respond_to?(:to_str)
154
+ if unit.respond_to?(:to_str)
155
+ parsed_unit = org.openhab.core.types.util.UnitUtils.parse_unit(unit.to_str)
156
+ raise ArgumentError, "Unknown unit #{unit}" unless parsed_unit
157
+
158
+ unit = parsed_unit
159
+ end
155
160
 
156
161
  return super unless unit.is_a?(javax.measure.Unit)
157
162
 
@@ -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
 
@@ -185,6 +187,30 @@ module OpenHAB
185
187
  def item_factory
186
188
  @item_factory ||= org.openhab.core.library.CoreItemFactory.new
187
189
  end
190
+
191
+ #
192
+ # Convert the given array to an array of strings.
193
+ # Convert Semantics classes to their simple name.
194
+ #
195
+ # @param [String,Symbol,Semantics::Tag] tags A list of strings, symbols, or Semantics classes
196
+ # @return [Array] An array of strings
197
+ #
198
+ # @example
199
+ # tags = normalize_tags("tag1", Semantics::LivingRoom)
200
+ #
201
+ # @!visibility private
202
+ def normalize_tags(*tags)
203
+ semantics = proc { |tag| tag.respond_to?(:java_class) && tag < Semantics::Tag }
204
+
205
+ tags.compact.map do |tag|
206
+ case tag
207
+ when String then tag
208
+ when Symbol then tag.to_s
209
+ when semantics then tag.java_class.simple_name
210
+ else raise ArgumentError, "`#{tag}` must be a subclass of Semantics::Tag, a `Symbol`, or a `String`."
211
+ end
212
+ end
213
+ end
188
214
  end
189
215
 
190
216
  # @param dimension [Symbol, nil] The unit dimension for a {NumberItem} (see {ItemBuilder#dimension})
@@ -292,18 +318,7 @@ module OpenHAB
292
318
  # @return [void]
293
319
  #
294
320
  def tag(*tags)
295
- unless tags.all? do |tag|
296
- tag.is_a?(String) ||
297
- tag.is_a?(Symbol) ||
298
- (tag.is_a?(Module) && tag < Semantics::Tag)
299
- end
300
- raise ArgumentError, "`tag` must be a subclass of Semantics::Tag, or a `String``."
301
- end
302
-
303
- tags.each do |tag|
304
- tag = tag.name.split("::").last if tag.is_a?(Module) && tag < Semantics::Tag
305
- @tags << tag.to_s
306
- end
321
+ @tags += self.class.normalize_tags(*tags)
307
322
  end
308
323
 
309
324
  #