openhab-scripting 2.16.2 → 2.19.0

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab.rb +12 -16
  3. data/lib/openhab/core/entity_lookup.rb +173 -0
  4. data/lib/openhab/core/openhab_setup.rb +31 -0
  5. data/lib/openhab/core/osgi.rb +61 -0
  6. data/lib/openhab/dsl/actions.rb +105 -0
  7. data/lib/openhab/dsl/dsl.rb +49 -0
  8. data/lib/openhab/{core/dsl → dsl}/gems.rb +0 -1
  9. data/lib/openhab/dsl/group.rb +100 -0
  10. data/lib/openhab/dsl/items/datetime_item.rb +97 -0
  11. data/lib/openhab/dsl/items/items.rb +46 -0
  12. data/lib/openhab/dsl/items/number_item.rb +352 -0
  13. data/lib/openhab/dsl/items/rollershutter_item.rb +179 -0
  14. data/lib/openhab/dsl/items/string_item.rb +120 -0
  15. data/lib/openhab/dsl/monkey_patch/actions/actions.rb +4 -0
  16. data/lib/openhab/dsl/monkey_patch/actions/script_thing_actions.rb +32 -0
  17. data/lib/openhab/dsl/monkey_patch/events/events.rb +5 -0
  18. data/lib/openhab/dsl/monkey_patch/events/item_command.rb +23 -0
  19. data/lib/openhab/dsl/monkey_patch/events/item_state_changed.rb +35 -0
  20. data/lib/openhab/dsl/monkey_patch/events/thing_status_info.rb +33 -0
  21. data/lib/openhab/dsl/monkey_patch/items/contact_item.rb +61 -0
  22. data/lib/openhab/dsl/monkey_patch/items/dimmer_item.rb +193 -0
  23. data/lib/openhab/dsl/monkey_patch/items/group_item.rb +37 -0
  24. data/lib/openhab/dsl/monkey_patch/items/items.rb +133 -0
  25. data/lib/openhab/dsl/monkey_patch/items/metadata.rb +281 -0
  26. data/lib/openhab/dsl/monkey_patch/items/persistence.rb +70 -0
  27. data/lib/openhab/dsl/monkey_patch/items/switch_item.rb +95 -0
  28. data/lib/openhab/dsl/monkey_patch/ruby/number.rb +39 -0
  29. data/lib/openhab/dsl/monkey_patch/ruby/range.rb +47 -0
  30. data/lib/openhab/dsl/monkey_patch/ruby/ruby.rb +8 -0
  31. data/lib/openhab/dsl/monkey_patch/ruby/string.rb +41 -0
  32. data/lib/openhab/dsl/monkey_patch/ruby/time.rb +32 -0
  33. data/lib/openhab/dsl/monkey_patch/types/decimal_type.rb +70 -0
  34. data/lib/openhab/dsl/monkey_patch/types/on_off_type.rb +51 -0
  35. data/lib/openhab/dsl/monkey_patch/types/open_closed_type.rb +36 -0
  36. data/lib/openhab/dsl/monkey_patch/types/percent_type.rb +32 -0
  37. data/lib/openhab/dsl/monkey_patch/types/quantity_type.rb +69 -0
  38. data/lib/openhab/dsl/monkey_patch/types/types.rb +9 -0
  39. data/lib/openhab/dsl/monkey_patch/types/up_down_type.rb +33 -0
  40. data/lib/openhab/dsl/persistence.rb +25 -0
  41. data/lib/openhab/dsl/rules/automation_rule.rb +342 -0
  42. data/lib/openhab/dsl/rules/guard.rb +134 -0
  43. data/lib/openhab/dsl/rules/property.rb +102 -0
  44. data/lib/openhab/dsl/rules/rule.rb +116 -0
  45. data/lib/openhab/dsl/rules/rule_config.rb +151 -0
  46. data/lib/openhab/dsl/rules/triggers/changed.rb +143 -0
  47. data/lib/openhab/dsl/rules/triggers/channel.rb +53 -0
  48. data/lib/openhab/dsl/rules/triggers/command.rb +104 -0
  49. data/lib/openhab/dsl/rules/triggers/cron.rb +177 -0
  50. data/lib/openhab/dsl/rules/triggers/trigger.rb +124 -0
  51. data/lib/openhab/dsl/rules/triggers/updated.rb +98 -0
  52. data/lib/openhab/dsl/states.rb +61 -0
  53. data/lib/openhab/dsl/things.rb +91 -0
  54. data/lib/openhab/dsl/time_of_day.rb +232 -0
  55. data/lib/openhab/dsl/timers.rb +77 -0
  56. data/lib/openhab/dsl/types/datetime.rb +326 -0
  57. data/lib/openhab/dsl/types/quantity.rb +290 -0
  58. data/lib/openhab/dsl/units.rb +39 -0
  59. data/lib/openhab/log/configuration.rb +21 -0
  60. data/lib/openhab/log/logger.rb +172 -0
  61. data/lib/openhab/version.rb +1 -1
  62. metadata +60 -58
  63. data/lib/openhab/configuration.rb +0 -16
  64. data/lib/openhab/core/cron.rb +0 -27
  65. data/lib/openhab/core/debug.rb +0 -34
  66. data/lib/openhab/core/dsl.rb +0 -51
  67. data/lib/openhab/core/dsl/actions.rb +0 -107
  68. data/lib/openhab/core/dsl/entities.rb +0 -147
  69. data/lib/openhab/core/dsl/group.rb +0 -102
  70. data/lib/openhab/core/dsl/items/items.rb +0 -51
  71. data/lib/openhab/core/dsl/items/number_item.rb +0 -323
  72. data/lib/openhab/core/dsl/items/string_item.rb +0 -122
  73. data/lib/openhab/core/dsl/monkey_patch/actions/actions.rb +0 -4
  74. data/lib/openhab/core/dsl/monkey_patch/actions/script_thing_actions.rb +0 -22
  75. data/lib/openhab/core/dsl/monkey_patch/events.rb +0 -5
  76. data/lib/openhab/core/dsl/monkey_patch/events/item_command.rb +0 -13
  77. data/lib/openhab/core/dsl/monkey_patch/events/item_state_changed.rb +0 -25
  78. data/lib/openhab/core/dsl/monkey_patch/events/thing_status_info.rb +0 -26
  79. data/lib/openhab/core/dsl/monkey_patch/items/contact_item.rb +0 -54
  80. data/lib/openhab/core/dsl/monkey_patch/items/dimmer_item.rb +0 -182
  81. data/lib/openhab/core/dsl/monkey_patch/items/group_item.rb +0 -27
  82. data/lib/openhab/core/dsl/monkey_patch/items/items.rb +0 -132
  83. data/lib/openhab/core/dsl/monkey_patch/items/metadata.rb +0 -283
  84. data/lib/openhab/core/dsl/monkey_patch/items/persistence.rb +0 -72
  85. data/lib/openhab/core/dsl/monkey_patch/items/switch_item.rb +0 -87
  86. data/lib/openhab/core/dsl/monkey_patch/ruby/number.rb +0 -41
  87. data/lib/openhab/core/dsl/monkey_patch/ruby/range.rb +0 -47
  88. data/lib/openhab/core/dsl/monkey_patch/ruby/ruby.rb +0 -7
  89. data/lib/openhab/core/dsl/monkey_patch/ruby/string.rb +0 -43
  90. data/lib/openhab/core/dsl/monkey_patch/types/decimal_type.rb +0 -60
  91. data/lib/openhab/core/dsl/monkey_patch/types/on_off_type.rb +0 -41
  92. data/lib/openhab/core/dsl/monkey_patch/types/open_closed_type.rb +0 -25
  93. data/lib/openhab/core/dsl/monkey_patch/types/percent_type.rb +0 -23
  94. data/lib/openhab/core/dsl/monkey_patch/types/quantity_type.rb +0 -58
  95. data/lib/openhab/core/dsl/monkey_patch/types/types.rb +0 -8
  96. data/lib/openhab/core/dsl/persistence.rb +0 -27
  97. data/lib/openhab/core/dsl/property.rb +0 -96
  98. data/lib/openhab/core/dsl/rule/automation_rule.rb +0 -345
  99. data/lib/openhab/core/dsl/rule/guard.rb +0 -136
  100. data/lib/openhab/core/dsl/rule/rule.rb +0 -117
  101. data/lib/openhab/core/dsl/rule/rule_config.rb +0 -153
  102. data/lib/openhab/core/dsl/rule/triggers/changed.rb +0 -145
  103. data/lib/openhab/core/dsl/rule/triggers/channel.rb +0 -55
  104. data/lib/openhab/core/dsl/rule/triggers/command.rb +0 -106
  105. data/lib/openhab/core/dsl/rule/triggers/cron.rb +0 -160
  106. data/lib/openhab/core/dsl/rule/triggers/trigger.rb +0 -126
  107. data/lib/openhab/core/dsl/rule/triggers/updated.rb +0 -100
  108. data/lib/openhab/core/dsl/states.rb +0 -63
  109. data/lib/openhab/core/dsl/things.rb +0 -93
  110. data/lib/openhab/core/dsl/time_of_day.rb +0 -231
  111. data/lib/openhab/core/dsl/timers.rb +0 -79
  112. data/lib/openhab/core/dsl/types/quantity.rb +0 -292
  113. data/lib/openhab/core/dsl/units.rb +0 -41
  114. data/lib/openhab/core/log.rb +0 -170
  115. data/lib/openhab/core/patch_load_path.rb +0 -7
  116. data/lib/openhab/core/startup_delay.rb +0 -23
  117. data/lib/openhab/osgi.rb +0 -59
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+ require 'delegate'
5
+ require 'forwardable'
6
+
7
+ module OpenHAB
8
+ module DSL
9
+ #
10
+ # Provides access to and ruby wrappers around OpenHAB timers
11
+ #
12
+ module Timers
13
+ java_import org.openhab.core.model.script.actions.ScriptExecution
14
+ java_import java.time.ZonedDateTime
15
+
16
+ # Ruby wrapper for OpenHAB Timer
17
+ # This class implements delegator to delegate methods to the OpenHAB timer
18
+ #
19
+ # @author Brian O'Connell
20
+ # @since 2.0.0
21
+ class Timer < SimpleDelegator
22
+ extend Forwardable
23
+
24
+ def_delegator :@timer, :is_active, :active?
25
+ def_delegator :@timer, :is_running, :running?
26
+ def_delegator :@timer, :has_terminated, :terminated?
27
+
28
+ #
29
+ # Create a new Timer Object
30
+ #
31
+ # @param [Duration] duration Duration until timer should fire
32
+ # @param [Block] block Block to execute when timer fires
33
+ #
34
+ def initialize(duration:, &block)
35
+ @duration = duration
36
+
37
+ # A semaphore is used to prevent a race condition in which calling the block from the timer thread
38
+ # occurs before the @timer variable can be set resulting in @timer being nil
39
+ semaphore = Mutex.new
40
+
41
+ timer_block = proc { semaphore.synchronize { block.call(self) } }
42
+
43
+ semaphore.synchronize do
44
+ @timer = ScriptExecution.createTimer(
45
+ ZonedDateTime.now.plus(@duration), timer_block
46
+ )
47
+ super(@timer)
48
+ end
49
+ end
50
+
51
+ #
52
+ # Reschedule timer
53
+ #
54
+ # @param [Duration] duration
55
+ #
56
+ # @return [<Type>] <description>
57
+ #
58
+ def reschedule(duration = nil)
59
+ duration ||= @duration
60
+ @timer.reschedule(ZonedDateTime.now.plus(duration))
61
+ end
62
+ end
63
+
64
+ #
65
+ # Execute the supplied block after the specified duration
66
+ #
67
+ # @param [Duration] duration after which to execute the block
68
+ # @param [Block] block to execute, block is passed a Timer object
69
+ #
70
+ # @return [Timer] Timer object
71
+ #
72
+ def after(duration, &block)
73
+ Timer.new(duration: duration, &block)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+ require 'forwardable'
5
+ require 'time'
6
+
7
+ module OpenHAB
8
+ module DSL
9
+ module Types
10
+ #
11
+ # Ruby implementation for OpenHAB DateTimeType
12
+ #
13
+ # @author Anders Alfredsson
14
+ #
15
+ # rubocop: disable Metrics/ClassLength
16
+ # Disabled because this class has a single responsibility, there does not appear a logical
17
+ # way of breaking it up into multiple classes
18
+ class DateTime
19
+ extend Forwardable
20
+ include Comparable
21
+ include OpenHAB::Log
22
+
23
+ def_delegator :datetime, :to_s
24
+ def_delegator :zoned_date_time, :month_value, :month
25
+ def_delegator :zoned_date_time, :minute, :min
26
+ def_delegator :zoned_date_time, :second, :sec
27
+ def_delegator :zoned_date_time, :nano, :nsec
28
+ def_delegator :zoned_date_time, :to_epoch_second, :to_i
29
+ alias inspect to_s
30
+
31
+ java_import Java::OrgOpenhabCoreLibraryTypes::DateTimeType
32
+ java_import java.time.ZonedDateTime
33
+ java_import java.time.Instant
34
+ java_import java.time.ZoneId
35
+ java_import java.time.ZoneOffset
36
+ java_import java.time.Duration
37
+
38
+ #
39
+ # Regex expression to identify strings defining a time in hours, minutes and optionally seconds
40
+ #
41
+ TIME_ONLY_REGEX = /\A\d\d:\d\d(:\d\d)?\Z/.freeze
42
+
43
+ #
44
+ # Regex expression to identify strings defining a time in hours, minutes and optionally seconds
45
+ #
46
+ DATE_ONLY_REGEX = /\A\d{4}-\d\d-\d\d\Z/.freeze
47
+
48
+ attr_reader :datetime
49
+
50
+ #
51
+ # Create a new DateTime instance wrapping an OpenHAB DateTimeType
52
+ #
53
+ # @param [Java::org::openhab::core::library::types::DateTimeType] datetime The DateTimeType instance to
54
+ # delegate to, or an object that can be converted to a DateTimeType
55
+ #
56
+ def initialize(datetime)
57
+ @datetime = case datetime
58
+ when DateTimeType
59
+ datetime
60
+ when ZonedDateTime
61
+ DateTimeType.new(datetime)
62
+ else
63
+ raise "Unexpected type #{datetime.class} provided to DateTime initializer"
64
+ end
65
+ end
66
+
67
+ #
68
+ # Compare thes DateTime object to another
69
+ #
70
+ # @param [Object] other Other object to compare against
71
+ #
72
+ # @return [Integer] -1, 0 or 1 depending on the outcome
73
+ #
74
+ def <=>(other)
75
+ case other
76
+ when DateTime, DateTimeType, DateTimeItem
77
+ zoned_date_time.to_instant.compare_to(other.zoned_date_time.to_instant)
78
+ when TimeOfDay::TimeOfDay, TimeOfDay::TimeOfDayRangeElement
79
+ to_tod <=> other
80
+ when String
81
+ self <=> DateTime.parse(DATE_ONLY_REGEX =~ other ? "#{other}'T'00:00:00#{zone}" : other)
82
+ else
83
+ self <=> DateTime.from(other)
84
+ end
85
+ end
86
+
87
+ #
88
+ # Adds another object to this DateTime
89
+ #
90
+ # @param [Object] other Object to add to this. Can be a Numeric, another DateTime/Time/DateTimeType, a
91
+ # Duration or a String that can be parsed into a DateTimeType or Time object
92
+ #
93
+ # @return [DateTime] A new DateTime object representing the result of the calculation
94
+ #
95
+ def +(other)
96
+ logger.trace("Adding #{other} (#{other.class}) to #{self}")
97
+ case other
98
+ when Numeric then DateTime.from(to_time + other)
99
+ when DateTime, Time then self + other.to_f
100
+ when DateTimeType, String then self + DateTime.from(other).to_f
101
+ when Duration then DateTime.new(zoned_date_time.plus(other))
102
+ end
103
+ end
104
+
105
+ #
106
+ # Subtracts another object from this DateTime
107
+ #
108
+ # @param [Object] other Object to subtract fom this. Can be a Numeric, another DateTime/Time/DateTimeType, a
109
+ # Duration or a String that can be parsed into a DateTimeType or Time object
110
+ #
111
+ # @return [DateTime, Float] A new DateTime object representing the result of the calculation, or a Float
112
+ # representing the time difference in seconds if the subtraction is between two time objects
113
+ #
114
+ def -(other)
115
+ logger.trace("Subtracting #{other} (#{other.class}) from self")
116
+ case other
117
+ when Numeric then DateTime.from(to_time - other)
118
+ when String
119
+ dt = DateTime.parse(other)
120
+ TIME_ONLY_REGEX =~ other ? self - dt.to_f : time_diff(dt)
121
+ when Duration then DateTime.new(zoned_date_time.minus(other))
122
+ when Time, DateTime, DateTimeType, DateTimeItem then time_diff(other)
123
+ end
124
+ end
125
+
126
+ #
127
+ # Convert this DateTime to a ruby Time object
128
+ #
129
+ # @return [Time] A Time object representing the same instant and timezone
130
+ #
131
+ def to_time
132
+ Time.at(to_i, nsec, :nsec).localtime(utc_offset)
133
+ end
134
+
135
+ #
136
+ # Convert the time part of this DateTime to a TimeOfDay object
137
+ #
138
+ # @return [TimeOfDay] A TimeOfDay object representing the time
139
+ #
140
+ def to_time_of_day
141
+ TimeOfDay::TimeOfDay.new(h: hour, m: minute, s: second)
142
+ end
143
+
144
+ alias to_tod to_time_of_day
145
+
146
+ #
147
+ # Returns the value of time as a floating point number of seconds since the Epoch
148
+ #
149
+ # @return [Float] Number of seconds since the Epoch, with nanosecond presicion
150
+ #
151
+ def to_f
152
+ zoned_date_time.to_epoch_second + zoned_date_time.nano / 1_000_000_000
153
+ end
154
+
155
+ #
156
+ # The ZonedDateTime representing the state
157
+ #
158
+ # @return [Java::java::time::ZonedDateTime] ZonedDateTime representing the state
159
+ #
160
+ def zoned_date_time
161
+ @datetime.zonedDateTime
162
+ end
163
+
164
+ alias to_zdt zoned_date_time
165
+
166
+ #
167
+ # The offset in seconds from UTC
168
+ #
169
+ # @return [Integer] The offset from UTC, in seconds
170
+ #
171
+ def utc_offset
172
+ zoned_date_time.offset.total_seconds
173
+ end
174
+
175
+ #
176
+ # Returns true if time represents a time in UTC (GMT)
177
+ #
178
+ # @return [Boolean] true if utc_offset == 0, false otherwise
179
+ #
180
+ def utc?
181
+ utc_offset.zero?
182
+ end
183
+
184
+ #
185
+ # The timezone
186
+ #
187
+ # @return [String] The timezone in `[+-]hh:mm(:ss)` format ('Z' for UTC) or nil if the Item has no state
188
+ #
189
+ def zone
190
+ zoned_date_time.zone.id
191
+ end
192
+
193
+ #
194
+ # Check if missing method can be delegated to other contained objects
195
+ #
196
+ # @param [String, Symbol] meth the method name to check for
197
+ #
198
+ # @return [Boolean] true if DateTimeType, ZonedDateTime or Time responds to the method, false otherwise
199
+ #
200
+ def respond_to_missing?(meth, *)
201
+ @datetime.respond_to?(meth) ||
202
+ zoned_date_time.respond_to?(meth) ||
203
+ Time.instance_methods.include?(meth.to_sym)
204
+ end
205
+
206
+ #
207
+ # Forward missing methods to the OpenHAB DateTimeType, its ZonedDateTime object or a ruby Time
208
+ # object representing the same instant
209
+ #
210
+ # @param [String] meth method name
211
+ # @param [Array] args arguments for method
212
+ # @param [Proc] block <description>
213
+ #
214
+ # @return [Object] Value from delegated method in OpenHAB NumberItem
215
+ #
216
+ def method_missing(meth, *args, &block)
217
+ if @datetime.respond_to?(meth)
218
+ @datetime.__send__(meth, *args, &block)
219
+ elsif zoned_date_time.respond_to?(meth)
220
+ zoned_date_time.__send__(meth, *args, &block)
221
+ elsif Time.instance_methods.include?(meth.to_sym)
222
+ to_time.send(meth, *args, &block)
223
+ else
224
+ raise NoMethodError, "undefined method `#{meth}' for #{self.class}"
225
+ end
226
+ end
227
+
228
+ #
229
+ # Converts other objects to a DateTimeType
230
+ #
231
+ # @param [String, Numeric, Time] datetime an object that can be parsed or converted into
232
+ # a DateTimeType
233
+ #
234
+ # @return [Java::org::openhab::core::library::types::DateTimeType] Object representing the same time
235
+ #
236
+ def self.from(datetime)
237
+ case datetime
238
+ when String
239
+ parse(datetime)
240
+ when Numeric
241
+ from_numeric(datetime)
242
+ when Time
243
+ from_time(datetime)
244
+ else
245
+ raise "Cannot convert #{datetime.class} to DateTime"
246
+ end
247
+ end
248
+
249
+ #
250
+ # Converts a Numeric into a DateTimeType
251
+ #
252
+ # @param [Numeric] numeric A Integer or Float representing the number of seconds since the epoch
253
+ #
254
+ # @return [Java::org::openhab::core::library::types::DateTimeType] Object representing the same time
255
+ #
256
+ def self.from_numeric(numeric)
257
+ case numeric
258
+ when Integer
259
+ DateTime.new(ZonedDateTime.ofInstant(Instant.ofEpochSecond(datetime), ZoneId.systemDefault))
260
+ else
261
+ DateTime.new(ZonedDateTime.ofInstant(Instant.ofEpochSecond(datetime.to_i,
262
+ ((datetime % 1) * 1_000_000_000).to_i),
263
+ ZoneId.systemDefault))
264
+ end
265
+ end
266
+
267
+ #
268
+ # Converts a ruby Time object to an OpenHAB DateTimeType
269
+ #
270
+ # @param [Time] time The Time object to be converted
271
+ #
272
+ # @return [Java::org::openhab::core::library::types::DateTimeType] Object representing the same time
273
+ #
274
+ def self.from_time(time)
275
+ instant = Instant.ofEpochSecond(time.to_i, time.nsec)
276
+ zone_id = ZoneId.of_offset('UTC', ZoneOffset.of_total_seconds(time.utc_offset))
277
+ DateTime.new(ZonedDateTime.ofInstant(instant, zone_id))
278
+ end
279
+
280
+ #
281
+ # Parses a string representing a time into an OpenHAB DateTimeType. First tries to parse it
282
+ # using the DateTimeType's parser, then falls back to the ruby Time.parse
283
+ #
284
+ # @param [String] time_string The string to be parsed
285
+ #
286
+ # @return [Java::org::openhab::core::library::types::DateTimeType] Object representing the same time
287
+ #
288
+ def self.parse(time_string)
289
+ time_string += 'Z' if TIME_ONLY_REGEX =~ time_string
290
+ DateTime.new(DateTimeType.new(time_string))
291
+ rescue Java::JavaLang::StringIndexOutOfBoundsException, Java::JavaLang::IllegalArgumentException
292
+ # Try ruby's Time.parse if OpenHAB's DateTimeType parser fails
293
+ begin
294
+ time = Time.parse(time_string)
295
+ DateTime.from(time)
296
+ rescue ArgumentError
297
+ raise "Unable to parse #{time_string} into a DateTime"
298
+ end
299
+ end
300
+
301
+ private
302
+
303
+ #
304
+ # Calculates the difference in time between this instance and another time object
305
+ #
306
+ # @param [Time, DateTime, DateTimeItem, Java::org::openhab::core::library::types::DateTimeType] time_obj
307
+ # The other time object to subtract from self
308
+ #
309
+ # @return [Float] The time difference between the two objects, in seconds
310
+ #
311
+ def time_diff(time_obj)
312
+ logger.trace("Calculate time difference between #{self} and #{time_obj}")
313
+ case time_obj
314
+ when Time
315
+ to_time - time_obj
316
+ when DateTime, DateTimeItem
317
+ self - time_obj.to_time
318
+ when DateTimeType
319
+ self - DateTime.new(time_obj).to_time
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+ # rubocop: enable Metrics/ClassLength
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+ require 'forwardable'
5
+
6
+ module OpenHAB
7
+ module DSL
8
+ #
9
+ # Ruby implementation of OpenHAB Types
10
+ #
11
+ module Types
12
+ #
13
+ # Ruby implementation for OpenHAB quantities
14
+ #
15
+ # rubocop: disable Metrics/ClassLength
16
+ # Disabled because this class has a single responsibility, there does not appear a logical
17
+ # way of breaking it up into multiple classes
18
+ class Quantity < Numeric
19
+ extend Forwardable
20
+ include OpenHAB::Log
21
+
22
+ def_delegator :@quantity, :to_s
23
+
24
+ java_import org.openhab.core.library.types.QuantityType
25
+ java_import 'tec.uom.se.format.SimpleUnitFormat'
26
+ java_import 'tec.uom.se.AbstractUnit'
27
+
28
+ # @return [Hash] Mapping of operation symbols to BigDecimal methods
29
+ OPERATIONS = {
30
+ '+' => 'add',
31
+ '-' => 'subtract',
32
+ '*' => 'multiply',
33
+ '/' => 'divide'
34
+ }.freeze
35
+
36
+ private_constant :OPERATIONS
37
+
38
+ attr_reader :quantity
39
+
40
+ #
41
+ # Create a new Quantity
42
+ #
43
+ # @param [object] quantity String,QuantityType or Numeric to be this quantity
44
+ #
45
+ # Cop disabled, case statement is compact and idiomatic
46
+ def initialize(quantity)
47
+ @quantity = case quantity
48
+ when String then QuantityType.new(quantity)
49
+ when QuantityType then quantity
50
+ when NumberItem, Numeric then QuantityType.new(quantity.to_d.to_java, AbstractUnit::ONE)
51
+ else raise ArgumentError, "Unexpected type #{quantity.class} provided to Quantity initializer"
52
+ end
53
+ super()
54
+ end
55
+
56
+ #
57
+ # Convert this quantity into a another unit
58
+ #
59
+ # @param [Object] other String or Unit to convert to
60
+ #
61
+ # @return [Quantity] This quantity converted to another unit
62
+ #
63
+ def |(other)
64
+ other = SimpleUnitFormat.instance.unitFor(other) if other.is_a? String
65
+
66
+ Quantity.new(quantity.to_unit(other))
67
+ end
68
+
69
+ #
70
+ # Compare this quantity
71
+ #
72
+ # @param [Object] other object to compare to
73
+ #
74
+ # @return [Integer] -1,0,1 if this object is less than, equal to, or greater than the supplied object,
75
+ # nil if it cannot be compared
76
+ #
77
+ def <=>(other)
78
+ logger.trace("Comparing #{self} to #{other}")
79
+ my_qt, other_qt = unitize(*to_qt(coerce(other).reverse))
80
+ my_qt.compare_to(other_qt)
81
+ end
82
+
83
+ #
84
+ # Coerce other object into a Quantity
85
+ #
86
+ # @param [Object] other object to convert to Quantity
87
+ #
88
+ # @return [Array] of self and other object as Quantity types, nil if object cannot be coerced
89
+ #
90
+ def coerce(other)
91
+ logger.trace("Coercing #{self} as a request from #{other.class}")
92
+ case other
93
+ when Quantity then [other.quantity, quantity]
94
+ when QuantityType then [other, quantity]
95
+ when NumberItem then [other.to_qt.quantity, quantity]
96
+ when Numeric, String then [Quantity.new(other), self]
97
+ end
98
+ end
99
+
100
+ #
101
+ # Forward missing methods to Openhab Quantity Item if they are defined
102
+ #
103
+ # @param [String] meth name of method invoked
104
+ # @param [Array] args arguments to invoked method
105
+ # @param [Proc] block block passed ot method
106
+ #
107
+ # @return [Object] result of delegation
108
+ #
109
+ def method_missing(meth, *args, &block)
110
+ logger.trace("Method missing, performing dynamic lookup for: #{meth}")
111
+ if quantity.respond_to?(meth)
112
+ quantity.__send__(meth, *args, &block)
113
+ elsif ::Kernel.method_defined?(meth) || ::Kernel.private_method_defined?(meth)
114
+ ::Kernel.instance_method(meth).bind_call(self, *args, &block)
115
+ else
116
+ super(meth, *args, &block)
117
+ end
118
+ end
119
+
120
+ #
121
+ # Checks if this method responds to the missing method
122
+ #
123
+ # @param [String] method_name Name of the method to check
124
+ # @param [Boolean] _include_private boolean if private methods should be checked
125
+ #
126
+ # @return [Boolean] true if this object will respond to the supplied method, false otherwise
127
+ #
128
+ def respond_to_missing?(method_name, _include_private = false)
129
+ quantity.respond_to?(method_name) ||
130
+ ::Kernel.method_defined?(method_name) ||
131
+ ::Kernel.private_method_defined?(method_name)
132
+ end
133
+
134
+ #
135
+ # Negate the quantity
136
+ #
137
+ # @return [Quantity] This quantity negated
138
+ #
139
+ def -@
140
+ Quantity.new(quantity.negate)
141
+ end
142
+
143
+ OPERATIONS.each do |operation, method|
144
+ define_method(operation) do |other|
145
+ logger.trace("Executing math operation '#{operation}' on quantity #{inspect} "\
146
+ "with other type #{other.class} and value #{other.inspect}")
147
+
148
+ a, b = to_qt(coerce(other).reverse)
149
+ logger.trace("Coerced a='#{a}' with b='#{b}'")
150
+ a, b = unitize(a, b, operation)
151
+ logger.trace("Unitized a='#{a}' b='#{b}'")
152
+ logger.trace("Performing operation '#{operation}' with method '#{method}' on a='#{a}' with b='#{b}'")
153
+ Quantity.new(a.public_send(method, b))
154
+ end
155
+ end
156
+
157
+ #
158
+ # Provide details about quantity object
159
+ #
160
+ # @return [String] Representing details about the quantity object
161
+ #
162
+ def inspect
163
+ if @quantity.unit == AbstractUnit::ONE
164
+ "unit=#{@quantity.unit}, value=#{@quantity.to_string}"
165
+ else
166
+ @quantity.to_string
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ # @return [Array] Array of strings for operations for which the operands will not be unitized
173
+ DIMENSIONLESS_NON_UNITIZED_OPERATIONS = %w[* /].freeze
174
+
175
+ # Dimensionless numbers should only be unitzed for addition and subtraction
176
+
177
+ #
178
+ # Convert one or more Quantity obects to the underlying quantitytypes
179
+ #
180
+ # @param [Array] quanities Array of either Quantity or QuantityType objects
181
+ #
182
+ # @return [Array] Array of QuantityType objects
183
+ #
184
+ def to_qt(*quanities)
185
+ [quanities].flatten.compact.map { |item| item.is_a?(Quantity) ? item.quantity : item }
186
+ end
187
+
188
+ #
189
+ # Checks if an item should be unitized
190
+ #
191
+ # @param [Quantity] quantity to check
192
+ # @param [String] operation quantity is being used with
193
+ #
194
+ # @return [Boolean] True if the quantity should be unitzed based on the unit and operation, false otherwise
195
+ #
196
+ def unitize?(quantity, operation)
197
+ !(quantity.unit == AbstractUnit::ONE && DIMENSIONLESS_NON_UNITIZED_OPERATIONS.include?(operation))
198
+ end
199
+
200
+ #
201
+ # Convert the unit for the quantity
202
+ #
203
+ # @param [Quantity] quantity being converted
204
+ #
205
+ # @return [Quantity] Quantity coverted to unit set by unit block
206
+ #
207
+ def convert_unit(quantity)
208
+ return quantity unless unit?
209
+
210
+ case quantity.unit
211
+ when unit
212
+ quantity
213
+ when AbstractUnit::ONE
214
+ convert_unit_from_dimensionless(quantity, unit)
215
+ else
216
+ convert_unit_from_dimensioned(quantity, unit)
217
+ end
218
+ end
219
+
220
+ #
221
+ # Converts a dimensioned quantity to a specific unit
222
+ #
223
+ # @param [Quantity] quantity to convert
224
+ # @param [Unit] unit to convert to
225
+ #
226
+ # @return [Java::org::openhab::core::library::types::QuantityType] converted quantity
227
+ #
228
+ def convert_unit_from_dimensioned(quantity, unit)
229
+ logger.trace("Converting dimensioned item #{inspect} to #{unit}")
230
+ quantity.to_unit(unit).tap do |converted|
231
+ raise "Conversion from #{quantity.unit} to #{unit} failed" unless converted
232
+ end
233
+ end
234
+
235
+ #
236
+ # Converts a dimensionless quantity to a unit
237
+ #
238
+ # @param [Quantity] quantity to convert
239
+ # @param [Unit] unit to convert to
240
+ #
241
+ # @return [Java::org::openhab::core::library::types::QuantityType] converted quantity
242
+ #
243
+ def convert_unit_from_dimensionless(quantity, unit)
244
+ logger.trace("Converting dimensionless #{quantity} to #{unit}")
245
+ QuantityType.new(quantity.to_big_decimal, unit)
246
+ end
247
+
248
+ #
249
+ # Convert quantities to appropriate units
250
+ #
251
+ # @param [Quantity] quantity_a Quantity on left side of operation
252
+ # @param [Quantity] quantity_b Quantity on right side of operation
253
+ # @param [String] operation Math operation
254
+ # @yield [quantity_a, quantity_b] yields unitized versions of supplied quantities
255
+ #
256
+ # @return [Array, Object] of quantites in correct units for the supplied operation and the unit
257
+ # or the result of the block if a block is given
258
+ #
259
+ def unitize(quantity_a, quantity_b, operation = nil)
260
+ logger.trace("Unitizing (#{quantity_a}) and (#{quantity_b})")
261
+ quantity_a, quantity_b = [quantity_a, quantity_b].map do |qt|
262
+ unitize?(qt, operation) ? convert_unit(qt) : qt
263
+ end
264
+ return yield quantity_a, quantity_b if block_given?
265
+
266
+ [quantity_a, quantity_b]
267
+ end
268
+
269
+ #
270
+ # Get the unit from the current thread local variable
271
+ #
272
+ # @return [Object] Unit or string representation of Unit, or nil if not set
273
+ #
274
+ def unit
275
+ Thread.current.thread_variable_get(:unit)
276
+ end
277
+
278
+ #
279
+ # Is a unit set for this thread
280
+ #
281
+ # @return [boolean] true if a unit is set by this thread, false otherwise
282
+ #
283
+ def unit?
284
+ unit != nil
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+ # rubocop: enable Metrics/ClassLength