openhab-scripting 2.13.1 → 2.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) 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 +151 -50
  10. data/lib/openhab/core/dsl/items/string_item.rb +21 -3
  11. data/lib/openhab/core/dsl/monkey_patch/items/items.rb +2 -0
  12. data/lib/openhab/core/dsl/monkey_patch/items/metadata.rb +66 -42
  13. data/lib/openhab/core/dsl/monkey_patch/items/persistence.rb +72 -0
  14. data/lib/openhab/core/dsl/monkey_patch/items/switch_item.rb +2 -1
  15. data/lib/openhab/core/dsl/monkey_patch/ruby/range.rb +2 -1
  16. data/lib/openhab/core/dsl/monkey_patch/ruby/ruby.rb +1 -0
  17. data/lib/openhab/core/dsl/persistence.rb +27 -0
  18. data/lib/openhab/core/dsl/property.rb +15 -4
  19. data/lib/openhab/core/dsl/rule/automation_rule.rb +348 -0
  20. data/lib/openhab/core/dsl/rule/guard.rb +43 -6
  21. data/lib/openhab/core/dsl/rule/rule.rb +80 -354
  22. data/lib/openhab/core/dsl/rule/rule_config.rb +153 -0
  23. data/lib/openhab/core/dsl/rule/triggers/changed.rb +145 -0
  24. data/lib/openhab/core/dsl/rule/{channel.rb → triggers/channel.rb} +22 -8
  25. data/lib/openhab/core/dsl/rule/triggers/command.rb +106 -0
  26. data/lib/openhab/core/dsl/rule/{cron.rb → triggers/cron.rb} +36 -14
  27. data/lib/openhab/core/dsl/rule/triggers/trigger.rb +126 -0
  28. data/lib/openhab/core/dsl/rule/triggers/updated.rb +100 -0
  29. data/lib/openhab/core/dsl/time_of_day.rb +50 -24
  30. data/lib/openhab/core/dsl/timers.rb +2 -6
  31. data/lib/openhab/core/dsl/types/quantity.rb +106 -69
  32. data/lib/openhab/core/log.rb +3 -8
  33. data/lib/openhab/core/startup_delay.rb +1 -0
  34. data/lib/openhab/osgi.rb +7 -0
  35. data/lib/openhab/version.rb +1 -1
  36. metadata +12 -6
  37. data/lib/openhab/core/dsl/rule/item.rb +0 -208
  38. 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,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(duration))
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