openhab-scripting 2.14.0 → 2.16.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab.rb +3 -0
  3. data/lib/openhab/core/dsl.rb +4 -0
  4. data/lib/openhab/core/dsl/actions.rb +1 -1
  5. data/lib/openhab/core/dsl/entities.rb +41 -4
  6. data/lib/openhab/core/dsl/gems.rb +1 -1
  7. data/lib/openhab/core/dsl/group.rb +3 -1
  8. data/lib/openhab/core/dsl/items/items.rb +3 -1
  9. data/lib/openhab/core/dsl/items/number_item.rb +158 -52
  10. data/lib/openhab/core/dsl/items/string_item.rb +23 -3
  11. data/lib/openhab/core/dsl/monkey_patch/items/dimmer_item.rb +20 -5
  12. data/lib/openhab/core/dsl/monkey_patch/items/items.rb +2 -0
  13. data/lib/openhab/core/dsl/monkey_patch/items/metadata.rb +66 -42
  14. data/lib/openhab/core/dsl/monkey_patch/items/persistence.rb +72 -0
  15. data/lib/openhab/core/dsl/monkey_patch/items/switch_item.rb +2 -1
  16. data/lib/openhab/core/dsl/monkey_patch/ruby/range.rb +2 -1
  17. data/lib/openhab/core/dsl/monkey_patch/ruby/ruby.rb +2 -0
  18. data/lib/openhab/core/dsl/monkey_patch/ruby/string.rb +43 -0
  19. data/lib/openhab/core/dsl/monkey_patch/types/decimal_type.rb +41 -5
  20. data/lib/openhab/core/dsl/monkey_patch/types/quantity_type.rb +58 -0
  21. data/lib/openhab/core/dsl/monkey_patch/types/types.rb +1 -0
  22. data/lib/openhab/core/dsl/persistence.rb +27 -0
  23. data/lib/openhab/core/dsl/property.rb +15 -4
  24. data/lib/openhab/core/dsl/rule/automation_rule.rb +348 -0
  25. data/lib/openhab/core/dsl/rule/guard.rb +43 -6
  26. data/lib/openhab/core/dsl/rule/rule.rb +80 -367
  27. data/lib/openhab/core/dsl/rule/rule_config.rb +153 -0
  28. data/lib/openhab/core/dsl/rule/triggers/changed.rb +145 -0
  29. data/lib/openhab/core/dsl/rule/{channel.rb → triggers/channel.rb} +22 -8
  30. data/lib/openhab/core/dsl/rule/triggers/command.rb +106 -0
  31. data/lib/openhab/core/dsl/rule/{cron.rb → triggers/cron.rb} +36 -14
  32. data/lib/openhab/core/dsl/rule/triggers/trigger.rb +126 -0
  33. data/lib/openhab/core/dsl/rule/triggers/updated.rb +100 -0
  34. data/lib/openhab/core/dsl/time_of_day.rb +50 -24
  35. data/lib/openhab/core/dsl/timers.rb +2 -6
  36. data/lib/openhab/core/dsl/types/quantity.rb +106 -69
  37. data/lib/openhab/core/log.rb +3 -8
  38. data/lib/openhab/core/startup_delay.rb +1 -0
  39. data/lib/openhab/osgi.rb +7 -0
  40. data/lib/openhab/version.rb +1 -1
  41. metadata +14 -6
  42. data/lib/openhab/core/dsl/rule/item.rb +0 -203
  43. data/lib/openhab/core/dsl/rule/triggers.rb +0 -77
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+
5
+ #
6
+ # MonkeyPatching QuantityType
7
+ #
8
+ # rubocop:disable Style/ClassAndModuleChildren
9
+ class Java::OrgOpenhabCoreLibraryTypes::QuantityType
10
+ # rubocop:enable Style/ClassAndModuleChildren
11
+
12
+ #
13
+ # Compare QuantityType to supplied object
14
+ #
15
+ # @param [Object] other object to compare to
16
+ #
17
+ # @return [Integer] -1,0,1 or nil depending on value supplied, nil comparison to supplied object is not possible.
18
+ #
19
+ def <=>(other)
20
+ logger.trace("#{self.class} #{self} <=> #{other} (#{other.class})")
21
+ case other
22
+ when Java::OrgOpenhabCoreTypes::UnDefType then 1
23
+ when String then self <=> Quantity.new(other)
24
+ when OpenHAB::Core::DSL::Types::Quantity then self <=> other.quantity
25
+ else
26
+ other = other.state if other.respond_to? :state
27
+ compare_to(other)
28
+ end
29
+ end
30
+
31
+ #
32
+ # Coerce objects into a QuantityType
33
+ #
34
+ # @param [Object] other object to coerce to a QuantityType if possible
35
+ #
36
+ # @return [Object] Numeric when applicable
37
+ #
38
+ def coerce(other)
39
+ logger.trace("Coercing #{self} as a request from #{other.class}")
40
+ case other
41
+ when String
42
+ [Quantity.new(other), self]
43
+ else
44
+ [other, self]
45
+ end
46
+ end
47
+
48
+ #
49
+ # Compare self to other using the spaceship operator
50
+ #
51
+ # @param [Object] other object to compare to
52
+ #
53
+ # @return [Boolean] True if equals
54
+ #
55
+ def ==(other)
56
+ (self <=> other).zero?
57
+ end
58
+ end
@@ -5,3 +5,4 @@ require 'core/dsl/monkey_patch/types/open_closed_type'
5
5
  require 'core/dsl/monkey_patch/types/on_off_type'
