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
@@ -82,15 +82,52 @@ module OpenHAB
82
82
 
83
83
  items.each { |item| logger.trace("#{item} truthy? #{item.truthy?}") }
84
84
 
85
+ process_check(check_type: check_type, event: event, items: items, procs: procs)
86
+ end
87
+
88
+ #
89
+ # Execute the guard check
90
+ #
91
+ # @param [Symbol] check_type :only_if or :not_if to check
92
+ # @param [OpenHAB Event] event event to check if meets guard
93
+ # @param [Array<Item>] items to check if satisfy criteria
94
+ # @param [Array] procs to check if satisfy criteria
95
+ #
96
+ # @return [Boolean] True if criteria are satisfied, false otherwise
97
+ #
98
+ def process_check(check_type:, event:, items:, procs:)
85
99
  case check_type
86
- when :only_if
87
- items.all?(&:truthy?) && procs.all? { |proc| proc.call(event) }
88
- when :not_if
89
- items.none?(&:truthy?) && procs.none? { |proc| proc.call(event) }
90
- else
91
- raise ArgumentError, "Unexpected check type: #{check_type}"
100
+ when :only_if then process_only_if(event, items, procs)
101
+ when :not_if then process_not_if(event, items, procs)
102
+ else raise ArgumentError, "Unexpected check type: #{check_type}"
92
103
  end
93
104
  end
105
+
106
+ #
107
+ # Check not_if guard
108
+ #
109
+ # @param [OpenHAB Event] event event to check if meets guard
110
+ # @param [Array<Item>] items to check if satisfy criteria
111
+ # @param [Array] procs to check if satisfy criteria
112
+ #
113
+ # @return [Boolean] True if criteria are satisfied, false otherwise
114
+ #
115
+ def process_not_if(event, items, procs)
116
+ items.none?(&:truthy?) && procs.none? { |proc| proc.call(event) }
117
+ end
118
+
119
+ #
120
+ # Check only_if guard
121
+ #
122
+ # @param [OpenHAB Event] event event to check if meets guard
123
+ # @param [Array<Item>] items to check if satisfy criteria
124
+ # @param [Array] procs to check if satisfy criteria
125
+ #
126
+ # @return [Boolean] True if criteria are satisfied, false otherwise
127
+ #
128
+ def process_only_if(event, items, procs)
129
+ items.all?(&:truthy?) && procs.all? { |proc| proc.call(event) }
130
+ end
94
131
  end
95
132
  end
96
133
  end
@@ -1,402 +1,115 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'java'
4
- require 'pp'
5
- require 'core/dsl/property'
6
- require 'core/dsl/rule/cron'
7
- require 'core/dsl/rule/triggers'
8
- require 'core/dsl/rule/item'
9
- require 'core/dsl/rule/channel'
3
+ require 'core/dsl/rule/rule_config'
4
+ require 'core/dsl/rule/automation_rule'
10
5
  require 'core/dsl/rule/guard'
11
- require 'core/dsl/entities'
12
- require 'core/dsl/time_of_day'
13
- require 'core/dsl'
14
- require 'core/dsl/timers'
15
6
 
16
7
  module OpenHAB
17
8
  module Core
9
+ #
10
+ # Contains code to create an OpenHAB DSL
11
+ #
18
12
  module DSL
19
13
  #
20
14
  # Creates and manages OpenHAB Rules
21
15
  #
22
16
  module Rule
23
17
  #
24
- # Rule configuration for OpenHAB Rules engine
18
+ # Create a new rule
25
19
  #
