openhab-jrubyscripting 5.0.0.rc9 → 5.0.0.rc11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab/core/actions/audio.rb +47 -0
  3. data/lib/openhab/core/actions/ephemeris.rb +39 -0
  4. data/lib/openhab/core/actions/exec.rb +51 -0
  5. data/lib/openhab/core/actions/http.rb +80 -0
  6. data/lib/openhab/core/actions/ping.rb +30 -0
  7. data/lib/openhab/core/actions/transformation.rb +32 -0
  8. data/lib/openhab/core/actions/voice.rb +36 -0
  9. data/lib/openhab/core/actions.rb +23 -120
  10. data/lib/openhab/core/{events → dto}/item_channel_link.rb +1 -4
  11. data/lib/openhab/core/{events → dto}/thing.rb +10 -12
  12. data/lib/openhab/core/dto.rb +11 -0
  13. data/lib/openhab/core/entity_lookup.rb +1 -1
  14. data/lib/openhab/core/events/abstract_event.rb +1 -0
  15. data/lib/openhab/core/events/abstract_item_registry_event.rb +36 -0
  16. data/lib/openhab/core/events/abstract_thing_registry_event.rb +40 -0
  17. data/lib/openhab/core/events/item_command_event.rb +1 -1
  18. data/lib/openhab/core/events/item_state_changed_event.rb +6 -6
  19. data/lib/openhab/core/events/item_state_event.rb +6 -6
  20. data/lib/openhab/core/events/thing_status_info_event.rb +8 -6
  21. data/lib/openhab/core/items/generic_item.rb +2 -1
  22. data/lib/openhab/core/items/persistence.rb +52 -18
  23. data/lib/openhab/core/items/player_item.rb +1 -1
  24. data/lib/openhab/core/items/proxy.rb +20 -14
  25. data/lib/openhab/core/items/registry.rb +2 -0
  26. data/lib/openhab/core/items.rb +3 -3
  27. data/lib/openhab/core/profile_factory.rb +3 -1
  28. data/lib/openhab/core/proxy.rb +125 -0
  29. data/lib/openhab/core/things/links/provider.rb +1 -1
  30. data/lib/openhab/core/things/proxy.rb +8 -0
  31. data/lib/openhab/core/types/date_time_type.rb +2 -1
  32. data/lib/openhab/core/types/decimal_type.rb +1 -1
  33. data/lib/openhab/core/types/un_def_type.rb +2 -2
  34. data/lib/openhab/core/value_cache.rb +1 -1
  35. data/lib/openhab/core_ext/ephemeris.rb +53 -0
  36. data/lib/openhab/core_ext/java/class.rb +1 -1
  37. data/lib/openhab/core_ext/java/duration.rb +25 -1
  38. data/lib/openhab/core_ext/java/local_date.rb +2 -0
  39. data/lib/openhab/core_ext/java/month_day.rb +2 -0
  40. data/lib/openhab/core_ext/java/zoned_date_time.rb +85 -0
  41. data/lib/openhab/core_ext/ruby/date.rb +2 -0
  42. data/lib/openhab/core_ext/ruby/date_time.rb +1 -0
  43. data/lib/openhab/core_ext/ruby/time.rb +1 -0
  44. data/lib/openhab/dsl/debouncer.rb +259 -0
  45. data/lib/openhab/dsl/items/builder.rb +4 -2
  46. data/lib/openhab/dsl/items/timed_command.rb +31 -13
  47. data/lib/openhab/dsl/rules/automation_rule.rb +28 -21
  48. data/lib/openhab/dsl/rules/builder.rb +357 -37
  49. data/lib/openhab/dsl/rules/guard.rb +12 -54
  50. data/lib/openhab/dsl/rules/name_inference.rb +11 -0
  51. data/lib/openhab/dsl/rules/property.rb +3 -4
  52. data/lib/openhab/dsl/rules/terse.rb +4 -1
  53. data/lib/openhab/dsl/rules/triggers/conditions/duration.rb +5 -6
  54. data/lib/openhab/dsl/rules/triggers/cron/cron.rb +1 -0
  55. data/lib/openhab/dsl/rules/triggers/cron/cron_handler.rb +19 -31
  56. data/lib/openhab/dsl/rules/triggers/watch/watch.rb +1 -0
  57. data/lib/openhab/dsl/rules/triggers/watch/watch_handler.rb +22 -30
  58. data/lib/openhab/dsl/things/builder.rb +1 -1
  59. data/lib/openhab/dsl/thread_local.rb +1 -0
  60. data/lib/openhab/dsl/version.rb +1 -1
  61. data/lib/openhab/dsl.rb +224 -3
  62. data/lib/openhab/rspec/hooks.rb +5 -2
  63. data/lib/openhab/rspec/karaf.rb +7 -0
  64. data/lib/openhab/rspec/mocks/instance_method_stasher.rb +22 -0
  65. data/lib/openhab/rspec/mocks/space.rb +23 -0
  66. data/lib/openhab/rspec/openhab/core/actions.rb +16 -4
  67. data/lib/openhab/rspec/openhab/core/items/proxy.rb +1 -13
  68. data/lib/openhab/rspec/suspend_rules.rb +1 -14
  69. data/lib/openhab/yard/base_helper.rb +19 -0
  70. data/lib/openhab/yard/code_objects/group_object.rb +9 -3
  71. data/lib/openhab/yard/coderay.rb +17 -0
  72. data/lib/openhab/yard/handlers/jruby/base.rb +10 -1
  73. data/lib/openhab/yard/handlers/jruby/java_import_handler.rb +3 -0
  74. data/lib/openhab/yard/html_helper.rb +49 -15
  75. data/lib/openhab/yard/markdown_helper.rb +135 -0
  76. data/lib/openhab/yard.rb +6 -0
  77. metadata +36 -4