6
6
  require 'core/dsl/monkey_patch/types/decimal_type'
7
7
  require 'core/dsl/monkey_patch/types/percent_type'
8
+ require 'core/dsl/monkey_patch/types/quantity_type'
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenHAB
4
+ module Core
5
+ module DSL
6
+ #
7
+ # Provides support for interacting with OpenHAB Persistence service
8
+ #
9
+ module Persistence
10
+ #
11
+ # Sets a thread local variable to set the default persistence service
12
+ # for method calls inside the block
13
+ #
14
+ # @param [Object] Persistence service either as a String or a Symbol
15
+ # @yield [] Block executed in context of the supplied persistence service
16
+ #
17
+ #
18
+ def persistence(service)
19
+ Thread.current.thread_variable_set(:persistence_service, service)
20
+ yield
21
+ ensure
22
+ Thread.current.thread_variable_set(:persistence_service, nil)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -29,7 +29,14 @@ module DSLProperty
29
29
  # @param [String] name of the property
30
30
  #
31
31
  #
32
+ # rubocop:disable Metrics/MethodLength
33
+ # rubocop:disable Metrics/AbcSize
34
+ # rubocop:disable Metrics/CyclomaticComplexity
35
+ # rubocop:disable Metrics/PerceivedComplexity
32
36
  def prop(name)
37
+ # rubocop rules are disabled because this method is dynamically defined on the calling
38
+ # object making calls to other methods in this module impossible, or if done on methods
39
+ # in this module than instance variable belong to the module not the calling class
33
40
  define_method(name) do |*args, &block|
34
41
  if args.length.zero? && block.nil? == true
35
42
  instance_variable_get("@#{name}")
@@ -75,11 +82,15 @@ module DSLProperty
75
82
  end
76
83
  end
77
84
 
78
- if array_name
79
- define_method(array_name) do
80
- instance_variable_get("@#{array_name}")
81
- end
85
+ return unless array_name
86
+
87
+ define_method(array_name) do
88
+ instance_variable_get("@#{array_name}")
82
89
  end
83
90
  end
84
91
  end
92
+ # rubocop:enable Metrics/MethodLength
93
+ # rubocop:enable Metrics/AbcSize
94
+ # rubocop:enable Metrics/CyclomaticComplexity
95
+ # rubocop:enable Metrics/PerceivedComplexity
85
96
  end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+
