openhab-scripting 5.5.0 → 5.6.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openhab/core/actions/audio.rb +5 -2
  3. data/lib/openhab/core/configuration.rb +70 -0
  4. data/lib/openhab/core/emulate_hash.rb +241 -0
  5. data/lib/openhab/core/events/abstract_event.rb +11 -0
  6. data/lib/openhab/core/events/timer_event.rb +48 -0
  7. data/lib/openhab/core/items/group_function.rb +37 -0
  8. data/lib/openhab/core/items/group_item.rb +10 -4
  9. data/lib/openhab/core/items/item.rb +31 -0
  10. data/lib/openhab/core/items/metadata/hash.rb +9 -179
  11. data/lib/openhab/core/items/metadata/namespace_hash.rb +38 -141
  12. data/lib/openhab/core/items/semantics/semantic_tag.rb +5 -0
  13. data/lib/openhab/core/items/semantics/tag_class_methods.rb +9 -0
  14. data/lib/openhab/core/items/semantics.rb +7 -3
  15. data/lib/openhab/core/rules/rule.rb +17 -4
  16. data/lib/openhab/core/things/links/provider.rb +1 -1
  17. data/lib/openhab/core/things/proxy.rb +2 -1
  18. data/lib/openhab/core/things/thing.rb +23 -0
  19. data/lib/openhab/core/types/date_time_type.rb +2 -1
  20. data/lib/openhab/core/types/open_closed_type.rb +2 -1
  21. data/lib/openhab/core/types/string_type.rb +1 -1
  22. data/lib/openhab/core/types/up_down_type.rb +2 -1
  23. data/lib/openhab/core/value_cache.rb +6 -5
  24. data/lib/openhab/core_ext/java/duration.rb +2 -1
  25. data/lib/openhab/core_ext/java/local_time.rb +8 -6
  26. data/lib/openhab/core_ext/java/month_day.rb +2 -1
  27. data/lib/openhab/core_ext/java/period.rb +1 -1
  28. data/lib/openhab/core_ext/ruby/numeric.rb +1 -0
  29. data/lib/openhab/dsl/items/builder.rb +13 -6
  30. data/lib/openhab/dsl/rules/automation_rule.rb +29 -5
  31. data/lib/openhab/dsl/rules/builder.rb +47 -22
  32. data/lib/openhab/dsl/rules/rule_triggers.rb +1 -1
  33. data/lib/openhab/dsl/rules/triggers/conditions/generic.rb +1 -1
  34. data/lib/openhab/dsl/rules/triggers/cron/cron.rb +43 -16
  35. data/lib/openhab/dsl/rules/triggers/cron/cron_handler.rb +101 -96
  36. data/lib/openhab/dsl/things/builder.rb +8 -6
  37. data/lib/openhab/dsl/thread_local.rb +1 -0
  38. data/lib/openhab/dsl/timer_manager.rb +4 -4
  39. data/lib/openhab/dsl/version.rb +1 -1
  40. data/lib/openhab/dsl.rb +23 -4
  41. data/lib/openhab/osgi.rb +1 -1
  42. data/lib/openhab/rspec/karaf.rb +7 -4
  43. data/lib/openhab/rspec/openhab/core/actions.rb +0 -3
  44. metadata +13 -23
@@ -37,7 +37,8 @@ module OpenHAB
37
37
  #
38
38
  def !
39
39
  return UP if down?
40
- return DOWN if up?
40
+
41
+ DOWN if up?
41
42
  end
42
43
  end
43
44
  end
@@ -28,7 +28,7 @@ module OpenHAB
28
28
  # For a private cache, simply use an instance variable. See
29
29
  # {file:docs/ruby-basics.md#variables Instance Variables}.
30
30
  #
31
- # @note Because every script or UI rule gets it own JRuby engine instance,
31
+ # @note Because every script or UI rule gets its own JRuby engine instance,
32
32
  # you cannot rely on being able to access Ruby objects between them. Only