26
- class RuleConfig
27
- include EntityLookup
28
- include OpenHAB::Core::DSL::Rule::Cron
29
- include Guard
30
- include Item
31
- include Channel
32
- include DSLProperty
33
- include Logging
34
- extend OpenHAB::Core::DSL
35
-
36
- java_import org.openhab.core.library.items.SwitchItem
37
-
38
- # @return [Array] Of triggers
39
- attr_reader :triggers
40
-
41
- # @return [Array] Of trigger delays
42
- attr_reader :trigger_delays
43
-
44
- #
45
- # Struct holding a run block
46
- #
47
- Run = Struct.new(:block)
48
-
49
- #
50
- # Struct holding a Triggered block
51
- #
52
- Trigger = Struct.new(:block)
53
-
54
- #
55
- # Struct holding an otherwise block
56
- #
57
- Otherwise = Struct.new(:block)
58
-
59
- #
60
- # Struct holding rule delays
61
- #
62
- Delay = Struct.new(:duration)
63
-
64
- prop_array :run, array_name: :run_queue, wrapper: Run
65
- prop_array :triggered, array_name: :run_queue, wrapper: Trigger
66
- prop_array :delay, array_name: :run_queue, wrapper: Delay
67
- prop_array :otherwise, array_name: :run_queue, wrapper: Otherwise
68
-
69
- prop :name
70
- prop :description
71
- prop :enabled
72
- prop :between
73
-
74
- #
75
- # Create a new RuleConfig
76
- #
77
- # @param [Object] caller_binding The object initializing this configuration.
78
- # Used to execute within the object's context
79
- #
80
- def initialize(caller_binding)
81
- @triggers = []
82
- @trigger_delays = {}
83
- @enabled = true
84
- @on_start = false
85
- @caller = caller_binding.eval 'self'
86
- end
20
+ # @param [String] rule_name <description>
21
+ # @yield [] Block executed in context of a RuleConfig
22
+ #
23
+ #
24
+ def rule(rule_name, &block)
25
+ config = RuleConfig.new(rule_name, block.binding)
26
+ config.instance_eval(&block)
27
+ config.guard = Guard::Guard.new(only_if: config.only_if, not_if: config.not_if)
28
+ logger.trace { config.inspect }
29
+ process_rule_config(config)
30
+ end
87
31
 
88
- #
89
- # Start this rule on system startup
90
- #
91
- # @param [Boolean] run_on_start Run this rule on start, defaults to True
92
- #
93
- #
94
- def on_start(run_on_start = true)
95
- @on_start = run_on_start
32
+ #
33
+ # Create a logger where name includes rule name if name is set
34
+ #
35
+ # @return [Logging::Logger] Logger with name that appended with rule name if rule name is set
36
+ #
37
+ def logger
38
+ if name
39
+ Logging.logger(name.chomp.gsub(/\s+/, '_'))
40
+ else
41
+ super
96
42
  end
43
+ end
97
44
 
98
- #
99
- # Checks if this rule should run on start
100
- #
101
- # @return [Boolean] True if rule should run on start, false otherwise.
102
- #
103
- def on_start?
104
- @on_start
105
- end
45
+ private
106
46
 
107
- #
108
- # Run the supplied block inside the object instance of the object that created the rule config
109
- #
110
- # @yield [] Block executed in context of the object creating the rule config
111
- #
112
- #
113
- def my(&block)
114
- @caller.instance_eval(&block)
115
- end
47
+ #
48
+ # Process a rule based on the supplied configuration
49
+ #
50
+ # @param [RuleConfig] config for rule
51
+ #
52
+ #
53
+ def process_rule_config(config)
54
+ return unless create_rule?(config)
116
55
 
117
- #
118
- # Create a logger where name includes rule name if name is set
119
- #
120
- # @return [Logging::Logger] Logger with name that appended with rule name if rule name is set
121
- #
122
- def logger
123
- if name
124
- Logging.logger(name.chomp.gsub(/\s+/, '_'))
125
- else
126
- super
127
- end
128
- end
56
+ rule = AutomationRule.new(config: config)
57
+ add_rule(rule)
58
+ rule.execute if config.on_start?
129
59
  end
130
60
 
131
61
  #
132
- # Create a new rule
62
+ # Should a rule be created based on rule configuration
133
63
  #
134
- # @param [String] name <description>
135
- # @yield [] Block executed in context of a RuleConfic
64
+ # @param [RuleConfig] config to check
136
65
  #
66
+ # @return [Boolean] true if it should be created, false otherwise
137
67
  #