@@ -30,8 +30,8 @@ module OpenHAB
30
30
  # @param [Config] config Rule configuration
31
31
  #
32
32
  # Constructor sets a number of variables, no further decomposition necessary
33
+ #
33
34
  def initialize(config)
34
- # Metrics disabled because only setters are called or defaults set.
35
35
  super()
36
36
  set_name(config.name)
37
37
  set_description(config.description)
@@ -50,6 +50,8 @@ module OpenHAB
50
50
  @thread_locals = ThreadLocal.persist
51
51
  @cleanup_hooks = Set.new
52
52
  @listener = nil
53
+ debounce_settings = config.debounce_settings || { for: nil }
54
+ @debouncer = Debouncer.new(**debounce_settings)
53
55
  end
54
56
 
55
57
  #
@@ -58,30 +60,34 @@ module OpenHAB
58
60
  # @param [java.util.Map] mod map provided by openHAB rules engine
59
61
  # @param [java.util.Map] inputs map provided by openHAB rules engine containing event and other information
60
62
  #
61
- #
62
63
  def execute(mod = nil, inputs = nil)
64
+ execute!(mod, inputs)
65
+ end
66
+
67
+ # @!visibility private
68
+ def on_removal(listener)
69
+ @cleanup_hooks << listener
70
+ listen_for_removal unless @listener
71
+ end
72
+
73
+ private
74
+
75
+ # This method gets called in rspec's SuspendRules as well
76
+ def execute!(mod, inputs)
63
77
  ThreadLocal.thread_local(**@thread_locals) do
64
78
  logger.trace { "Execute called with mod (#{mod&.to_string}) and inputs (#{inputs.inspect})" }
65
79
  logger.trace { "Event details #{inputs["event"].inspect}" } if inputs&.key?("event")
66
80
  trigger_conditions(inputs).process(mod: mod, inputs: inputs) do
67
81
  event = extract_event(inputs)
68
- process_queue(create_queue(event), mod, event)
82
+ @debouncer.call { process_queue(create_queue(event), mod, event) }
69
83
  end
70
84
  rescue Exception => e
71
- raise if defined?(::RSpec)
85
+ raise if defined?(::RSpec) && ::RSpec.current_example.example_group.propagate_exceptions?
72
86
 
73
87
  @run_context.send(:logger).log_exception(e)
74
88
  end
75
89
  end
76
90
 
77
- # @!visibility private
78
- def on_removal(listener)
79
- @cleanup_hooks << listener
80
- listen_for_removal unless @listener
81
- end
82
-
83
- private
84
-
85
91
  def cleanup
86
92
  @cleanup_hooks.each(&:cleanup)
87
93
  end
@@ -180,17 +186,18 @@ module OpenHAB
180
186
  def check_guards(event:)
181
187
  return true if @guard.nil?