33
33
  # objects that implement a Java interface that's part of Java or openHAB
34
34
  # Core (such as Hash implements {java.util.Map}, or other basic
@@ -107,10 +107,11 @@ module OpenHAB
107
107
 
108
108
  # @see https://docs.ruby-lang.org/en/master/Hash.html#method-i-assoc Hash#assoc
109
109
  def assoc(key)
110
- [key, fetch(key) do
111
- # return nil directly, without storing a value to the cache
112
- return nil
113
- end]
110
+ [key,
111
+ fetch(key) do
112
+ # return nil directly, without storing a value to the cache
113
+ return nil
114
+ end]
114
115
  end
115
116
 
116
117
  # @see https://docs.ruby-lang.org/en/master/Hash.html#method-i-dig Hash#dig
@@ -64,7 +64,8 @@ module OpenHAB
64
64
  #
65
65
  def coerce(other)
66
66
  return [other.seconds, self] if other.is_a?(Numeric)
67
- return [other.to_i.seconds, self] if other.is_a?(Period)
67
+
68
+ [other.to_i.seconds, self] if other.is_a?(Period)
68
69
  end
69
70
 
70
71
  {
@@ -56,12 +56,14 @@ module OpenHAB
56
56
  return raw_parse(string, formatter) if formatter
57
57
 
58
58
  format = /(am|pm)$/i.match?(string) ? "h[:mm[:ss][.S]][ ]a" : "H[:mm[:ss][.S]]"
59
- java_send(:parse, [java.lang.CharSequence, java.time.format.DateTimeFormatter],
60
- string, java.time.format.DateTimeFormatterBuilder.new
61
- .parse_case_insensitive
62
- .parse_lenient
63
- .append_pattern(format)
64
- .to_formatter(java.util.Locale::ENGLISH))
59
+ java_send(:parse,
60
+ [java.lang.CharSequence, java.time.format.DateTimeFormatter],
61
+ string,
62
+ java.time.format.DateTimeFormatterBuilder.new
63
+ .parse_case_insensitive
64
+ .parse_lenient
65
+ .append_pattern(format)
66
+ .to_formatter(java.util.Locale::ENGLISH))
65
67
  end
66
68
  end
67
69
 
@@ -21,7 +21,8 @@ module OpenHAB
21
21
  #
22
22
  def parse(string)
23
23
  logger.trace("#{self.class}.parse #{string} (#{string.class})")
24
- java_send(:parse, [java.lang.CharSequence, java.time.format.DateTimeFormatter],
24
+ java_send(:parse,
25
+ [java.lang.CharSequence, java.time.format.DateTimeFormatter],
25
26
  string.to_s,
26
27
  java.time.format.DateTimeFormatter.ofPattern("[--]M-d"))
27
28
  end
@@ -47,7 +47,7 @@ module OpenHAB
47
47
  # @return [Array, nil]
48
48
  #
49
49
  def coerce(other)
50
- return [other.seconds, to_i.seconds] if other.is_a?(Numeric)
50
+ [other.seconds, to_i.seconds] if other.is_a?(Numeric)
51
51
  end
52
52
 
53
53
  {
@@ -190,6 +190,7 @@ module OpenHAB
190
190
  # Integer already has #|, so we have to prepend it here
191
191
  ::Integer.prepend(QuantityTypeConversion)
192
192
  ::Numeric.include(Numeric)
193
+ java.math.BigDecimal.include(QuantityTypeConversion)
193
194
  end
194
195
  end
195
196
  end
@@ -230,9 +230,7 @@ module OpenHAB
230
230
  tags.compact.map do |tag|
231
231
  case tag
232
232
  when String then tag
233
- when Symbol then tag.to_s
234
- when old_semantics then tag.java_class.simple_name
235
- when semantics then tag.name
233
+ when Symbol, semantics, old_semantics then tag.to_s
236
234
  else raise ArgumentError, "`#{tag}` must be a subclass of Semantics::Tag, a `Symbol`, or a `String`."
237
235
  end
238
236
  end
@@ -260,6 +258,8 @@ module OpenHAB
260
258
  # @param tags [String, Symbol, Semantics::Tag, Array<String, Symbol, Semantics::Tag>, nil]
261
259
  # Fluent alias for `tag`.
262
260
  # @param autoupdate [true, false, nil] Autoupdate setting (see {ItemBuilder#autoupdate})
261
+ # @param thing [String, Core::Things::Thing, Core::Things::ThingUID, nil]
262
+ # A Thing to be used as the base for the channel
263
263
  # @param channel [String, Core::Things::ChannelUID, nil] Channel to link the item to
264
264
  # @param expire [String] An expiration specification.
265
265
  # @param alexa [String, Symbol, Array<(String, Hash<String, Object>)>, nil]
@@ -270,7 +270,9 @@ module OpenHAB
270
270
  # Homekit metadata (see {ItemBuilder#homekit})
271
271
  # @param metadata [Hash<String, Hash>] Generic metadata (see {ItemBuilder#metadata})
272
272
  # @param state [State] Initial state
273
- def initialize(type, name = nil, label = nil,
273
+ def initialize(type,
274
+ name = nil,
275
+ label = nil,
274
276
  provider:,
275
277
  dimension: nil,
276
278
  unit: nil,
@@ -281,6 +283,7 @@ module OpenHAB
281
283
  tag: nil,
282
284
  tags: nil,
283
285
  autoupdate: nil,
286
+ thing: nil,
284
287
  channel: nil,
285
288
  expire: nil,
286
289
  alexa: nil,
@@ -310,6 +313,7 @@ module OpenHAB
310
313
  @metadata.merge!(metadata) if metadata
311
314
  @autoupdate = autoupdate
312
315
  @channels = []
316
+ @thing = thing
313
317
  @expire = nil
314
318
  if expire
315
319
  expire = Array(expire)
@@ -420,6 +424,7 @@ module OpenHAB
420
424
  # end
421
425
  #
422
426
  def channel(channel, config = {})
427
+ channel = "#{@thing}:#{channel}" if @thing && !channel.include?(":")
423
428
  @channels << [channel, config]
424
429
  end
425
430
 
@@ -569,7 +574,7 @@ module OpenHAB
569
574
  RUBY
570
575
  end
571
576
 
572
- FUNCTION_REGEX = /^([a-z]+)(?:\(([a-z]+)(?:,([a-z]+))*\))?/i.freeze
577
+ FUNCTION_REGEX = /^([a-z]+)(?:\((.*)\))?/i.freeze
573
578
  private_constant :FUNCTION_REGEX
574
579
 
575
580
  # The combiner function for this group
@@ -607,11 +612,13 @@ module OpenHAB
607
612
  def create_item
608
613
  base_item = super if type
609
614
  if function
615
+ require "csv"
616
+
610
617
  match = function.match(FUNCTION_REGEX)
611
618
 
612
619
  dto = org.openhab.core.items.dto.GroupFunctionDTO.new
613
620
  dto.name = match[1]
614
- dto.params = match[2..]
621
+ dto.params = CSV.parse_line(match[2]) if match[2]
615
622
  function = org.openhab.core.items.dto.ItemDTOMapper.map_function(base_item, dto)
616
623
  Core::Items::GroupItem.new(name, base_item, function)
617
624
  else
@@ -74,6 +74,10 @@ module OpenHAB
74
74
 
75
75
  # This method gets called in rspec's SuspendRules as well
76
76
  def execute!(mod, inputs)
77
+ # Store the context in a thread variable. It is accessed through DSL#method_missing
78
+ # which is triggered when the context variable is referenced inside the run block.
79
+ # It is added to @thread_locals so it is also available in #process_task below.
80
+ @thread_locals[:openhab_context] = extract_context(inputs)
77
81
  ThreadLocal.thread_local(**@thread_locals) do
78
82
  if logger.trace?
79
83
  logger.trace("Execute called with mod (#{mod&.to_string}) and inputs (#{inputs.inspect})")
@@ -84,10 +88,12 @@ module OpenHAB
84
88
  @debouncer.call { process_queue(create_queue(event), mod, event) }
85
89
  end
86
90
  rescue Exception => e
87
- raise if defined?(::RSpec) && ::RSpec.current_example.example_group.propagate_exceptions?
91
+ raise if defined?(::RSpec) && ::RSpec.current_example&.example_group&.propagate_exceptions?
88
92
 
89
93
  @run_context.send(:logger).log_exception(e)
90
94
  end
95
+ ensure
96
+ @thread_locals.delete(:openhab_context)
91
97
  end
92
98
 
93
99
  def cleanup
@@ -157,6 +163,24 @@ module OpenHAB
157
163
  struct_class.new(**inputs)
158
164
  end
159
165
 
166
+ #
167
+ # Converts inputs into context hash
168
+ # @return [Hash] Context hash.
169
+ #
170
+ def extract_context(inputs)
171
+ return unless inputs
172
+
173
+ inputs.reject { |key, _| key.include?(".") }
174
+ .to_h do |key, value|
175
+ [key.to_sym,
176
+ if value.is_a?(Item) && !value.is_a?(Core::Items::Proxy)
177
+ Core::Items::Proxy.new(value)
178
+ else
179
+ value
180
+ end]
181
+ end
182
+ end
183
+
160
184
  #
161
185
  # Get the trigger_id for the trigger that caused the rule creation
162
186
  #
@@ -230,7 +254,7 @@ module OpenHAB
230
254
  ThreadLocal.thread_local(**@thread_locals) do
231
255
  case task
232
256
  when BuilderDSL::Run then process_run_task(event, task)
233
- when BuilderDSL::Script then process_script_task(task)
257
+ when BuilderDSL::Script then process_script_task(event, task)
234
258
  when BuilderDSL::Trigger then process_trigger_task(event, task)
235
259
  when BuilderDSL::Otherwise then process_otherwise_task(event, task)
236
260
  end
@@ -294,9 +318,9 @@ module OpenHAB
294
318
  #
295
319
  # @param [Script] task to execute
296
320
  #
297
- def process_script_task(task)
298
- logger.trace { "Executing script '#{name}' run block" }
299
- @run_context.instance_exec(&task.block)
321
+ def process_script_task(event, task)
322
+ logger.trace { "Executing script '#{name}' run block with event(#{event})" }
323
+ @run_context.instance_exec(event, &task.block)
300
324
  end
301
325
 
302
326
  #
@@ -70,7 +70,8 @@ module OpenHAB
70
70
  builder = BuilderDSL.new(binding || block.binding)
71
71
  builder.uid(id)
72
72
  builder.instance_exec(builder, &block)
73
- builder.guard = Guard.new(run_context: builder.caller, only_if: builder.only_if,
73
+ builder.guard = Guard.new(run_context: builder.caller,
74
+ only_if: builder.only_if,
74
75
  not_if: builder.not_if)
75
76
 
76
77
  name ||= NameInference.infer_rule_name(builder)
@@ -1075,28 +1076,50 @@ module OpenHAB
1075
1076
  #
1076
1077
  # @overload cron(second: nil, minute: nil, hour: nil, dom: nil, month: nil, dow: nil, year: nil, attach: nil)
1077
1078
  # The trigger can be created by specifying each field as keyword arguments.
1078
- # Omitted fields will default to `*` or `?` as appropriate.
1079
+ #
1080
+ # When certain fields were omitted:
1081
+ # - The more specific fields will default to `0` for `hour`, `minute`, and `second`,
1082
+ # to `MON` for `dow`, and to `1` for `dom` and `month`.
1083
+ # - The less specific fields will default to `*` or `?` as appropriate.
1079
1084
  #
1080
1085
  # Each field is optional, but at least one must be specified.
1081
1086
  #
1082
1087
  # The same rules for the standard
1083
1088
  # [cron expression](https://www.quartz-scheduler.org/documentation/quartz-2.2.2/tutorials/tutorial-lesson-06.html)
1084
1089
  # apply for each field. For example, multiple values can be separated
1085
- # with a comma within a string.
1086
- #
1087
- # @param [Integer, String, nil] second
1088
- # @param [Integer, String, nil] minute
1089
- # @param [Integer, String, nil] hour
1090
- # @param [Integer, String, nil] dom
1091
- # @param [Integer, String, nil] month
1092
- # @param [Integer, String, nil] dow
1093
- # @param [Integer, String, nil] year
1090
+ # with a comma within a string, and ranges can be specified with a dash or with
1091
+ # a Ruby Range.
1092
+ #
1093
+ # @param [Integer, String, Range, nil] second
1094
+ # @param [Integer, String, Range, nil] minute
1095
+ # @param [Integer, String, Range, nil] hour
1096
+ # @param [Integer, String, Symbol, Range, nil] dom
1097
+ # @param [Integer, String, Symbol, Range, nil] month
1098
+ # @param [Integer, String, Symbol, Range, nil] dow
1099
+ # @param [Integer, String, Range, nil] year
1094
1100
  # @param [Object] attach object to be attached to the trigger
1095
- # @example
1101
+ #
1102
+ # @example Using String values
1096
1103
  # # Run every 3 minutes on Monday to Friday
1097
1104
  # # equivalent to the cron expression "0 */3 * ? * MON-FRI *"
1098
1105
  # rule "Using cron fields" do
1099
- # cron second: 0, minute: "*/3", dow: "MON-FRI"
1106
+ # cron minute: "*/3", dow: "MON-FRI"
1107
+ # run { logger.info "Cron rule executed" }
1108
+ # end
1109
+ #
1110
+ # @example Defaults for unspecified fields
1111
+ # # Run at midnight on the first day of January, February, and March
1112
+ # # equivalent to the cron expression "0 0 0 1 JAN-MAR ? *"
1113
+ # rule "Using cron fields" do
1114
+ # cron month: "JAN-MAR"
1115
+ # run { logger.info "Cron rule executed" }
1116
+ # end
1117
+ #
1118
+ # @example Using Ruby Range values
1119
+ # # Run on the hour, every hour between 1pm and 5pm
1120
+ # # equivalent to the cron expression "0 0 13-17 ? * ? *"
1121
+ # rule "Using cron fields" do
1122
+ # cron hour: 13..17
1100
1123
  # run { logger.info "Cron rule executed" }
1101
1124
  # end
1102
1125
  #
@@ -1112,7 +1135,6 @@ module OpenHAB
1112
1135
 
1113
1136
  raise ArgumentError, "Missing cron expression or elements" unless expression
1114
1137
 
1115
- add_tag("Schedule")
1116
1138
  cron = Cron.new(rule_triggers: @rule_triggers)
1117
1139
  cron.trigger(config: { "cronExpression" => expression }, attach: attach)
1118
1140
  end
@@ -1211,7 +1233,6 @@ module OpenHAB
1211
1233
  if value == :day && at.is_a?(Item)
1212
1234
  raise ArgumentError, "Attachments are not supported with dynamic datetime triggers" unless attach.nil?
1213
1235
 
1214
- add_tag("Schedule")
1215
1236
  return trigger("timer.DateTimeTrigger", itemName: at.name, timeOnly: true)
1216
1237
  end
1217
1238
 
@@ -1561,8 +1582,12 @@ module OpenHAB
1561
1582
  # @deprecated OH3.4 - OH3 config uses eventXXX vs OH4 uses `topic`, `source`, and `types`
1562
1583
  # See https://github.com/openhab/openhab-core/pull/3299
1563
1584
  trigger("core.GenericEventTrigger",
1564
- eventTopic: topic, eventSource: source, eventTypes: types, # @deprecated OH3.4
1565
- topic: topic, source: source, types: types,
1585
+ eventTopic: topic,
1586
+ eventSource: source,
1587
+ eventTypes: types, # @deprecated OH3.4
1588
+ topic: topic,
1589
+ source: source,
1590
+ types: types,
1566
1591
  attach: attach)
1567
1592
  end
1568
1593
 
@@ -1593,7 +1618,6 @@ module OpenHAB
1593
1618
  #
1594
1619
  def at(item)
1595
1620
  item = item.name if item.is_a?(Item)
1596
- add_tag("Schedule")
1597
1621
  trigger("timer.DateTimeTrigger", itemName: item.to_s)
1598
1622
  end
1599
1623
 
@@ -1834,10 +1858,11 @@ module OpenHAB
1834
1858
  types = [binding.local_variable_get(:for)].flatten
1835
1859
 
1836
1860
  WatchHandler::WatchTriggerHandlerFactory.instance # ensure it's registered
1837
- trigger(WatchHandler::WATCH_TRIGGER_MODULE_ID, path: path.to_s,
1838
- types: types.map(&:to_s),
1839
- glob: glob.to_s,
1840
- attach: attach)
1861
+ trigger(WatchHandler::WATCH_TRIGGER_MODULE_ID,
1862
+ path: path.to_s,
1863
+ types: types.map(&:to_s),
1864
+ glob: glob.to_s,
1865
+ attach: attach)
1841
1866
  end
1842
1867
 
1843
1868
  # @!endgroup
@@ -64,7 +64,7 @@ module OpenHAB
64
64
  org.openhab.core.automation.util.TriggerBuilder.create
65
65
  .with_id(uuid)
66
66
  .with_type_uid(type)
67
- .with_configuration(org.openhab.core.config.core.Configuration.new(config))
67
+ .with_configuration(Core::Configuration.new(config))
68
68
  .build
69
69
  end
70
70
 
@@ -29,7 +29,7 @@ module OpenHAB
29
29
  # @param [Hash] inputs inputs from trigger
30
30
  # @return [true, false] if the conditions passed (and therefore the block was run)
31
31
  #
32
- def process(mod:, inputs:) # rubocop:disable Lint/UnusedMethodArgument - mod is unused here but required
32
+ def process(mod:, inputs:)
33
33
  logger.trace("Checking #{inputs} against condition trigger #{self}")
34
34
  unless check_value(Conditions.old_state_from(inputs), @from) &&
35
35
  check_value(Conditions.new_state_from(inputs), @to) &&
@@ -12,7 +12,12 @@ module OpenHAB
12
12
  #
13
13
  class Cron < Trigger
14
14
  # Trigger ID for Cron Triggers
15
- CRON_TRIGGER_MODULE_ID = "jsr223.jruby.CronTrigger"
15
+ CRON_TRIGGER_MODULE_ID = if Gem::Version.new(OpenHAB::Core::VERSION) >= Gem::Version.new("4.0.0")
16
+ "timer.GenericCronTrigger"
17
+ else
18
+ # @deprecated OH3.4 We need to use a custom CronTrigger handler
19
+ "jsr223.jruby.CronTrigger"
20
+ end
16
21
 
17
22
  #
18
23
  # Returns a default map for cron expressions that fires every second
@@ -21,16 +26,15 @@ module OpenHAB
21
26
  # @return [Hash] Map with symbols for :seconds, :minute, :hour, :dom, :month, :dow
22
27
  # configured to fire every second
23
28
  #
24
- CRON_EXPRESSION_MAP =
25
- {
26
- second: "*",
27
- minute: "*",
28
- hour: "*",
29
- dom: "?",
30
- month: "*",
31
- dow: "?",
32
- year: "*"
33
- }.freeze
29
+ CRON_EXPRESSION_MAP = {
30
+ second: "*",
31
+ minute: "*",
32
+ hour: "*",
33
+ dom: "?",
34
+ month: "*",
35
+ dow: "?",
36
+ year: "*"
37
+ }.freeze
34
38
  private_constant :CRON_EXPRESSION_MAP
35
39
 
36
40
  # @return [Hash] Map of days of the week from symbols to to openHAB cron strings
@@ -47,7 +51,6 @@ module OpenHAB
47
51
 
48
52
  # @return [Hash] Converts the DAY_OF_WEEK_MAP to map used by Cron Expression
49
53
  DAY_OF_WEEK_EXPRESSION_MAP = DAY_OF_WEEK_MAP.transform_values { |v| CRON_EXPRESSION_MAP.merge(dow: v) }
50
-
51
54
  private_constant :DAY_OF_WEEK_EXPRESSION_MAP
52
55
 
53
56
  # @return [Hash] Create a set of cron expressions based on different time intervals
@@ -60,9 +63,12 @@ module OpenHAB
60
63
  month: CRON_EXPRESSION_MAP.merge(second: "0", minute: "0", hour: "0", dom: "1"),
61
64
  year: CRON_EXPRESSION_MAP.merge(second: "0", minute: "0", hour: "0", dom: "1", month: "1")
62
65
  }.merge(DAY_OF_WEEK_EXPRESSION_MAP).freeze
63
-
64
66
  private_constant :EXPRESSION_MAP
65
67
 
68
+ # @return [Hash] Translate cron field names to expression keys
69
+ FIELD_TO_EXPRESSION_KEY = Hash.new { |_, key| key }.merge({ dow: :week, dom: :day })
70
+ private_constant :FIELD_TO_EXPRESSION_KEY
71
+
66
72
  #
67
73
  # Create a cron map from a duration
68
74
  #
@@ -119,10 +125,28 @@ module OpenHAB
119
125
  "unknown keyword#{"s" if extra_fields.size > 1}: #{extra_fields.map(&:inspect).join(", ")}"
120
126
  end
121
127
 
122
- fields = fields.transform_values { |value| value.to_s.delete(" ") }
128
+ fields = fields.to_h do |key, value|
129
+ if value.is_a?(Range)
130
+ if value.exclude_end?
131
+ raise ArgumentError,
132
+ "Range must be inclusive for '#{key}'. Try '#{value.begin}..#{value.end}' instead"
133
+ end
134
+
135
+ unless value.begin && value.end
136
+ raise ArgumentError,
137
+ "Range must have a beginning and ending for '#{key}'"
138
+ end
139
+
140
+ [key, "#{value.begin.to_s.upcase}-#{value.end.to_s.upcase}".delete(" ")]
141
+ else
142
+ [key, value.to_s.delete(" ").upcase]
143
+ end
144
+ end
145
+ # convert fields' key dow->week, dom->day to look into EXPRESSION_MAP
146
+ fields_expression = fields.transform_keys { |key| FIELD_TO_EXPRESSION_KEY[key] }
123
147
  # find the first expression map that has a field from fields.
124
148
  # this ensure more-specific fields get set to 0, not *
125
- base_key = EXPRESSION_MAP.keys.find { |field, _| fields.key?(field) }
149
+ base_key = EXPRESSION_MAP.keys.find { |field, _| fields_expression.key?(field) }
126
150
  base_expression = EXPRESSION_MAP[base_key]
127
151
  expression_map = base_expression.merge(fields)
128
152
 
@@ -186,7 +210,10 @@ module OpenHAB
186
210
  #
187
211
  #
188
212
  def trigger(config:, attach:)
189
- CronHandler::CronTriggerHandlerFactory.instance # ensure it's registered
213
+ # @deprecated OH3.4 needs a custom CronTriggerHandlerFactory
214
+ if Gem::Version.new(OpenHAB::Core::VERSION) < Gem::Version.new("4.0.0")
215
+ CronHandler::CronTriggerHandlerFactory.instance # ensure it's registered
216
+ end
190
217
  append_trigger(type: CRON_TRIGGER_MODULE_ID, config: config, attach: attach)
191
218
  end
192
219
  end