openhab-scripting 4.30.3 → 4.32.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openhab/log/logger'
4
- require 'openhab/dsl/rules/triggers/trigger'
4
+ require 'openhab/dsl/things'
5
+ require_relative 'trigger'
5
6
 
6
7
  module OpenHAB
7
8
  module DSL
@@ -22,32 +23,59 @@ module OpenHAB
22
23
  # thing(s) to create trigger for if not specified with the channel
23
24
  # @param [String, Array<String>] triggered specific triggering condition(s) to match for trigger
24
25
  #
25
- def channel(*channels, thing: nil, triggered: nil, attach: nil) # rubocop:disable Metrics/AbcSize
26
- channels.flatten.product([thing].flatten).each do |(channel, t)|
27
- channel = channel.uid if channel.is_a?(org.openhab.core.thing.Channel)
28
- t = t.uid if t.is_a?(Thing)
29
- channel = [t, channel].compact.join(':')
30
- logger.trace("Creating channel trigger for channel(#{channel}), thing(#{t}), trigger(#{triggered})")
26
+ def channel(*channels, thing: nil, triggered: nil, attach: nil)
27
+ channel_trigger = Channel.new(rule_triggers: @rule_triggers)
28
+ Channel.channels(channels: channels, thing: thing).each do |channel|
31
29
  [triggered].flatten.each do |trigger|
32
- create_channel_trigger(channel, trigger, attach)
30
+ channel_trigger.trigger(channel: channel, trigger: trigger, attach: attach)
33
31
  end
34
32
  end
35
33
  end
36
34
 
37
- private
38
-
39
- #
40
- # Create a trigger for a channel
41
35
  #
42
- # @param [String] channel to look for triggers
43
- # @param [String] trigger specific channel trigger to match
36
+ # Creates channel triggers
44
37
  #
45
- #
46
- def create_channel_trigger(channel, trigger, attach)
47
- config = { 'channelUID' => channel }
48
- config['event'] = trigger.to_s unless trigger.nil?
49
- logger.trace("Creating Change Trigger for #{config}")
50
- append_trigger(Trigger::CHANNEL_EVENT, config, attach: attach)
38
+ class Channel < Trigger
39
+ include OpenHAB::Log
40
+
41
+ # @return [String] A channel event trigger
42
+ CHANNEL_EVENT = 'core.ChannelEventTrigger'
43
+
44
+ #
45
+ # Get an enumerator over the product of the channels and things and map them to a channel id
46
+ # @param [Object] channels to iterate over
47
+ # @param [Object] thing to combine with channels and iterate over
48
+ # @return [Enumerable] enumerable channel ids to trigger on
49
+ def self.channels(channels:, thing:)
50
+ logger.state 'Creating Channel/Thing Pairs', channels: channels, thing: thing
51
+ channels.flatten.product([thing].flatten)
52
+ .map { |channel_thing| channel_id(*channel_thing) }
53
+ end
54
+
55
+ #
56
+ # Get a channel id from a channel and thing
57
+ # @param [Object] channel part of channel id, get UID if object is a Channel
58
+ # @param [Object] thing part of channel id, get UID if object is a Thing
59
+ #
60
+ def self.channel_id(channel, thing)
61
+ channel = channel.uid if channel.is_a?(org.openhab.core.thing.Channel)
62
+ thing = thing.uid if thing.is_a?(Thing)
63
+ [thing, channel].compact.join(':')
64
+ end
65
+
66
+ #
67
+ # Create a trigger for a channel
68
+ #
69
+ # @param [String] channel to look for triggers
70
+ # @param [String] trigger specific channel trigger to match
71
+ #
72
+ #
73
+ def trigger(channel:, trigger:, attach:)
74
+ config = { 'channelUID' => channel }
75
+ config['event'] = trigger.to_s unless trigger.nil?
76
+ logger.state 'Creating Channel Trigger', channel: channel, config: config
77
+ append_trigger(type: CHANNEL_EVENT, config: config, attach: attach)
78
+ end
51
79
  end