182
188
 
183
- if @guard.should_run? event
184
- return true if @between.nil?
185
-
189
+ unless @between.nil?
186
190
  now = Time.now
187
- return true if @between.cover? now
188
-
189
- logger.trace("Skipped execution of rule '#{name}' because the current time #{now} " \
190
- "is not between #{@between.begin} and #{@between.end}")
191
- else
192
- logger.trace("Skipped execution of rule '#{name}' because of guard #{@guard}")
191
+ unless @between.cover?(now)
192
+ logger.trace("Skipped execution of rule '#{name}' because the current time #{now} " \
193
+ "is not between #{@between.begin} and #{@between.end}")
194
+ return false
195
+ end
193
196
  end
197
+
198
+ return true if @guard.should_run?(event)
199
+
200
+ logger.trace("Skipped execution of rule '#{name}' because of guard #{@guard}")
194
201
  false
195
202
  end
196
203
 
@@ -139,6 +139,9 @@ module OpenHAB
139
139
  # @return [Array] Of trigger definitions as passed in Ruby
140
140
  attr_reader :ruby_triggers
141
141
 
142
+ # @!visibility private
143
+ attr_reader :debounce_settings
144
+
142
145
  # @!visibility private
143
146
  Run = Struct.new(:block)
144
147
 
@@ -380,6 +383,30 @@ module OpenHAB
380
383
  #
381
384
  prop :enabled
382
385
 
386
+ #
387
+ # Returns all {Item Items} (or {GroupItem::Members GroupItem::Members}) referenced
388
+ # by the specified trigger types in this rule.
389
+ #
390
+ # @param [Symbol, Array<Symbol>] trigger_types Trigger types to search for dependencies
391
+ # @return [Array<Item, GroupItem::Members>]
392
+ #
393
+ # @example Ensure all dependencies have a state when executing a rule
394
+ # rule do |rule|
395
+ # changed Item1, Item2, Item3
396
+ # only_if { rule.dependencies.all?(&:state?) }
397
+ # run { FormulaItem.update(Item3.state - (Item1.state + Item2.state)) }
398
+ # end
399
+ #
400
+ def dependencies(trigger_types = %i[changed updated])
401
+ trigger_types = Array.wrap(trigger_types)
402
+
403
+ ruby_triggers.flat_map do |t|
404
+ next [] unless trigger_types.include?(t.first)
405
+
406
+ t[1].select { |i| i.is_a?(Item) || i.is_a?(GroupItem::Members) }
407
+ end.uniq
408
+ end
409
+
383
410
  # @!group Guards
384
411
  # Guards exist to only permit rules to run if certain conditions are
385
412
  # satisfied. Think of these as declarative `if` statements that keep
@@ -388,8 +415,8 @@ module OpenHAB
388
415
  #
389
416
  # ### Guard Combination
390
417
  #
391
- # {#only_if} and {#not_if} can be used on the same rule. Both must be
392
- # satisfied for a rule to execute.
418
+ # Multiple guards can be used on the same rule. All must be satisfied
419
+ # for a rule to execute.
393
420
  #
394
421
  # @example
395
422
  # rule "Set OutsideDimmer to 50% if LightSwitch turned on and OtherSwitch is OFF and Door is CLOSED" do
@@ -403,7 +430,7 @@ module OpenHAB
403
430
  #
404
431
  # @!method between(range)
405
432
  #
406
- # Only execute rule if current time is between supplied time ranges.
433
+ # Only execute rule if the current time is between the supplied time ranges.
407
434
  #
408
435
  # If the range is of strings, it will be parsed to an appropriate time class.
409
436
  #
@@ -464,7 +491,7 @@ module OpenHAB
464
491
  #
465
492
  # @!method only_if
466
493
  #
467
- # {only_if} allows rule execution when the block's is true and prevents it when it's false.
494
+ # Allows rule execution when the block's result is true and prevents it when it's false.
468
495
  #
469
496
  # @yieldparam [Core::Events::AbstractEvent] event The event data that is about to trigger the rule.
470
497
  # @yieldreturn [Boolean] A value indicating if the rule should run.
@@ -493,15 +520,13 @@ module OpenHAB
493
520
  # end
494
521
  #
495
522
  prop_array(:only_if) do |item|