5
+ module OpenHAB
6
+ module Core
7
+ module DSL
8
+ #
9
+ # Creates and manages OpenHAB Rules
10
+ #
11
+ module Rule
12
+ #
13
+ # JRuby extension to OpenHAB Rule
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 AutomationRule < Java::OrgOpenhabCoreAutomationModuleScriptRulesupportSharedSimple::SimpleRule
19
+ include Logging
20
+ include OpenHAB::Core::DSL::Tod
21
+ java_import java.time.ZonedDateTime
22
+
23
+ #
24
+ # Create a new Rule
25
+ #
26
+ # @param [Config] config Rule configuration
27
+ #
28
+ def initialize(config:)
29
+ super()
30
+ set_name(config.name)
31
+ set_description(config.description)
32
+ set_triggers(config.triggers)
33
+ @run_queue = config.run
34
+ @guard = config.guard
35
+ between = config.between&.yield_self { between(config.between) }
36
+ @between = between || OpenHAB::Core::DSL::Tod::ALL_DAY
37
+ # Convert between to correct range or nil if not set
38
+ @trigger_delays = config.trigger_delays
39
+ end
40
+
41
+ #
42
+ # Execute the rule
43
+ #
44
+ # @param [Map] mod map provided by OpenHAB rules engine
45
+ # @param [Map] inputs map provided by OpenHAB rules engine containing event and other information
46
+ #
47
+ #
48
+ def execute(mod = nil, inputs = nil)
49
+ logger.trace { "Execute called with mod (#{mod&.to_string}) and inputs (#{inputs&.pretty_inspect}" }
50
+ logger.trace { "Event details #{inputs['event'].pretty_inspect}" } if inputs&.key?('event')
51
+ if trigger_delay inputs
52
+ trigger_delay = trigger_delay(inputs)
53
+ process_trigger_delay(trigger_delay, mod, inputs)
54
+ else
55
+ # If guards are satisfied execute the run type blocks
56
+ # If they are not satisfied, execute the Othewise blocks
57
+ queue = create_queue(inputs)
58
+ process_queue(queue, mod, inputs)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ #
65
+ # Create the run queue based on guards
66
+ #
67
+ # @param [Map] inputs rule inputs
68
+ #
69
+ # @return [Queue] <description>
70
+ #
71
+ def create_queue(inputs)
72
+ case check_guards(event: inputs&.dig('event'))
73
+ when true
74
+ @run_queue.dup
75
+ when false
76
+ @run_queue.dup.grep(RuleConfig::Otherwise)
77
+ end
78
+ end
79
+
80
+ #
81
+ # Returns trigger delay from inputs if it exists
82
+ #
83
+ # @param [Map] inputs map from OpenHAB containing UID
84
+ #
85
+ # @return [Array] Array of trigger delays that match rule UID
86
+ #
87
+ def trigger_delay(inputs)
88
+ # Parse this to get the trigger UID:
89
+ # ["72698819-83cb-498a-8e61-5aab8b812623.event", "oldState", "module", \
90
+ # "72698819-83cb-498a-8e61-5aab8b812623.oldState", "event", "newState",\
91
+ # "72698819-83cb-498a-8e61-5aab8b812623.newState"]
92
+ @trigger_delays[inputs&.keys&.grep(/\.event$/)&.first&.chomp('.event')]
93
+ end
94
+
95
+ #
96
+ # Check if trigger guards prevent rule execution
97
+ #
98
+ # @param [Delay] trigger_delay rules delaying trigger because of
99
+ # @param [Map] inputs OpenHAB map object describing rule trigger
100
+ #
101
+ # @return [Boolean] True if the rule should execute, false if trigger guard prevents execution
102
+ #
103
+ def check_trigger_guards(trigger_delay, inputs)
104
+ old_state = inputs['oldState']
105
+ new_state = inputs['newState']
106
+ if check_from(trigger_delay, old_state)
107
+ return true if check_to(trigger_delay, new_state)
108
+
109
+ logger.trace("Skipped execution of rule '#{name}' because to state #{new_state}"\
110
+ " does not equal specified state(#{trigger_delay.to})")
111
+ else
112
+ logger.trace("Skipped execution of rule '#{name}' because old state #{old_state}"\
113
+ " does not equal specified state(#{trigger_delay.from})")
114
+ end
115
+ end
116
+
117
+ #
118
+ # Check the from state against the trigger delay
119
+ #
120
+ # @param [TriggerDelay] trigger_delay Information about the trigger delay
121
+ # @param [Item State] state from state to check
122
+ #
123
+ # @return [Boolean] true if no from state is defined or defined state equals supplied state
124
+ #
125
+ def check_from(trigger_delay, state)
126
+ trigger_delay.from.nil? || trigger_delay.from == state
127
+ end
128
+
129
+ #
130
+ # Check the to state against the trigger delay
131
+ #
132
+ # @param [TriggerDelay] trigger_delay Information about the trigger delay
133
+ # @param [Item State] state to-state to check
134
+ #
135
+ # @return [Boolean] true if no to state is defined or defined state equals supplied state
136
+ #
137
+ def check_to(trigger_delay, state)
138
+ trigger_delay.to.nil? || trigger_delay.to == state
139
+ end
140
+
141
+ #
142
+ # Process any matching trigger delays
143
+ #
144
+ # @param [Map] mod OpenHAB map object describing rule trigger
145
+ # @param [Map] inputs OpenHAB map object describing rule trigger
146
+ #
147
+ #
148
+ def process_trigger_delay(trigger_delay, mod, inputs)
149
+ if check_trigger_guards(trigger_delay, inputs)
150
+ logger.trace("Trigger Guards Matched for #{trigger_delay}, delaying rule execution")
151
+ # Add timer and attach timer to delay object, and also state being tracked to so timer can be cancelled if
152
+ # state changes
153
+ # Also another timer should not be created if changed to same value again but instead rescheduled
154
+ if trigger_delay.timer_active?
155
+ process_active_timer(inputs, mod, trigger_delay)
156
+ else
157
+ create_trigger_delay_timer(inputs, mod, trigger_delay)
158
+ end
159
+ else
160
+ logger.trace("Trigger Guards did not match for #{trigger_delay}, ignoring trigger.")
161
+ end
162
+ end
163
+
164
+ #
165
+ # Creatas a timer for trigger delays
166
+ #
167
+ # @param [Hash] inputs rule trigger inputs
168
+ # @param [Hash] mod rule trigger mods
169
+ # @param [TriggerDelay] trigger_delay specifications
170
+ #
171
+ #
172
+ def create_trigger_delay_timer(inputs, mod, trigger_delay)
173
+ logger.trace("Creating timer for rule #{name} and trigger delay #{trigger_delay}")
174
+ trigger_delay.timer = after(trigger_delay.duration) do
175
+ logger.trace("Delay Complete for #{trigger_delay}, executing rule")
176
+ trigger_delay.timer = nil
177
+ process_queue(@run_queue.dup, mod, inputs)
178
+ end
179
+ trigger_delay.tracking_to = inputs['newState']
180
+ end
181
+
182
+ #
183
+ # Process an active trigger timer
184
+ #
185
+ # @param [Hash] inputs rule trigger inputs
186
+ # @param [Hash] mod rule trigger mods
187
+ # @param [TriggerDelay] trigger_delay specifications
188
+ #
189
+ #
190
+ def process_active_timer(inputs, mod, trigger_delay)
191
+ state = inputs['newState']
192
+ if state == trigger_delay.tracking_to
193
+ logger.trace("Item changed to #{state} for #{trigger_delay}, rescheduling timer.")
194
+ trigger_delay.timer.reschedule(ZonedDateTime.now.plus(duration))
195
+ else
196
+ logger.trace("Item changed to #{state} for #{trigger_delay}, cancelling timer.")
197
+ trigger_delay.timer.cancel
198
+ # Reprocess trigger delay after cancelling to track new state (if guards matched, etc)
199
+ process_trigger_delay(trigger_delay, mod, inputs)
200
+ end
201
+ end
202
+
203
+ #
204
+ # Check if any guards prevent execution
205
+ #
206
+ # @param [Map] event OpenHAB rule trigger event
207
+ #
208
+ # @return [Boolean] True if guards says rule should execute, false otherwise
209
+ #
210
+ def check_guards(event:)
211
+ if @guard.should_run? event
212
+ now = TimeOfDay.now
213
+ return true if @between.cover? now
214
+
215
+ logger.trace("Skipped execution of rule '#{name}' because the current time #{now} "\
216
+ "is not between #{@between.begin} and #{@between.end}")
217
+ else
218
+ logger.trace("Skipped execution of rule '#{name}' because of guard #{@guard}")
219
+ end
220
+ false
221
+ end
222
+
223
+ #
224
+ # Patch event to include event.item when it doesn't exist
225
+ # This is to patch a bug see https://github.com/boc-tothefuture/openhab-jruby/issues/75
226
+ # It may be fixed in the openhab core in the future, in which case, this patch will no longer be necessary
227
+ #
228
+ # @param [OpenHAB Event] event to check for item accessor
229
+ # @param [OpenHAB Event Inputs] inputs inputs to running rule
230
+ #
231
+ def add_event_item(event, inputs)
232
+ return if event.nil? || defined?(event.item)
233
+
234
+ class << event
235
+ attr_accessor :item
236
+ end
237
+ event.item = inputs&.dig('triggeringItem')
238
+ end
239
+
240
+ #
241
+ # Process the run queue
242
+ #
243
+ # @param [Array] run_queue array of procs of various types to execute
244
+ # @param [Map] mod OpenHAB map object describing rule trigger
245
+ # @param [Map] inputs OpenHAB map object describing rule trigge
246
+ #
247
+ #
248
+ def process_queue(run_queue, mod, inputs)
249
+ event = inputs&.dig('event')
250
+
251
+ while (task = run_queue.shift)
252
+ case task
253
+ when RuleConfig::Run then process_run_task(event, inputs, task)
254
+ when RuleConfig::Trigger then process_trigger_task(event, task)
255
+ when RuleConfig::Delay then process_delay_task(inputs, mod, run_queue, task)
256
+ when RuleConfig::Otherwise then process_otherwise_task(event, task)
257
+ end
258
+ end
259
+ end
260
+
261
+ #
262
+ # Process an otherwise block
263
+ #
264
+ # @param [OpenHab Event] event that triggered the rule
265
+ # @param [Task] task task containing otherwise block to execute
266
+ #
267
+ #
268
+ def process_otherwise_task(event, task)
269
+ logger.trace { "Executing rule '#{name}' otherwise block with event(#{event})" }
270
+ task.block.call(event)
271
+ end
272
+
273
+ #
274
+ # Process delay task
275
+ #
276
+ # @param [Map] inputs Rule trigger inputs
277
+ # @param [Map] mod Rule modes
278
+ # @param [Queue] run_queue Queue of tasks for this rule
279
+ # @param [Delay] task to process
280
+ #
281
+ #
282
+ def process_delay_task(inputs, mod, run_queue, task)
283
+ remaining_queue = run_queue.slice!(0, run_queue.length)
284
+ after(task.duration) { process_queue(remaining_queue, mod, inputs) }
285
+ end
286
+
287
+ #
288
+ # Process a task that is caused by a group item
289
+ #
290
+ # @param [Map] event Rule event map
291
+ # @param [Trigger] task to execute
292
+ #
293
+ #
294
+ def process_trigger_task(event, task)
295
+ # rubocop: disable Style/GlobalVars
296
+ triggering_item = $ir.get(event&.itemName)
297
+ # rubocop: enable Style/GlobalVars
298
+ logger.trace { "Executing rule '#{name}' trigger block with item (#{triggering_item})" }
299
+ task.block.call(triggering_item) if triggering_item
300
+ end
301
+
302
+ #
303
+ # Process a run task
304
+ #
305
+ # @param [OpenHab Event] event information
306
+ # @param [Map] inputs of rule trigger information
307
+ # @param [Run] task to execute
308
+ #
309
+ #
310
+ def process_run_task(event, inputs, task)
311
+ add_event_item(event, inputs)
312
+ logger.trace { "Executing rule '#{name}' run block with event(#{event})" }
313
+ task.block.call(event)
314
+ end
315
+
316
+ #
317
+ # Create a new hash in which all elements are converted to strings
318
+ #
319
+ # @param [Map] hash in which all elements should be converted to strings
320
+ #
321
+ # @return [Map] new map with values converted to strings
322
+ #
323
+ def inspect_hash(hash)
324
+ hash.each_with_object({}) do |(key, value), new_hash|
325
+ new_hash[inspect_item(key)] = inspect_item(value)
326
+ end
327
+ end
328
+
329
+ #
330
+ # Convert an individual element into a string based on if it a Ruby or Java object
331
+ #
332
+ # @param [Object] item to convert to a string
333
+ #
334
+ # @return [String] representation of item
335
+ #
336
+ def inspect_item(item)
337
+ if item.respond_to? :to_string
338
+ item.to_string
339
+ elsif item.respond_to? :to_str
340
+ item.to_str
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
348
+ # rubocop: enable Metrics/ClassLength