52
80
  end
53
81
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openhab/log/logger'
4
+ require_relative 'trigger'
4
5
 
5
6
  module OpenHAB
6
7
  module DSL
@@ -22,81 +23,130 @@ module OpenHAB
22
23
  #
23
24
  #
24
25
  def received_command(*items, command: nil, commands: nil, attach: nil)
25
- separate_groups(items).map do |item|
26
- logger.trace("Creating received command trigger for item(#{item})"\
27
- "command(#{command}) commands(#{commands})")
26
+ command_trigger = Command.new(rule_triggers: @rule_triggers)
28
27
 
29
- # Combine command and commands, doing union so only a single nil will be in the combined array.
30
- combined_commands = combine_commands(command, commands)
31
- create_received_trigger(combined_commands, item, attach)
28
+ # Combine command and commands, doing union so only a single nil will be in the combined array.
29
+ combined_commands = Command.combine_commands(command: command, commands: commands)
30
+
31
+ Command.flatten_items(items).map do |item|
32
+ combined_commands.map do |cmd|
33
+ logger.states 'Creating received command trigger', item: item, command: cmd
34
+
35
+ command_trigger.trigger(item: item, command: cmd, attach: attach)
36
+ end
32
37
  end.flatten
33
38
  end
34
39
 
35
- private
36
-
37
- #
38
- # Create a received trigger based on item type
39
40
  #
40
- # @param [Array] commands to create trigger for
41
- # @param [Object] item to create trigger for
42
- #
43
- #
44
- def create_received_trigger(commands, item, attach)
45
- commands.map do |command|
46
- if item.is_a? OpenHAB::DSL::Items::GroupItem::GroupMembers
47
- config, trigger = create_group_command_trigger(item)
48
- else
49
- config, trigger = create_item_command_trigger(item)
41
+ # Creates command triggers
42
+ #
43
+ class Command < Trigger
44
+ # Combine command and commands into a single array
45
+ #
46
+ # @param [Array] command list of commands to trigger on
47
+ # @param [Array] commands list of commands to trigger on
48
+ #
49
+ # @return [Array] Combined flattened and compacted list of commands
50
+ #
51
+ def self.combine_commands(command:, commands:)
52
+ combined_commands = ([command] | [commands]).flatten
53
+
54
+ # If either command or commands has a value and one is nil, we need to remove nil from the array.
55
+ # If it is only now a single nil value, we leave the nil in place, so that we create a trigger
56
+ # That isn't looking for a specific command.
57
+ combined_commands = combined_commands.compact unless combined_commands.all?(&:nil?)
58
+ combined_commands
59
+ end
60
+
61
+ #
62
+ # Create a received command trigger
63
+ #
64
+ # @param [Object] item item to create trigger for
65
+ # @param [Object] command to check against
66
+ # @param [Object] attach attachment
67
+ #
68
+ # @return [Trigger] OpenHAB triggers
69
+ #
70
+ def trigger(item:, command:, attach:)
71
+ case command
72
+ when Range then range_trigger(item: item, command: command, attach: attach)
73
+ when Proc then proc_trigger(item: item, command: command, attach: attach)
74
+ else command_trigger(item: item, command: command, attach: attach)
50
75
  end