496
- unless item.is_a?(Proc) || [item].flatten.all? { |it| it.respond_to?(:truthy?) }
497
- raise ArgumentError, "Object passed to only_if must be a proc"
498
- end
523
+ raise ArgumentError, "Object passed to only_if must be a proc" unless item.is_a?(Proc)
499
524
  end
500
525
 
501
526
  #
502
527
  # @!method not_if
503
528
  #
504
- # {not_if} prevents execution of rules when the block's result is true and allows it when it's true.
529
+ # Prevents execution of rules when the block's result is true and allows it when it's true.
505
530
  #
506
531
  # @yieldparam [Core::Events::AbstractEvent] event The event data that is about to trigger the rule.
507
532
  # @yieldreturn [Boolean] A value indicating if the rule should _not_ run.
@@ -523,9 +548,187 @@ module OpenHAB
523
548
  # end
524
549
  #
525
550
  prop_array(:not_if) do |item|
526
- unless item.is_a?(Proc) || [item].flatten.all? { |it| it.respond_to?(:truthy?) }
527
- raise ArgumentError, "Object passed to not_if must be a proc"
528
- end
551
+ raise ArgumentError, "Object passed to not_if must be a proc" unless item.is_a?(Proc)
552
+ end
553
+
554
+ # rubocop:disable Layout/LineLength
555
+
556
+ #
557
+ # Waits until triggers have stopped firing for a period of time before executing the rule.
558
+ #
559
+ # It ignores triggers that are "bouncing around" (rapidly firing) by ignoring
560
+ # them until they have quiesced (stopped triggering for a while).
561
+ #
562
+ # ## Comparison Table
563
+ # | Guard | Triggers Immediately | Description |
564
+ # | -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
565
+ # | {debounce_for} | No | Waits until there is a minimum interval between triggers. |
566
+ # | {throttle_for} | No | Rate-limits the executions to a minimum interval, regardless of the interval between triggers. Waits until the end of the period before executing, ignores any leading triggers. |
567
+ # | {only_every} | Yes | Rate-limits the executions to a minimum interval. Immediately executes the first trigger, then ignores subsequent triggers for the period. |
568
+ #
569
+ # ## Timing Diagram
570
+ # The following timing diagram illustrates the difference between {debounce_for},
571
+ # {throttle_for}, and {only_every} guards:
572
+ #
573
+ # ```
574
+ # TIME INDEX ===> 1 1 2 2 3 3 4 4
575
+ # 0 5 0 5 0 5 0 5 0 5
576
+ # Triggers : "X.X...X...X..XX.X.X....X.XXXXXXXXXXX....X....."
577
+ # debounce_for 5 : "|......................X.|..............X....."
578
+ # debounce_for 5..5 : "|....X|....X.|....X....|....X|....X|....X....X"
579
+ # debounce_for 5..6 : "|.....X...|.....X.|....X.|.....X|.....X.|....X"
580
+ # debounce_for 5..7 : "|......X..|......X|....X.|......X|......X....."
581
+ # debounce_for 5..8 : "|.......X.|.......X....|.......X|.......X....."
582
+ # debounce_for 5..20: "|...................X..|................X....."
583
+ # # throttle_for will fire every 5 intervals after the "first" trigger
584
+ # throttle_for 5 : "|....X|....X.|....X....|....X|....X|....X....."
585
+ # only_every 5 : "X.....X......X....X....X....X....X......X....."
586
+ #
587
+ # Triggers : "X.X...X...X..XX.X.X..X...XXXXXXXXXXX.X..X.X..."
588
+ # debounce_for 5..44: "|...........................................X."
589
+ #
590
+ # # Notice above, triggers keep firing with intervals less than 5, so
591
+ # # debouncer keeps waiting, but puts a stop at 44 (the end of range).
592
+ # ```
593
+ #
594
+ # @param [Duration,Range] debounce_time The minimum interval between two consecutive
595
+ # triggers before the rules are allowed to run.
596
+ #
597
+ # When specified just as a Duration or an endless range, it sets the minimum interval
598
+ # between two consecutive triggers before rules are executed. It will
599
+ # wait endlessly unless this condition is met or an end of range was specified.
600
+ #
601
+ # When the end of the range is specified, it sets the maximum amount of time to wait
602
+ # from the first trigger before the rule will execute, even when triggers continue
603
+ # to occur more frequently than the minimum interval.
604
+ #
605
+ # When an equal beginning and ending values are given, it will behave just like
606
+ # {throttle_for}.
607
+ #
608
+ # @return [void]
609
+ #
610
+ # @see throttle_for
611
+ # @see only_every
612
+ #
613
+ # @example Wait until item stopped changing for at least 1 minute before running the rule
614
+ # rule do
615
+ # changed Item1
616
+ # debounce_for 1.minute
617
+ # run { ... }
618
+ # end
619
+ #
620
+ # @example Alert when door is open for a while
621
+ # # Note: When combined with a state check (only_if), this becomes functionally
622
+ # # equivalent to the changed duration feature.
623
+ # rule "Door alert" do
624
+ # changed Door_State
625
+ # debounce_for 10.minutes
626
+ # only_if { Door_State.open? }
627
+ # run { notify("The Door has been open for 10 minutes!") }
628
+ # end
629
+ #
630
+ def debounce_for(debounce_time)
631
+ idle_time = debounce_time.is_a?(Range) ? debounce_time.begin : debounce_time
632
+ debounce(for: debounce_time, idle_time: idle_time)
633
+ end
634
+ # rubocop:enable Layout/LineLength
635
+
636
+ #
637
+ # Rate-limits rule executions by delaying triggers and executing the last
638
+ # trigger within the given duration.
639
+ #
640
+ # When a new trigger occurs, it will hold the execution and start a fixed timer for
641
+ # the given duration. Should more triggers occur during this time, keep holding
642
+ # and once the wait time is over, execute the latest trigger.
643
+ #
644
+ # {throttle_for} will execute rules after it had waited
645
+ # for the given duration, regardless of how frequently the triggers were occuring.
646
+ # In contrast, {debounce_for} will wait until there is a minimum interval
647
+ # between two triggers.
648
+ #
649
+ # {throttle_for} is ideal in situations where regular status updates need to be made
650
+ # for frequently changing values. It is also useful when a rule responds to triggers
651
+ # from multiple related items that are updated at around the same time. Instead of
652
+ # executing the rule multiple times, {throttle_for} will wait for a pre-set amount
653
+ # of time since the first group of triggers occurred before executing the rule.
654
+ #
655
+ # @param [Duration] duration The minimum amount of time to wait inbetween rule
656
+ # executions.
657
+ #
658
+ # @return [void]
659
+ #
660
+ # @see debounce_for
661
+ # @see only_every
662
+ #
663
+ # @example Perform calculations from multiple items
664
+ # rule "Update Power Summary " do |rule|
665
+ # changed Power_From_Solar, Power_Load, Power_From_Grid
666
+ # throttle_for 1.second
667
+ # only_if { rule.dependencies.all?(&:state?) } # make sure all items have a state
668
+ # run do
669
+ # msg = []
670
+ # msg << Power_Load.state.negate.to_unit("kW").format("Load: %.2f %unit%")
671
+ # msg << Power_From_Solar.state.to_unit("kW").format("PV: %.2f %unit%")
672
+ # if Power_From_Grid.positive?
673
+ # msg << Power_From_Grid.state.to_unit("kW").format("From Grid: %.1f %unit%")
674
+ # else
675
+ # msg << Power_From_Grid.state.negate.to_unit("kW").format("To Grid: %.1f %unit%")
676
+ # end
677
+ # Power_Summary.update(msg.join(", "))
678
+ # end
679
+ # end
680
+ #
681
+ def throttle_for(duration)
682
+ debounce(for: duration)
683
+ end
684
+
685
+ #
686
+ # Executes the rule then ignores subsequent triggers for a given duration.
687
+ #
688
+ # Additional triggers that occur within the given duration after the rule execution
689
+ # will be ignored. This results in executions happening only at the specified interval or
690
+ # more.
691
+ #
692
+ # Unlike {throttle_for}, this guard will execute the rule as soon as a new trigger
693
+ # occurs instead of waiting for the specified duration. This is ideal for triggers
694
+ # such as a door bell where the rule should run as soon as a new trigger is detected
695
+ # but ignore subsequent triggers if they occur too soon after.
696
+ #
697
+ # @param [Duration,:second,:minute,:hour,:day] interval The period during which
698
+ # subsequent triggers are ignored.
699
+ # @return [void]
700
+ #
701
+ # @example Only allow executions every 10 minutes or more
702
+ # rule "Aircon Vent Control" do
703
+ # changed BedRoom_Temperature
704
+ # only_every 10.minutes
705
+ # run do
706
+ # # Adjust BedRoom_Aircon_Vent
707
+ # end
708
+ # end
709
+ #
710
+ # @example Run only on the first update and ignore subsequent triggers for the next minute
711
+ # # They can keep pressing the door bell as often as they like,
712
+ # # but the bell will only ring at most once every minute
713
+ # rule do
714
+ # updated DoorBell_Button, to: "single"
715
+ # only_every 1.minute
716
+ # run { Audio.play_stream "doorbell.mp3" }
717
+ # end
718
+ #
719
+ # @example Using symbolic duration
720
+ # rule "Display update" do
721
+ # updated Power_Usage
722
+ # only_every :minute
723
+ # run { Power_Usage_Display.update "Current power usage: #{Power_Usage.average_since(1.minute.ago)}" }
724
+ # end
725
+ #
726
+ # @see debounce_for
727
+ # @see throttle_for
728
+ #
729
+ def only_every(interval)
730
+ interval = 1.send(interval) if %i[second minute hour day].include?(interval)
731
+ debounce(for: interval, leading: true)
529
732
  end
