openhab-scripting 2.14.1 → 2.14.2

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab.rb +3 -0
  3. data/lib/openhab/core/dsl/actions.rb +1 -1
  4. data/lib/openhab/core/dsl/entities.rb +41 -4
  5. data/lib/openhab/core/dsl/gems.rb +1 -1
  6. data/lib/openhab/core/dsl/group.rb +3 -1
  7. data/lib/openhab/core/dsl/items/items.rb +3 -1
  8. data/lib/openhab/core/dsl/items/number_item.rb +151 -50
  9. data/lib/openhab/core/dsl/items/string_item.rb +21 -3
  10. data/lib/openhab/core/dsl/monkey_patch/items/metadata.rb +66 -42
  11. data/lib/openhab/core/dsl/monkey_patch/items/switch_item.rb +2 -1
  12. data/lib/openhab/core/dsl/monkey_patch/ruby/range.rb +2 -1
  13. data/lib/openhab/core/dsl/property.rb +15 -4
  14. data/lib/openhab/core/dsl/rule/automation_rule.rb +348 -0
  15. data/lib/openhab/core/dsl/rule/guard.rb +43 -6
  16. data/lib/openhab/core/dsl/rule/rule.rb +80 -367
  17. data/lib/openhab/core/dsl/rule/rule_config.rb +153 -0
  18. data/lib/openhab/core/dsl/rule/triggers/changed.rb +145 -0
  19. data/lib/openhab/core/dsl/rule/{channel.rb → triggers/channel.rb} +22 -8
  20. data/lib/openhab/core/dsl/rule/triggers/command.rb +106 -0
  21. data/lib/openhab/core/dsl/rule/{cron.rb → triggers/cron.rb} +36 -14
  22. data/lib/openhab/core/dsl/rule/triggers/trigger.rb +126 -0
  23. data/lib/openhab/core/dsl/rule/triggers/updated.rb +100 -0
  24. data/lib/openhab/core/dsl/time_of_day.rb +50 -24
  25. data/lib/openhab/core/dsl/timers.rb +2 -6
  26. data/lib/openhab/core/dsl/types/quantity.rb +106 -69
  27. data/lib/openhab/core/log.rb +3 -8
  28. data/lib/openhab/core/startup_delay.rb +1 -0
  29. data/lib/openhab/osgi.rb +7 -0
  30. data/lib/openhab/version.rb +1 -1
  31. metadata +10 -6
  32. data/lib/openhab/core/dsl/rule/item.rb +0 -203
  33. data/lib/openhab/core/dsl/rule/triggers.rb +0 -77
@@ -123,25 +123,7 @@ module OpenHAB
123
123
  # @return [OpenHAB::Core::DSL::MonkeyPatch::Items::MetadataItem]
124
124
  #
125
125
  def []=(namespace, value)
126
- case value
127
- when MetadataItem
128
- meta_value = value.value
129
- configuration = value.__getobj__
130
- when Metadata
131
- meta_value = value.value
132
- configuration = value.configuration
133
- when Array
134
- raise ArgumentError, 'Array must contain 2 elements: value, config' if value.length < 2
135
-
136
- meta_value = value[0]
137
- configuration = value[1]
138
- when Hash
139
- meta_value = nil
140
- configuration = value
141
- else
142
- meta_value = value
143
- configuration = nil
144
- end
126
+ meta_value, configuration = update_from_value(value)
145
127
 
146
128
  key = MetadataKey.new(namespace, @item_name)
147
129
  metadata = Metadata.new(key, meta_value, configuration)
@@ -185,12 +167,12 @@ module OpenHAB
185
167
  #
186
168
  # @return [Boolean] True if the given namespace exists, false otherwise
187
169
  #
188
- def has_key?(namespace)
170
+ def key?(namespace)
189
171
  !NamespaceAccessor.registry.get(MetadataKey.new(namespace, @item_name)).nil?
190
172
  end
191
173
 
192
- alias key? has_key?
193
- alias include? has_key?
174
+ alias has_key? key?
175
+ alias include? key?
194
176
 
195
177
  #
196
178
  # Merge the given hash with the current metadata. Existing namespace that matches the name
@@ -201,25 +183,9 @@ module OpenHAB
201
183
 
202
184
  others.each do |other|
203
185
  case other