76
+ end
77
+
78
+ #
79
+ # Creates a trigger with a range condition on the 'command' field
80
+ # @param [Object] item to create changed trigger on
81
+ # @param [Object] command to restrict trigger to
82
+ # @param [Object] attach to trigger
83
+ # @return [Trigger] OpenHAB trigger
84
+ #
85
+ def range_trigger(item:, command:, attach:)
86
+ command_range, * = Conditions::Proc.range_procs(command)
87
+ proc_trigger(item: item, command: command_range, attach: attach)
88
+ end
89
+
90
+ #
91
+ # Creates a trigger with a proc condition on the 'command' field
92
+ # @param [Object] item to create changed trigger on
93
+ # @param [Object] command to restrict trigger to
94
+ # @param [Object] attach to trigger
95
+ # @return [Trigger] OpenHAB trigger
96
+ #
97
+ def proc_trigger(item:, command:, attach:)
98
+ conditions = Conditions::Proc.new(command: command)
99
+ command_trigger(item: item, command: nil, attach: attach, conditions: conditions)
100
+ end
101
+
102
+ #
103
+ # Create a received trigger based on item type
104
+ #
105
+ # @param [Array] commands to create trigger for
106
+ # @param [Object] item to create trigger for
107
+ #
108
+ #
109
+ def command_trigger(item:, command:, attach: nil, conditions: nil)
110
+ type, config = if item.is_a? OpenHAB::DSL::Items::GroupItem::GroupMembers
111
+ group(group: item)
112
+ else
113
+ item(item: item)
114
+ end
51
115
  config['command'] = command.to_s unless command.nil?
52
- append_trigger(trigger, config, attach: attach)
116
+ append_trigger(type: type, config: config, attach: attach, conditions: conditions)
53
117
  end
54
- end
55
118
 
56
- #
57
- # Create trigger for item commands
58
- #
59
- # @param [Item] item to create trigger for
60
- #
61
- # @return [Array<Hash,Trigger>] first element is hash of trigger config properties
62
- # second element is trigger type
63
- #
64
- def create_item_command_trigger(item)
65
- config = { 'itemName' => item.name }
66
- trigger = Trigger::ITEM_COMMAND
67
- [config, trigger]
68
- end
119
+ private
69
120
 
70
- #
71
- # Create trigger for group items
72
- #
73
- # @param [Group] group to create trigger for
74
- #
75
- # @return [Array<Hash,Trigger>] first element is hash of trigger config properties
76
- # second element is trigger type
77
- #
78
- def create_group_command_trigger(group)
79
- config = { 'groupName' => group.group.name }
80
- trigger = Trigger::GROUP_COMMAND
81
- [config, trigger]
82
- end
121
+ # @return [String] item command trigger
122
+ ITEM_COMMAND = 'core.ItemCommandTrigger'
83
123
 
84
- #
85
- # Combine command and commands into a single array
86
- #
87
- # @param [Array] command list of commands to trigger on
88
- # @param [Array] commands list of commands to trigger on
89
- #
90
- # @return [Array] Combined flattened and compacted list of commands
91
- #
92
- def combine_commands(command, commands)
93
- combined_commands = ([command] | [commands]).flatten
94
-
95
- # If either command or commands has a value and one is nil, we need to remove nil from the array.
96
- # If it is only now a single nil value, we leave the nil in place, so that we create a trigger
97
- # That isn't looking for a specific command.
98
- combined_commands = combined_commands.compact unless combined_commands.all?(&:nil?)
99
- combined_commands
124
+ # @return [String] A group command trigger for items in the group
125
+ GROUP_COMMAND = 'core.GroupCommandTrigger'
126
+
127
+ #
128
+ # Create trigger for item commands
129
+ #
130
+ # @param [Item] item to create trigger for
131
+ #
132
+ # @return [Array<Hash,Trigger>] first element is hash of trigger config properties
133
+ # second element is trigger type
134
+ #
135
+ def item(item:)
136
+ [ITEM_COMMAND, { 'itemName' => item.name }]
137
+ end
138
+
139
+ #
140
+ # Create trigger for group items
141
+ #
142
+ # @param [Group] group to create trigger for
143
+ #
144
+ # @return [Array<Hash,Trigger>] first element is hash of trigger config properties
145
+ # second element is trigger type
146
+ #
147
+ def group(group:)
148
+ [GROUP_COMMAND, { 'groupName' => group.group.name }]
149
+ end
100
150
  end
101
151
  end