138
- def rule(name, &block)
139
- config = RuleConfig.new(block.binding)
140
- config.name(name)
141
- config.instance_eval(&block)
142
- return if !config.on_start? && config.triggers.empty?
143
-
144
- guard = Guard::Guard.new(only_if: config.only_if, not_if: config.not_if)
145
-
146
- logger.trace do
147
- "Triggers: #{config.triggers} Guard: #{guard} Runs: #{config.run} on_start: #{config.on_start?}"
148
- end
149
- config.triggers.each { |trigger| logger.trace { "Trigger UID: #{trigger.id}" } }
150
- logger.trace { "Trigger Waits #{config.trigger_delays}" }
151
-
152
- if config.enabled
153
- # Convert between to correct range or nil if not set
154
- between = config.between&.yield_self { between(config.between) }
155
-
156
- rule = Rule.new(name: config.name, description: config.description, run_queue: config.run_queue,
157
- guard: guard, between: between, trigger_delays: config.trigger_delays)
158
-
159
- rule.set_triggers(config.triggers)
160
- am = $scriptExtension.get('automationManager')
161
- am.addRule(rule)
162
- rule.execute(nil, nil) if config.on_start?
68
+ def create_rule?(config)
69
+ if !triggers?(config)
70
+ logger.warn "Rule '#{config.name}' has no triggers, not creating rule"
71
+ elsif !execution_blocks?(config)
72
+ logger.warn "Rule '#{config.name}' has no execution blocks, not creating rule"
73
+ elsif !config.enabled
74
+ logger.debug "Rule '#{config.name}' marked as disabled, not creating rule."
163
75
  else
164
- logger.debug "#{name} marked as disabled, not creating rule."
76
+ return true
165
77
  end
78
+ false
166
79
  end
167
80
 
168
81
  #
169
- # JRuby extension to OpenHAB Rule
82
+ # Check if the rule has any triggers
170
83
  #