530
733
 
531
734
  # @!endgroup
@@ -541,7 +744,8 @@ module OpenHAB
541
744
  @rule_triggers = RuleTriggers.new
542
745
  @caller = caller_binding.eval "self"
543
746
  @ruby_triggers = []
544
- @on_load_id = nil
747
+ @on_load = nil
748
+ @debounce_settings = nil
545
749
  enabled(true)
546
750
  tags([])
547
751
  end
@@ -680,8 +884,7 @@ module OpenHAB
680
884
  # end
681
885
  def channel_linked(attach: nil)
682
886
  @ruby_triggers << [:channel_linked]
683
- trigger("core.GenericEventTrigger", eventTopic: "openhab/links/*/added",
684
- eventTypes: "ItemChannelLinkAddedEvent", attach: attach)
887
+ event("openhab/links/*/added", types: "ItemChannelLinkAddedEvent", attach: attach)
685
888
  end
686
889
 
687
890
  #
@@ -702,8 +905,7 @@ module OpenHAB
702
905
  # end
703
906
  def channel_unlinked(attach: nil)
704
907
  @ruby_triggers << [:channel_linked]
705
- trigger("core.GenericEventTrigger", eventTopic: "openhab/links/*/removed",
706
- eventTypes: "ItemChannelLinkRemovedEvent", attach: attach)
908
+ event(topic: "openhab/links/*/removed", types: "ItemChannelLinkRemovedEvent", attach: attach)
707
909
  end