102
152
  end
@@ -38,9 +38,9 @@ module OpenHAB
38
38
  #
39
39
  def self.range_proc(range)
40
40
  logger.trace("Creating range proc for #{range}")
41
- lambda do |state|
42
- logger.trace("Range proc checking if #{state} is in #{range}")
43
- range.include? state
41
+ lambda do |val|
42
+ logger.trace("Range proc checking if #{val} is in #{range}")
43
+ range.cover? val
44
44
  end
45
45
  end
46
46
 
@@ -77,9 +77,10 @@ module OpenHAB
77
77
  # @param [Proc] from Proc to check against from value
78
78
  # @param [Proc] to Proc to check against to value
79
79
  #
80
- def initialize(from: nil, to: nil)
80
+ def initialize(from: nil, to: nil, command: nil)
81
81
  @from = from
82
82
  @to = to
83
+ @command = command
83
84
  end
84
85
 
85
86
  #
@@ -88,7 +89,18 @@ module OpenHAB
88
89
  #
89
90
  def process(mod:, inputs:) # rubocop:disable Lint/UnusedMethodArgument - mod is unused here but required
90
91
  logger.trace("Checking #{inputs} against condition trigger #{self}")
91
- yield if check_from(inputs: inputs) && check_to(inputs: inputs)
92
+ yield if check_procs(inputs: inputs)
93
+ end
94
+
95
+ #
96
+ # Check if command condition match the proc
97
+ # @param [Hash] inputs from trigger must be supplied if state is not supplied
98
+ # @return [true/false] depending on if from is set and matches supplied conditions
99
+ #
100
+ def check_command(inputs: nil)
101
+ command = input_state(inputs, 'command')
102
+ logger.trace "Checking command(#{@command}) against command(#{command})"
103
+ check_proc(proc: @command, value: command)
92
104
  end
93
105
 
94
106
  #
@@ -100,7 +112,7 @@ module OpenHAB
100
112
  def check_from(inputs: nil, state: nil)
101
113
  state ||= input_state(inputs, 'oldState')
102
114
  logger.trace "Checking from(#{@from}) against state(#{state})"
103
- check_proc(proc: @from, state: state)
115
+ check_proc(proc: @from, value: state)
104
116
  end
105
117
 
106
118
  #
@@ -112,27 +124,34 @@ module OpenHAB
112
124
  def check_to(inputs: nil, state: nil)
113
125
  state ||= input_state(inputs, 'newState', 'state')
114
126
  logger.trace "Checking to(#{@to}) against state(#{state})"
115
- check_proc(proc: @to, state: state)
127
+ check_proc(proc: @to, value: state)
116
128
  end
117
129
 
118
130
  # Describe the Proc Condition as a string
119
131
  # @return [String] string representation of proc condition
120
132
  #
121
133
  def to_s
122
- "From:(#{@from}) To:(#{@to})"
134
+ "From:(#{@from}) To:(#{@to}) Command:(#{@command})"
123
135
  end
124
136
 
125
137
  private
126
138
 
139
+ #
140
+ # Check all procs
141
+ # @param [Hash] inputs from event
142
+ # @return [true/false] true if all procs return true, false otherwise
143
+ def check_procs(inputs:)
144
+ check_from(inputs: inputs) && check_to(inputs: inputs) && check_command(inputs: inputs)
145
+ end
146
+
127
147
  # Check if a field matches the proc condition
128
148
  # @param [Proc] proc to call
129
- # @param [Hash] inputs containing fields
130
- # @param [Array] fields array of fields to extract from inputs, first one found is passed to proc
149
+ # @param [Hash] value to check
131
150
  # @return [true,false] true if proc is nil or proc.call returns true, false otherwise
132
- def check_proc(proc:, state:)
133
- return true if proc.nil? || proc.call(state)
151
+ def check_proc(proc:, value:)
152
+ return true if proc.nil? || proc.call(value)
134
153
 
