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