708
910
 
709
911
  #
@@ -976,6 +1178,8 @@ module OpenHAB
976
1178
  # and on subsequent reloads on file modifications.
977
1179
  # This is useful to perform initialization routines, especially when combined with other triggers.
978
1180
  #
1181
+ # @param [Duration] delay The amount of time to wait before executing the rule.
1182
+ # When nil, execute immediately.
979
1183
  # @param [Object] attach Object to be attached to the trigger
980
1184
  # @return [void]
981
1185
  #
@@ -993,12 +1197,12 @@ module OpenHAB
993
1197
  # run { Security_Lights.on }
994
1198
  # end
995
1199
  #
996
- def on_load(attach: nil)
997
- # prevent overwriting @on_load_id
998
- raise ArgumentError, "on_load can only be used once within a rule" if @on_load_id
1200
+ def on_load(delay: nil, attach: nil)
1201
+ # prevent overwriting @on_load
1202
+ raise ArgumentError, "on_load can only be used once within a rule" if @on_load
999
1203
 
1000
- @on_load_id = SecureRandom.uuid
1001
- attachments[@on_load_id] = attach
1204
+ @on_load = { module: SecureRandom.uuid, delay: delay }
1205
+ attachments[@on_load[:module]] = attach
1002
1206
  end
1003
1207
 
1004
1208
  #
@@ -1165,6 +1369,61 @@ module OpenHAB
1165
1369
  end
1166
1370
  end
1167
1371
 
