openhab-scripting 2.16.1 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab.rb +12 -16
  3. data/lib/openhab/core/entity_lookup.rb +168 -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/string_item.rb +120 -0
  14. data/lib/openhab/dsl/monkey_patch/actions/actions.rb +4 -0
  15. data/lib/openhab/dsl/monkey_patch/actions/script_thing_actions.rb +32 -0
  16. data/lib/openhab/dsl/monkey_patch/events/events.rb +5 -0
  17. data/lib/openhab/dsl/monkey_patch/events/item_command.rb +23 -0
  18. data/lib/openhab/dsl/monkey_patch/events/item_state_changed.rb +35 -0
  19. data/lib/openhab/dsl/monkey_patch/events/thing_status_info.rb +33 -0
  20. data/lib/openhab/dsl/monkey_patch/items/contact_item.rb +61 -0
  21. data/lib/openhab/dsl/monkey_patch/items/dimmer_item.rb +193 -0
  22. data/lib/openhab/dsl/monkey_patch/items/group_item.rb +37 -0
  23. data/lib/openhab/dsl/monkey_patch/items/items.rb +133 -0
  24. data/lib/openhab/dsl/monkey_patch/items/metadata.rb +281 -0
  25. data/lib/openhab/dsl/monkey_patch/items/persistence.rb +70 -0
  26. data/lib/openhab/dsl/monkey_patch/items/switch_item.rb +95 -0
  27. data/lib/openhab/dsl/monkey_patch/ruby/number.rb +39 -0
  28. data/lib/openhab/dsl/monkey_patch/ruby/range.rb +47 -0
  29. data/lib/openhab/dsl/monkey_patch/ruby/ruby.rb +8 -0
  30. data/lib/openhab/dsl/monkey_patch/ruby/string.rb +41 -0
  31. data/lib/openhab/dsl/monkey_patch/ruby/time.rb +32 -0
  32. data/lib/openhab/dsl/monkey_patch/types/decimal_type.rb +70 -0
  33. data/lib/openhab/dsl/monkey_patch/types/on_off_type.rb +51 -0
  34. data/lib/openhab/dsl/monkey_patch/types/open_closed_type.rb +36 -0
  35. data/lib/openhab/dsl/monkey_patch/types/percent_type.rb +32 -0
  36. data/lib/openhab/dsl/monkey_patch/types/quantity_type.rb +69 -0
  37. data/lib/openhab/dsl/monkey_patch/types/types.rb +8 -0
  38. data/lib/openhab/dsl/persistence.rb +25 -0
  39. data/lib/openhab/dsl/rules/automation_rule.rb +342 -0
  40. data/lib/openhab/dsl/rules/guard.rb +134 -0
  41. data/lib/openhab/dsl/rules/property.rb +102 -0
  42. data/lib/openhab/dsl/rules/rule.rb +116 -0
  43. data/lib/openhab/dsl/rules/rule_config.rb +151 -0
  44. data/lib/openhab/dsl/rules/triggers/changed.rb +143 -0
  45. data/lib/openhab/dsl/rules/triggers/channel.rb +53 -0
  46. data/lib/openhab/dsl/rules/triggers/command.rb +104 -0
  47. data/lib/openhab/dsl/rules/triggers/cron.rb +177 -0
  48. data/lib/openhab/dsl/rules/triggers/trigger.rb +124 -0
  49. data/lib/openhab/dsl/rules/triggers/updated.rb +98 -0
  50. data/lib/openhab/dsl/states.rb +61 -0
  51. data/lib/openhab/dsl/things.rb +91 -0
  52. data/lib/openhab/dsl/time_of_day.rb +232 -0
  53. data/lib/openhab/dsl/timers.rb +77 -0
  54. data/lib/openhab/dsl/types/datetime.rb +326 -0
  55. data/lib/openhab/dsl/types/quantity.rb +290 -0
  56. data/lib/openhab/dsl/units.rb +39 -0
  57. data/lib/openhab/log/configuration.rb +21 -0
  58. data/lib/openhab/log/logger.rb +172 -0
  59. data/lib/openhab/version.rb +1 -1
  60. metadata +58 -58
  61. data/lib/openhab/configuration.rb +0 -16
  62. data/lib/openhab/core/cron.rb +0 -27
  63. data/lib/openhab/core/debug.rb +0 -34
  64. data/lib/openhab/core/dsl.rb +0 -51
  65. data/lib/openhab/core/dsl/actions.rb +0 -107
  66. data/lib/openhab/core/dsl/entities.rb +0 -140
  67. data/lib/openhab/core/dsl/group.rb +0 -93
  68. data/lib/openhab/core/dsl/items/items.rb +0 -51
  69. data/lib/openhab/core/dsl/items/number_item.rb +0 -323
  70. data/lib/openhab/core/dsl/items/string_item.rb +0 -122
  71. data/lib/openhab/core/dsl/monkey_patch/actions/actions.rb +0 -4
  72. data/lib/openhab/core/dsl/monkey_patch/actions/script_thing_actions.rb +0 -22
  73. data/lib/openhab/core/dsl/monkey_patch/events.rb +0 -5
  74. data/lib/openhab/core/dsl/monkey_patch/events/item_command.rb +0 -13
  75. data/lib/openhab/core/dsl/monkey_patch/events/item_state_changed.rb +0 -25
  76. data/lib/openhab/core/dsl/monkey_patch/events/thing_status_info.rb +0 -26
  77. data/lib/openhab/core/dsl/monkey_patch/items/contact_item.rb +0 -54
  78. data/lib/openhab/core/dsl/monkey_patch/items/dimmer_item.rb +0 -182
  79. data/lib/openhab/core/dsl/monkey_patch/items/group_item.rb +0 -27
  80. data/lib/openhab/core/dsl/monkey_patch/items/items.rb +0 -132
  81. data/lib/openhab/core/dsl/monkey_patch/items/metadata.rb +0 -283
  82. data/lib/openhab/core/dsl/monkey_patch/items/persistence.rb +0 -72
  83. data/lib/openhab/core/dsl/monkey_patch/items/switch_item.rb +0 -87
  84. data/lib/openhab/core/dsl/monkey_patch/ruby/number.rb +0 -41
  85. data/lib/openhab/core/dsl/monkey_patch/ruby/range.rb +0 -47
  86. data/lib/openhab/core/dsl/monkey_patch/ruby/ruby.rb +0 -7
  87. data/lib/openhab/core/dsl/monkey_patch/ruby/string.rb +0 -43
  88. data/lib/openhab/core/dsl/monkey_patch/types/decimal_type.rb +0 -60
  89. data/lib/openhab/core/dsl/monkey_patch/types/on_off_type.rb +0 -41
  90. data/lib/openhab/core/dsl/monkey_patch/types/open_closed_type.rb +0 -25
  91. data/lib/openhab/core/dsl/monkey_patch/types/percent_type.rb +0 -23
  92. data/lib/openhab/core/dsl/monkey_patch/types/quantity_type.rb +0 -58
  93. data/lib/openhab/core/dsl/monkey_patch/types/types.rb +0 -8
  94. data/lib/openhab/core/dsl/persistence.rb +0 -27
  95. data/lib/openhab/core/dsl/property.rb +0 -96
  96. data/lib/openhab/core/dsl/rule/automation_rule.rb +0 -348
  97. data/lib/openhab/core/dsl/rule/guard.rb +0 -136
  98. data/lib/openhab/core/dsl/rule/rule.rb +0 -117
  99. data/lib/openhab/core/dsl/rule/rule_config.rb +0 -153
  100. data/lib/openhab/core/dsl/rule/triggers/changed.rb +0 -145
  101. data/lib/openhab/core/dsl/rule/triggers/channel.rb +0 -55
  102. data/lib/openhab/core/dsl/rule/triggers/command.rb +0 -106
  103. data/lib/openhab/core/dsl/rule/triggers/cron.rb +0 -160
  104. data/lib/openhab/core/dsl/rule/triggers/trigger.rb +0 -126
  105. data/lib/openhab/core/dsl/rule/triggers/updated.rb +0 -100
  106. data/lib/openhab/core/dsl/states.rb +0 -63
  107. data/lib/openhab/core/dsl/things.rb +0 -93
  108. data/lib/openhab/core/dsl/time_of_day.rb +0 -231
  109. data/lib/openhab/core/dsl/timers.rb +0 -79
  110. data/lib/openhab/core/dsl/types/quantity.rb +0 -292
  111. data/lib/openhab/core/dsl/units.rb +0 -41
  112. data/lib/openhab/core/log.rb +0 -170
  113. data/lib/openhab/core/patch_load_path.rb +0 -7
  114. data/lib/openhab/core/startup_delay.rb +0 -23
  115. 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