204
- when Hash
205
- other.each do |key, new_meta|
206
- if block_given?
207
- current_meta = self[key]&.to_a
208
- new_meta = yield key, current_meta, new_meta unless current_meta.nil?
209
- end
210
- self[key] = new_meta
211
- end
212
- when self.class
213
- other.each do |key, new_value, new_config|
214
- new_meta = new_value, new_config
215
- if block_given?
216
- current_meta = self[key]&.to_a
217
- new_meta = yield key, current_meta, new_meta unless current_meta.nil?
218
- end
219
- self[key] = new_meta
220
- end
221
- else
222
- raise ArgumentError, "merge only supports Hash, or another item's metadata"
186
+ when Hash then merge_hash!(other)
187
+ when self.class then merge_metadata!(other)
188
+ else raise ArgumentError, "merge only supports Hash, or another item's metadata"
223
189
  end
224
190
  end
225
191
  self
@@ -238,7 +204,65 @@ module OpenHAB
238
204
  # @return [Java::org::openhab::core::items::MetadataRegistry]
239
205
  #
240
206
  def self.registry
241
- @@registry ||= OpenHAB::OSGI.service('org.openhab.core.items.MetadataRegistry')
207
+ @registry ||= OpenHAB::OSGI.service('org.openhab.core.items.MetadataRegistry')
208
+ end
209
+
210
+ private
211
+
212
+ #
213
+ # perform an updated based on the supplied value
214
+ #
215
+ # @param [MetadataItem,Metadata,Array,Hash] value to perform update from
216
+ #
217
+ # @return [Array<Object,Hash>] Array containing the value and configuration based on the
218
+ # the supplied object
219
+ #
220
+ def update_from_value(value)
221
+ case value
222
+ when MetadataItem then [value.value, value.__getobj__]
223
+ when Metadata then [value.value, value.configuration]
224
+ when Array
225
+ raise ArgumentError, 'Array must contain 2 elements: value, config' if value.length != 2
226
+
227
+ value
228
+ when Hash then [nil, value]
229
+ else [value, nil]
230
+ end
231
+ end
232
+
233
+ #
234
+ # Merge the metadata from the supplied other metadata object
235
+ #
236
+ # @param [Hash] other metadata object to merge
237
+ # @yield [key, current_metadata, new_meta] to process merge
238
+ #
239
+ #
240
+ def merge_metadata!(other)
241
+ other.each do |key, new_value, new_config|
242
+ new_meta = new_value, new_config
243
+ if block_given?
244
+ current_meta = self[key]&.to_a
245
+ new_meta = yield key, current_meta, new_meta unless current_meta.nil?
246
+ end
247
+ self[key] = new_meta
248
+ end
249
+ end
250
+
251
+ #
252
+ # Merge a hash into the metadata
253
+ #
254
+ # @param [Hash] other to merge into metadata
255
+ # @yield [key, current_metadata, new_meta] to process merge
256
+ #
257
+ #
258
+ def merge_hash!(other)
259
+ other.each do |key, new_meta|
260
+ if block_given?
261
+ current_meta = self[key]&.to_a
262
+ new_meta = yield key, current_meta, new_meta unless current_meta.nil?
263
+ end
264
+ self[key] = new_meta
265
+ end
242
266
  end
243
267
  end
244
268
 
@@ -74,7 +74,8 @@ class Java::OrgOpenhabCoreLibraryItems::SwitchItem
74
74
  #
75
75
  # @param [Object] other object to compare to
76
76
  #
77
- # @return [Boolean] True if other is a OnOffType and other equals state for this switch item, otherwise result from super
77
+ # @return [Boolean] True if other is a OnOffType and other equals state for this switch item,
78
+ # otherwise result from super
78
79
  #
79
80
  def ==(other)
80
81
  if other.is_a? OnOffType
@@ -24,7 +24,8 @@ module OpenHAB
24
24
  #
25
25
  # @param [Object] other object to compare for case equals
26
26
  #
27
- # @return [Boolean] if other is DimmerItem and state is covered by range, result from parent Range class if not DimmerItem
27
+ # @return [Boolean] if other is DimmerItem and state is covered by range,
28
+ # result from parent Range class if not DimmerItem
28
29
  #
29
30
  def ===(other)
30
31
  return super unless other.is_a? DimmerItem
@@ -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