1372
+ #
1373
+ # Creates an item added trigger
1374
+ #
1375
+ # @param [Object] attach object to be attached to the trigger
1376
+ # @return [void]
1377
+ #
1378
+ # @example
1379
+ # rule "item added" do
1380
+ # item_added
1381
+ # run do |event|
1382
+ # logger.info("#{event.item.name} added.")
1383
+ # end
1384
+ # end
1385
+ def item_added(attach: nil)
1386
+ @ruby_triggers << [:item_added]
1387
+ event("openhab/items/*/added", types: "ItemAddedEvent", attach: attach)
1388
+ end
1389
+
1390
+ #
1391
+ # Creates an item removed trigger
1392
+ #
1393
+ # @param [Object] attach object to be attached to the trigger
1394
+ # @return [void]
1395
+ #
1396
+ # @example
1397
+ # rule "item removed" do
1398
+ # item_removed
1399
+ # run do |event|
1400
+ # logger.info("#{event.item.name} removed.")
1401
+ # end
1402
+ # end
1403
+ def item_removed(attach: nil)
1404
+ @ruby_triggers << [:item_removed]
1405
+ event("openhab/items/*/removed", types: "ItemRemovedEvent", attach: attach)
1406
+ end
1407
+
1408
+ #
1409
+ # Creates an item updated trigger
1410
+ #
1411
+ # @param [Object] attach object to be attached to the trigger
1412
+ # @return [void]
1413
+ #
1414
+ # @example
1415
+ # rule "item updated" do
1416
+ # item_updated
1417
+ # run do |event|
1418
+ # logger.info("#{event.item.name} updated.")
1419
+ # end
1420
+ # end
1421
+ #
1422
+ def item_updated(attach: nil)
1423
+ @ruby_triggers << [:item_updated]
1424
+ event("openhab/items/*/updated", types: "ItemUpdatedEvent", attach: attach)
1425
+ end
1426
+
1168
1427
  #
1169
1428
  # Creates a thing added trigger
1170
1429
  #
@@ -1180,8 +1439,7 @@ module OpenHAB
1180
1439
  # end
1181
1440
  def thing_added(attach: nil)
1182
1441
  @ruby_triggers << [:thing_added]
1183
- trigger("core.GenericEventTrigger", eventTopic: "openhab/things/*/added",
1184
- eventTypes: "ThingAddedEvent", attach: attach)
1442
+ event("openhab/things/*/added", types: "ThingAddedEvent", attach: attach)
1185
1443
  end
1186
1444
 
1187
1445
  #
@@ -1199,8 +1457,7 @@ module OpenHAB
1199
1457
  # end
1200
1458
  def thing_removed(attach: nil)
1201
1459
  @ruby_triggers << [:thing_removed]
1202
- trigger("core.GenericEventTrigger", eventTopic: "openhab/things/*/removed",
1203
- eventTypes: "ThingRemovedEvent", attach: attach)
1460
+ event("openhab/things/*/removed", types: "ThingRemovedEvent", attach: attach)
1204
1461
  end
1205
1462
 
1206
1463
  #
@@ -1210,17 +1467,39 @@ module OpenHAB
1210
1467
  # @return [void]
1211
1468
  #
1212
1469
  # @example
1213
- # rule "thing updated" do
1214
- # thing_updated
1215
- # run do |event|
1216
- # logger.info("#{event.thing.uid} updated.")
1217
- # end
1218
- # end
1470
+ # rule "thing updated" do
1471
+ # thing_updated
1472
+ # run do |event|
1473
+ # logger.info("#{event.thing.uid} updated.")
1474
+ # end
1475
+ # end
1219
1476
  #
1220
1477
  def thing_updated(attach: nil)
1221
- @ruby_triggers << [:thing_removed]
1222
- trigger("core.GenericEventTrigger", eventTopic: "openhab/things/*/updated",
1223
- eventTypes: "ThingUpdatedEvent", attach: attach)
1478
+ @ruby_triggers << [:thing_updated]
1479
+ event("openhab/things/*/updated", types: "ThingUpdatedEvent", attach: attach)
1480
+ end
1481
+
1482
+ #
1483
+ # Creates a trigger on events coming through the event bus
1484
+ #
1485
+ # @param [String] topic The topic to trigger on; can contain the wildcard `*`.
1486
+ # @param [String, nil] source The sender of the event to trigger on.
1487
+ # Default does not filter on source.
1488
+ # @param [String, Array<String>, nil] types Only subscribe to certain event types.
1489
+ # Default does not filter on event types.
1490
+ # @return [void]
1491
+ #
1492
+ # @example
1493
+ # rule "thing updated" do
1494
+ # event("openhab/things/*/updated", types: "ThingUpdatedEvent")
1495
+ # run do |event|
1496
+ # logger.info("#{event.thing.uid} updated")
1497
+ # end
1498
+ # end
1499
+ #
1500
+ def event(topic, source: nil, types: nil, attach: nil)
1501
+ types = types.join(",") if types.is_a?(Enumerable)
1502
+ trigger("core.GenericEventTrigger", eventTopic: topic, eventSource: source, eventTypes: types, attach: attach)
1224
1503
  end