171
- class Rule < Java::OrgOpenhabCoreAutomationModuleScriptRulesupportSharedSimple::SimpleRule
172
- include Logging
173
- include OpenHAB::Core::DSL::Tod
174
- java_import java.time.ZonedDateTime
175
-
176
- #
177
- # Create a new Rule
178
- #
179
- # @param [String] name Name of the rule
180
- # @param [String] description of the rule
181
- # @param [Array] run_queue array of procs to execute for rule
182
- # @param [Array] guard array of guards
183
- # @param [Range] between range in which the rule will execute
184
- # @param [Array] trigger_delays Array of delays for tiggers based on item config
185
- #
186
- def initialize(name:, description:, run_queue:, guard:, between:, trigger_delays:)
187
- super()
188
- setName(name)
189
- setDescription(description)
190
- @run_queue = run_queue
191
- @guard = guard
192
- @between = between || OpenHAB::Core::DSL::Tod::ALL_DAY
193
- @trigger_delays = trigger_delays
194
- end
195
-
196
- #
197
- # Execute the rule
198
- #
199
- # @param [Map] mod map provided by OpenHAB rules engine
200
- # @param [Map] inputs map provided by OpenHAB rules engine containing event and other information
201
- #
202
- #
203
- def execute(mod, inputs)
204
- logger.trace { "Execute called with mod (#{mod&.to_string}) and inputs (#{inputs&.pretty_inspect}" }
205
- logger.trace { "Event details #{inputs['event'].pretty_inspect}" } if inputs&.key?('event')
206
- if trigger_delay inputs
207
- process_trigger_delay(mod, inputs)
208
- else
209
- # If guards are satisfied execute the run type blocks
210
- # If they are not satisfied, execute the Othewise blocks
211
- queue = case check_guards(event: inputs&.dig('event'))
212
- when true
213
- @run_queue.dup
214
- when false
215
- @run_queue.dup.grep(RuleConfig::Otherwise)
216
- end
217
- process_queue(queue, mod, inputs)
218
- end
219
- end
220
-
221
- private
222
-
223
- #
224
- # Returns trigger delay from inputs if it exists
225
- #
226
- # @param [Map] inputs map from OpenHAB containing UID
227
- #
228
- # @return [Array] Array of trigger delays that match rule UID
229
- #
230
- def trigger_delay(inputs)
231
- # Parse this to get the trigger UID:
232
- # ["72698819-83cb-498a-8e61-5aab8b812623.event", "oldState", "module", "72698819-83cb-498a-8e61-5aab8b812623.oldState", "event", "newState", "72698819-83cb-498a-8e61-5aab8b812623.newState"
233
- @trigger_delays[inputs&.keys&.grep(/\.event$/)&.first&.chomp('.event')]
234
- end
235
-
236
- #
237
- # Check if trigger guards prevent rule execution
238
- #
239
- # @param [Delay] trigger_delay rules delaying trigger because of
240
- # @param [Map] inputs map from OpenHAB describing the rle trigger
241
- #
242
- # @return [Boolean] True if the rule should execute, false if trigger guard prevents execution
243
- #
244
- def check_trigger_guards(trigger_delay, inputs)
245
- old_state = inputs['oldState']
246
- new_state = inputs['newState']
247
- if trigger_delay.from.nil? || trigger_delay.from == old_state
248
- if trigger_delay.to.nil? || trigger_delay.to == new_state
249
- return true
250
- else
251
- logger.trace("Skipped execution of rule '#{name}' because to state #{new_state} does not equal specified state(#{trigger_delay.to})")
252
- end
253
- else
254
- logger.trace("Skipped execution of rule '#{name}' because old state #{old_state} does not equal specified state(#{trigger_delay.from})")
255
- end
256
- false
257
- end
258
-
259
- #
260
- # Process any matching trigger delays
261
- #
262
- # @param [Map] mod OpenHAB map object describing rule trigger
263
- # @param [Map] inputs OpenHAB map object describing rule trigge
264
- #
265
- #
266
- def process_trigger_delay(mod, inputs)
267
- trigger_delay = trigger_delay(inputs)
268
- if check_trigger_guards(trigger_delay, inputs)
269
- logger.trace("Trigger Guards Matched for #{trigger_delay}, delaying rule execution")
270
- # Add timer and attach timer to delay object, and also state being tracked to so timer can be cancelled if state changes
271
- # Also another timer should not be created if changed to same value again but instead rescheduled
272
- if trigger_delay.timer.nil? || trigger_delay.timer.is_active == false
273
- logger.trace("Creating timer for rule #{name} and trigger delay #{trigger_delay}")
274
- trigger_delay.timer = after(trigger_delay.duration) do
275
- logger.trace("Delay Complete for #{trigger_delay}, executing rule")
276
- trigger_delay.timer = nil
277
- process_queue(@run_queue.dup, mod, inputs)
278
- end
279
- trigger_delay.tracking_to = inputs['newState']
280
- else
281
- # Timer active
282
- state = inputs['newState']
283
- if state != trigger_delay.tracking_to
284
- logger.trace("Item changed to #{state} for #{trigger_delay}, cancelling timer.")
285
- trigger_delay.timer.cancel
286
- # Reprocess trigger delay after cancelling to track new state (if guards matched, etc)
287
- process_trigger_delay(mod, inputs)
288
- else
289
- logger.trace("Item changed to #{state} for #{trigger_delay}, rescheduling timer.")
290
- trigger_delay.timer.reschedule(ZonedDateTime.now.plus(duration))
291
- end
292
- end
293
- else
294
- logger.trace("Trigger Guards did not match for #{trigger_delay}, ignoring trigger.")
295
- end
296
- end
297
-
298
- #
299
- # Check if any guards prevent execution
300
- #
301
- # @param [Map] event OpenHAB rule trigger event
302
- #
303
- # @return [Boolean] True if guards says rule should execute, false otherwise
304
- #
305
- def check_guards(event:)
306
- if @guard.should_run? event
307
- now = TimeOfDay.now
308
- if @between.cover? now
309
- return true
310
- else
311
- logger.trace("Skipped execution of rule '#{name}' because the current time #{now} is not between #{@between.begin} and #{@between.end}")
312
- end
313
- else
314
- logger.trace("Skipped execution of rule '#{name}' because of guard #{@guard}")
315
- end
316
- false
317
- end
318
-
319
- #
320
- # Patch event to include event.item when it doesn't exist
321
- # This is to patch a bug see https://github.com/boc-tothefuture/openhab-jruby/issues/75
322
- # It may be fixed in the openhab core in the future, in which case, this patch will no longer be necessary
323
- #
324
- # @param [OpenHAB Event] event to check for item accessor
325
- # @param [OpenHAB Event Inputs] inputs inputs to running rule
326
- #
327
- def add_event_item(event, inputs)
328
- return if event.nil? || defined?(event.item)
329
-
330
- class << event
331
- attr_accessor :item
332
- end
333
- event.item = inputs&.dig('triggeringItem')
334
- end
335
-
336
- #
337
- # Process the run queue
338
- #
339
- # @param [Array] run_queue array of procs of various types to execute
340
- # @param [Map] mod OpenHAB map object describing rule trigger
341
- # @param [Map] inputs OpenHAB map object describing rule trigge
342
- #
343
- #
344
- def process_queue(run_queue, mod, inputs)
345
- while (task = run_queue.shift)
346
- case task
347
- when RuleConfig::Run
348
-
349
- event = inputs&.dig('event')
350
- add_event_item(event, inputs)
351
- logger.trace { "Executing rule '#{name}' run block with event(#{event})" }
352
- task.block.call(event)
353
- when RuleConfig::Trigger
354
-
355
- triggering_item = $ir.get(inputs&.dig('event')&.itemName)
356
-
357
- logger.trace { "Executing rule '#{name}' trigger block with item (#{triggering_item})" }
358
- task.block.call(triggering_item) if triggering_item
359
-
360
- when RuleConfig::Delay
361
- remaining_queue = run_queue.slice!(0, run_queue.length)
362
- after(task.duration) { process_queue(remaining_queue, mod, inputs) }
363
-
364
- when RuleConfig::Otherwise
365
- event = inputs&.dig('event')
366
- logger.trace { "Executing rule '#{name}' otherwise block with event(#{event})" }
367
- task.block.call(event)
368
-
369
- end
370
- end
371
- end
84
+ # @param [RuleConfig] config to check for triggers
85
+ #
86
+ # @return [Boolean] True if rule has triggers, false otherwise
87
+ #
88
+ def triggers?(config)
89
+ config.on_start? || config.triggers.length.positive?
90
+ end
372
91
 
