openhab-scripting 2.14.1 → 2.16.1

Sign up to get free protection for your applications and to get access to all the features.
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 +1 -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 +53 -25
  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
@@ -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