1225
1504
 
1226
1505
  #
@@ -1445,7 +1724,7 @@ module OpenHAB
1445
1724
  #<OpenHAB::DSL::Rules::Builder: #{uid}
1446
1725
  triggers=#{triggers.inspect},
1447
1726
  run blocks=#{run.inspect},
1448
- on_load=#{!@on_load_id.nil?},
1727
+ on_load=#{!@on_load.nil?},
1449
1728
  Trigger Conditions=#{trigger_conditions.inspect},
1450
1729
  Trigger UIDs=#{triggers.map(&:id).inspect},
1451
1730
  Attachments=#{attachments.inspect}
@@ -1468,12 +1747,26 @@ module OpenHAB
1468
1747
  added_rule.actions.first.configuration.put("type", "application/x-ruby")
1469
1748
  added_rule.actions.first.configuration.put("script", script) if script
1470
1749
 
1471
- rule.execute(nil, { "module" => @on_load_id }) if @on_load_id
1750
+ process_on_load { |module_id| rule.execute(nil, { "module" => module_id }) }
1751
+
1472
1752
  added_rule
1473
1753
  end
1474
1754
 
1475
1755
  private
1476
1756
 
1757
+ # Calls the on_load block, with a delay if specified
1758
+ # @yield block to execute on load time
1759
+ # @yieldparam [String] module The module ID that identifies this on_load event
1760
+ def process_on_load
1761
+ return unless @on_load
1762
+
1763
+ if @on_load[:delay]
1764
+ after(@on_load[:delay]) { yield @on_load[:module] }
1765
+ else
1766
+ yield @on_load[:module]
1767
+ end
1768
+ end
1769
+
1477
1770
  # delegate to the caller's logger
1478
1771
  def logger
1479
1772
  @caller.send(:logger)
@@ -1505,7 +1798,7 @@ module OpenHAB
1505
1798
  # @return [true,false] True if rule has triggers, false otherwise
1506
1799
  #
1507
1800
  def triggers?
1508
- @on_load_id || !triggers.empty?
1801
+ !(@on_load.nil? && triggers.empty?)
1509
1802
  end
1510
1803
 
1511
1804
  #
@@ -1534,6 +1827,33 @@ module OpenHAB
1534
1827
  provider.add(unmanaged_rule)
1535
1828
  unmanaged_rule
1536
1829
  end
1830
+
1831
+ #
1832
+ # Prevents or delays executions of rules to within a specified interval.
1833
+ # Debounce handling is done after the from/to/command types are filtered, but before only_if/not_if
1834
+ # guards.
1835
+ #
1836
+ # For a more detailed timing diagram, see {Debouncer}.
1837
+ #
1838
+ # Note the trailing edge debouncer delays the triggers so that they were postponed for the given interval after
1839
+ # the first detection of the trigger.
1840
+ #
1841
+ # @param (see Debouncer#initialize)
1842
+ #
1843
+ # @return [void]
1844
+ #
1845
+ # @see Debouncer Debouncer class
1846
+ # @see OpenHAB::DSL.debounce DSL.debounce method
1847
+ # @see only_every
1848
+ #
1849
+ # @!visibility private
1850
+ def debounce(for: 1.second, leading: false, idle_time: nil)
1851
+ raise ArgumentError, "Debounce guard can only be specified once" if @debounce_settings
1852
+
1853
+ interval = binding.local_variable_get(:for)
1854
+ # This hash structure must match the parameter signature for Debouncer.new
1855
+ @debounce_settings = { for: interval, leading: leading, idle_time: idle_time }
1856
+ end
1537
1857
  end
1538
1858
  end
1539
1859
  end