373
- #
374
- # Create a new hash in which all elements are converted to strings
375
- #
376
- # @param [Map] hash in which all elements should be converted to strings
377
- #
378
- # @return [Map] new map with values converted to strings
379
- #
380
- def inspect_hash(hash)
381
- hash.each_with_object({}) do |(key, value), new_hash|
382
- new_hash[inspect_item(key)] = inspect_item(value)
383
- end
384
- end
92
+ #
93
+ # Check if the rule has any execution blocks
94
+ #
95
+ # @param [RuleConfig] config to check for triggers
96
+ #
97
+ # @return [Boolean] True if rule has execution blocks, false otherwise
98
+ #
99
+ def execution_blocks?(config)
100
+ (config.run || []).length.positive?
101
+ end
385
102
 
386
- #
387
- # Convert an individual element into a string based on if it a Ruby or Java object
388
- #
389
- # @param [Object] item to convert to a string
390
- #
391
- # @return [String] representation of item
392
- #
393
- def inspect_item(item)
394
- if item.respond_to? :to_string
395
- item.to_string
396
- elsif item.respond_to? :to_str
397
- item.to_str
398
- end
399
- end
103
+ #
104
+ # Add a rule to the automation managed
105
+ #
106
+ # @param [Java::OrgOpenhabCoreAutomationModuleScriptRulesupportSharedSimple::SimpleRule] rule to add
107
+ #
108
+ #
109
+ def add_rule(rule)
110
+ # rubocop: disable Style/GlobalVars
111
+ $scriptExtension.get('automationManager').addRule(rule)
112
+ # rubocop: enable Style/GlobalVars
400
113
  end
401
114
  end
402
115
  end