135
- logger.trace("Skipped execution of rule because state #{state} evalulated false for (#{proc})")
154
+ logger.trace("Skipped execution of rule because value #{value} evaluated false for (#{proc})")
136
155
  false
137
156
  end
138
157
 
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'java'
4
+ require 'openhab/dsl/rules/triggers/trigger'
5
+
6
+ module OpenHAB
7
+ module DSL
8
+ module Rules
9
+ #
10
+ # Cron type rules
11
+ #
12
+ module Triggers
13
+ #
14
+ # Create a rule that executes at the specified interval
15
+ #
16
+ # @param [Object] value Symbol or Duration to execute this rule
17
+ # @param [Object] at TimeOfDay or String representing TimeOfDay in which to execute rule
18
+ #
19
+ #
20
+ def every(value, at: nil, attach: nil)
21
+ cron_expression = case value
22
+ when Symbol then Cron.from_symbol(value, at)
23
+ when Java::JavaTime::Duration then Cron.from_duration(value, at)
24
+ else raise ArgumentExpression, 'Unknown interval'
25
+ end
26
+ cron(cron_expression, attach: attach)
27
+ end
28
+
29
+ #
30
+ # Create a OpenHAB Cron trigger
31
+ #
32
+ # @param [String] expression OpenHAB style cron expression
33
+ #
34
+ def cron(expression, attach: nil)
35
+ cron = Cron.new(rule_triggers: @rule_triggers)
36
+ cron.trigger(config: { 'cronExpression' => expression }, attach: attach)
37
+ end
38
+
39
+ #
40
+ # Creates cron triggers
41
+ #
42
+ class Cron < Trigger
43
+ # Trigger ID for Watch Triggers
44
+ CRON_TRIGGER_MODULE_ID = 'jsr223.jruby.CronTrigger'
45
+
46
+ #
47
+ # Returns a default map for cron expressions that fires every second
48
+ # This map is usually updated via merge by other methods to refine cron type triggers.
49
+ #
50
+ # @return [Hash] Map with symbols for :seconds, :minute, :hour, :dom, :month, :dow
51
+ # configured to fire every second
52
+ #
53
+ CRON_EXPRESSION_MAP =
54
+ {
55
+ second: '*',
56
+ minute: '*',
57
+ hour: '*',
58
+ dom: '?',
59
+ month: '*',
60
+ dow: '?'
61
+ }.freeze
62
+ private_constant :CRON_EXPRESSION_MAP
63
+
64
+ # @return [Hash] Map of days of the week from symbols to to OpenHAB cron strings
65
+ DAY_OF_WEEK_MAP = {
66
+ monday: 'MON',
67
+ tuesday: 'TUE',
68
+ wednesday: 'WED',
69
+ thursday: 'THU',
70
+ friday: 'FRI',
71
+ saturday: 'SAT',
72
+ sunday: 'SUN'
73
+ }.freeze
74
+ private_constant :DAY_OF_WEEK_MAP
75
+
76
+ # @return [Hash] Converts the DAY_OF_WEEK_MAP to map used by Cron Expression
77
+ DAY_OF_WEEK_EXPRESSION_MAP = DAY_OF_WEEK_MAP.transform_values { |v| CRON_EXPRESSION_MAP.merge(dow: v) }
78
+
79
+ private_constant :DAY_OF_WEEK_EXPRESSION_MAP
80
+
81
+ # @return [Hash] Create a set of cron expressions based on different time intervals
82
+ EXPRESSION_MAP = {
83
+ second: CRON_EXPRESSION_MAP,
84
+ minute: CRON_EXPRESSION_MAP.merge(second: '0'),
85
+ hour: CRON_EXPRESSION_MAP.merge(second: '0', minute: '0'),
86
+ day: CRON_EXPRESSION_MAP.merge(second: '0', minute: '0', hour: '0'),
87
+ week: CRON_EXPRESSION_MAP.merge(second: '0', minute: '0', hour: '0', dow: 'MON'),
88
+ month: CRON_EXPRESSION_MAP.merge(second: '0', minute: '0', hour: '0', dom: '1'),
89
+ year: CRON_EXPRESSION_MAP.merge(second: '0', minute: '0', hour: '0', dom: '1', month: '1')
90
+ }.merge(DAY_OF_WEEK_EXPRESSION_MAP).freeze
91
+
92
+ private_constant :EXPRESSION_MAP
93
+
94
+ #
95
+ # Create a cron map from a duration
96
+ #
97
+ # @param [java::time::Duration] duration
98
+ # @param [Object] at TimeOfDay or String representing time of day
99
+ #
100
+ # @return [Hash] map describing cron expression
101
+ #
102
+ def self.from_duration(duration, at)
103
+ raise ArgumentError, '"at" cannot be used with duration' if at
104
+
105
+ map_to_cron(duration_to_map(duration))
106
+ end
107
+
108
+ #
109
+ # Create a cron map from a symbol
110
+ #
111
+ # @param [Symbol] symbol
112
+ # @param [Object] at TimeOfDay or String representing time of day
113
+ #
114
+ # @return [Hash] map describing cron expression created from symbol
115
+ #
116
+ def self.from_symbol(symbol, at)
117
+ expression_map = EXPRESSION_MAP[symbol]
118
+ expression_map = at_condition(expression_map, at) if at
119
+ map_to_cron(expression_map)
120
+ end
121
+
122
+ #
123
+ # Map cron expression to to cron string
124
+ #
125
+ # @param [Map] map of cron expression
126
+ #
127
+ # @return [String] OpenHAB cron string
128
+ #
129
+ def self.map_to_cron(map)
130
+ %i[second minute hour dom month dow].map { |field| map.fetch(field) }.join(' ')
131
+ end
132
+
133
+ #
134
+ # Convert a Java Duration to a map for the map_to_cron method
135
+ #
136
+ # @param duration [Java::JavaTime::Duration] The duration object
137
+ #
138
+ # @return [Hash] a map suitable for map_to_cron
139
+ #
140
+ def self.duration_to_map(duration)
141
+ if duration.to_millis_part.zero? && duration.to_nanos_part.zero? && duration.to_days.zero?
142
+ %i[second minute hour].each do |unit|
143
+ to_unit_part = duration.public_send("to_#{unit}s_part")
144
+ next unless to_unit_part.positive?
145
+
146
+ to_unit = duration.public_send("to_#{unit}s")
147
+ break unless to_unit_part == to_unit
148
+
149
+ return EXPRESSION_MAP[unit].merge(unit => "*/#{to_unit}")
150
+ end
151
+ end
152
+ raise ArgumentError, "Cron Expression not supported for duration: #{duration}"
153
+ end
154
+
155
+ #
156
+ # If an at time is provided, parse that and merge the new fields into the expression.
157
+ #
158
+ # @param [<Type>] expression_map <description>
159
+ # @param [<Type>] at_time <description>
160
+ #
161
+ # @return [<Type>] <description>
162
+ #
163
+ def self.at_condition(expression_map, at_time)
164
+ if at_time
165
+ tod = at_time.is_a?(TimeOfDay::TimeOfDay) ? at_time : TimeOfDay::TimeOfDay.parse(at_time)
166
+ expression_map = expression_map.merge(hour: tod.hour, minute: tod.minute, second: tod.second)
167
+ end
168
+ expression_map
169
+ end
170
+
171
+ #
172
+ # Create a cron trigger based on item type
173
+ #
174
+ # @param [Array] commands to create trigger for
175
+ # @param [Object] item to create trigger for
176
+ #
177
+ #
178
+ def trigger(config:, attach:)
179
+ append_trigger(type: CRON_TRIGGER_MODULE_ID, config: config, attach: attach)
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end