huebot 0.5.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/huebot/cli.rb CHANGED
@@ -1,7 +1,3 @@
1
- require 'optparse'
2
- require 'ostruct'
3
- require 'yaml'
4
-
5
1
  module Huebot
6
2
  #
7
3
  # Helpers for running huebot in cli-mode.
@@ -14,140 +10,7 @@ module Huebot
14
10
  #
15
11
  Options = Struct.new(:inputs, :read_stdin)
16
12
 
17
- #
18
- # Returns the command given to huebot.
19
- #
20
- # @return [Symbol]
21
- #
22
- def self.get_cmd
23
- ARGV[0].to_s.to_sym
24
- end
25
-
26
- def self.get_args(min: nil, max: nil, num: nil)
27
- args = ARGV[1..]
28
- if num
29
- if num != args.size
30
- $stderr.puts "Expected #{num} args, found #{args.size}"
31
- exit 1
32
- end
33
- elsif min and max
34
- if args.size < min or args.size > max
35
- $stderr.puts "Expected #{min}-#{max} args, found #{args.size}"
36
- end
37
- elsif min
38
- if args.size < min
39
- $stderr.puts "Expected at least #{num} args, found #{args.size}"
40
- exit 1
41
- end
42
- elsif max
43
- if args.size > max
44
- $stderr.puts "Expected no more than #{num} args, found #{args.size}"
45
- exit 1
46
- end
47
- end
48
- args
49
- end
50
-
51
- #
52
- # Parses and returns input from the CLI. Serious errors might result in the program exiting.
53
- #
54
- # @return [Huebot::CLI::Options] All given CLI options
55
- # @return [Array<Huebot::ProgramSrc>] Array of given program sources
56
- #
57
- def self.get_input!
58
- options, parser = option_parser
59
- parser.parse!
60
-
61
- files = ARGV[1..-1]
62
- if files.empty? and !options.read_stdin
63
- puts parser.help
64
- exit 1
65
- elsif (bad_paths = files.select { |p| !File.exist? p }).any?
66
- $stderr.puts "Cannot find #{bad_paths.join ', '}"
67
- exit 1
68
- else
69
- sources = files.map { |path|
70
- ProgramSrc.new(YAML.load_file(path), path)
71
- }
72
- sources << ProgramSrc.new(YAML.load($stdin.read), "STDIN") if options.read_stdin
73
- return options, sources
74
- end
75
- end
76
-
77
- #
78
- # Prints any program errors or warnings, and returns a boolean for each.
79
- #
80
- # @param programs [Array<Huebot::Program>]
81
- # @param io [IO] Usually $stdout or $stderr
82
- # @param quiet [Boolean] if true, don't print anything
83
- #
84
- def self.check!(programs, io, quiet: false)
85
- if (invalid_progs = programs.select { |prog| prog.errors.any? }).any?
86
- print_messages! io, "Errors", invalid_progs, :errors unless quiet
87
- end
88
-
89
- if (imperfect_progs = programs.select { |prog| prog.warnings.any? }).any?
90
- puts "" if invalid_progs.any?
91
- print_messages! io, "Warnings", imperfect_progs, :warnings unless quiet
92
- end
93
-
94
- return invalid_progs.any?, imperfect_progs.any?
95
- end
96
-
97
- # Print help and exit
98
- def self.help!
99
- _, parser = option_parser
100
- puts parser.help
101
- exit 1
102
- end
103
-
104
- private
105
-
106
- #
107
- # Print each message (of the given type) for each program.
108
- #
109
- # @param io [IO] Usually $stdout or $stderr
110
- # @param label [String] Top-level for this group of messages
111
- # @param progs [Array<Huebot::CLI::Program>]
112
- # @param msg_type [Symbol] name of method that holds the messages (i.e. :errors or :warnings)
113
- #
114
- def self.print_messages!(io, label, progs, msg_type)
115
- io.puts "#{label}:"
116
- progs.each { |prog|
117
- io.puts " #{prog.name}:"
118
- prog.send(msg_type).each_with_index { |msg, i| io.puts " #{i+1}. #{msg}" }
119
- }
120
- end
121
-
122
- def self.option_parser
123
- options = Options.new([], false)
124
- parser = OptionParser.new { |opts|
125
- opts.banner = %(
126
- List all lights and groups:
127
- huebot ls
128
-
129
- Run program(s):
130
- huebot run file1.yml [file2.yml [file3.yml ...]] [options]
131
-
132
- Validate programs and inputs:
133
- huebot check file1.yml [file2.yml [file3.yml ...]] [options]
134
-
135
- Manually set/clear the IP for your Hue Bridge (useful when on a VPN):
136
- huebot set-ip 192.168.1.20
137
- huebot clear-ip
138
-
139
- Clear all connection config:
140
- huebot unregister
141
-
142
- Options:
143
- ).strip
144
- opts.on("-lLIGHT", "--light=LIGHT", "Light ID or name") { |l| options.inputs << LightInput.new(l) }
145
- opts.on("-gGROUP", "--group=GROUP", "Group ID or name") { |g| options.inputs << GroupInput.new(g) }
146
- opts.on("--all", "All lights and groups TODO") { $stderr.puts "Not Implemented"; exit 1 }
147
- opts.on("-i", "Read program from STDIN") { options.read_stdin = true }
148
- opts.on("-h", "--help", "Prints this help") { puts opts; exit }
149
- }
150
- return options, parser
151
- end
13
+ autoload :Helpers, 'huebot/cli/helpers'
14
+ autoload :Runner, 'huebot/cli/runner'
152
15
  end
153
16
  end
data/lib/huebot/client.rb CHANGED
@@ -6,6 +6,7 @@ module Huebot
6
6
  class Client
7
7
  DISCOVERY_URI = URI(ENV["HUE_DISCOVERY_API"] || "https://discovery.meethue.com/")
8
8
  Bridge = Struct.new(:id, :ip)
9
+ Error = Class.new(Error)
9
10
 
10
11
  attr_reader :config
11
12
 
@@ -44,7 +45,7 @@ module Huebot
44
45
 
45
46
  def get!(path)
46
47
  resp, error = get path
47
- raise error if error
48
+ raise Error, error if error
48
49
  resp
49
50
  end
50
51
 
@@ -58,7 +59,7 @@ module Huebot
58
59
 
59
60
  def post!(path, body)
60
61
  resp, error = post path, body
61
- raise error if error
62
+ raise Error, error if error
62
63
  resp
63
64
  end
64
65
 
@@ -74,7 +75,7 @@ module Huebot
74
75
 
75
76
  def put!(path, body)
76
77
  resp, error = put path, body
77
- raise error if error
78
+ raise Error, error if error
78
79
  resp
79
80
  end
80
81
 
@@ -101,7 +102,7 @@ module Huebot
101
102
  return data, nil
102
103
  end
103
104
  else
104
- raise "Unexpected response from Bridge (#{resp.code}): #{resp.body}"
105
+ raise Error, "Unexpected response from Bridge (#{resp.code}): #{resp.body}"
105
106
  end
106
107
  end
107
108
 
@@ -0,0 +1,304 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ module Huebot
5
+ module Compiler
6
+ class ApiV1
7
+ DEVICE_REF = /\A\$([1-9][0-9]*)\Z/.freeze
8
+ TRANSITION_KEYS = ["transition"].freeze
9
+ SERIAL_KEYS = ["serial"].freeze
10
+ PARALLEL_KEYS = ["parallel"].freeze
11
+ INFINITE_KEYS = ["infinite"].freeze
12
+ COUNT_KEYS = ["count"].freeze
13
+ TIMER_KEYS = ["timer"].freeze
14
+ DEADLINE_KEYS = ["until"].freeze
15
+ HHMM = /\A[0-9]{2}:[0-9]{2}\Z/.freeze
16
+ PERCENT_CAPTURE = /\A([0-9]+)%\Z/.freeze
17
+ MIN_KELVIN = 2000
18
+ MAX_KELVIN = 6530
19
+ MAX_BRI = 254
20
+
21
+ def initialize(api_version)
22
+ @api_version = api_version
23
+ end
24
+
25
+ # @return [Huebot::Program]
26
+ def build(tokens, default_name = nil)
27
+ prog = Program.new
28
+ prog.name = tokens.delete("name") || default_name
29
+ prog.api_version = @api_version
30
+ prog.data = node tokens.dup
31
+ prog
32
+ end
33
+
34
+ private
35
+
36
+ def node(t, inherited_devices = nil)
37
+ errors, warnings = [], []
38
+ instruction, child_nodes =
39
+ case t.keys
40
+ when TRANSITION_KEYS
41
+ build_transition t.fetch("transition"), errors, warnings, inherited_devices
42
+ when SERIAL_KEYS
43
+ build_serial t.fetch("serial"), errors, warnings, inherited_devices
44
+ when PARALLEL_KEYS
45
+ build_parallel t.fetch("parallel"), errors, warnings, inherited_devices
46
+ else
47
+ errors << "Expected exactly one of: transition, serial, parallel. Found #{t.keys}"
48
+ [Program::AST::NoOp.new, []]
49
+ end
50
+ Program::AST::Node.new(instruction, child_nodes, errors, warnings)
51
+ end
52
+
53
+ def build_transition(t, errors, warnings, inherited_devices = nil)
54
+ state = build_state(t, errors, warnings)
55
+ devices = build_devices(t, errors, warnings, inherited_devices)
56
+ slp = build_sleep(t, errors, warnings)
57
+
58
+ errors << "'transition' requires devices" if devices.empty?
59
+ errors << "Unknown keys in 'transition': #{t.keys.join ", "}" if t.keys.any?
60
+
61
+ instruction = Program::AST::Transition.new(state, devices, slp)
62
+ return instruction, []
63
+ end
64
+
65
+ def build_serial(t, errors, warnings, inherited_devices = nil)
66
+ lp = build_loop(t, errors, warnings)
67
+ slp = build_sleep(t, errors, warnings)
68
+ devices = build_devices(t, errors, warnings, inherited_devices)
69
+ children = build_steps(t, errors, warnings, devices)
70
+
71
+ errors << "'serial' requires steps" if children.empty?
72
+ errors << "Unknown keys in 'serial': #{t.keys.join ", "}" if t.keys.any?
73
+
74
+ instruction = Program::AST::SerialControl.new(lp, slp)
75
+ return instruction, children
76
+ end
77
+
78
+ def build_parallel(t, errors, warnings, inherited_devices = nil)
79
+ lp = build_loop(t, errors, warnings)
80
+ slp = build_sleep(t, errors, warnings)
81
+ devices = build_devices(t, errors, warnings, inherited_devices)
82
+ children = build_steps(t, errors, warnings, devices)
83
+
84
+ errors << "'parallel' requires steps" if children.empty?
85
+ errors << "Unknown keys in 'parallel': #{t.keys.join ", "}" if t.keys.any?
86
+
87
+ instruction = Program::AST::ParallelControl.new(lp, slp)
88
+ return instruction, children
89
+ end
90
+
91
+ def map_state_keys(state, errors, warnings)
92
+ # bugfix to YAML
93
+ case state.delete true
94
+ when true
95
+ state["on"] = true
96
+ when false
97
+ state["on"] = false
98
+ end
99
+
100
+ time = state.delete "time"
101
+ case time
102
+ when Integer, Float
103
+ state["transitiontime"] = (time.to_f * 10).round(0)
104
+ when nil
105
+ # pass
106
+ else
107
+ errors << "'transition.state.time' must be a number"
108
+ end
109
+
110
+ ctk = state.delete "ctk"
111
+ case ctk
112
+ when MIN_KELVIN..MAX_KELVIN
113
+ state["ct"] = (1_000_000 / ctk).round # https://en.wikipedia.org/wiki/Mired
114
+ when nil
115
+ # pass
116
+ else
117
+ errors << "'transition.state.ctk' must be an integer between #{MIN_KELVIN} and #{MAX_KELVIN}"
118
+ end
119
+
120
+ case state["bri"]
121
+ when Integer, nil
122
+ # pass
123
+ when PERCENT_CAPTURE
124
+ n = $1.to_i
125
+ if n >= 0 and n <= 100
126
+ percent = n * 0.01
127
+ state["bri"] = (MAX_BRI * percent).round
128
+ else
129
+ errors << "'transition.state.bri' must be an integer or a percent between 0% and 100%"
130
+ end
131
+ else
132
+ errors << "'transition.state.bri' must be an integer or a percent between 0% and 100%"
133
+ end
134
+
135
+ state
136
+ end
137
+
138
+ def build_state(t, errors, warnings)
139
+ state = t.delete "state"
140
+ case state
141
+ when Hash
142
+ map_state_keys state, errors, warnings
143
+ when nil
144
+ errors << "'state' is required in a transition"
145
+ {}
146
+ else
147
+ errors << "Expected 'state' to be an object, got a #{state.class.name}"
148
+ {}
149
+ end
150
+ end
151
+
152
+ def build_steps(t, errors, warnings, inherited_devices = nil)
153
+ steps_val = t.delete "steps"
154
+ case steps_val
155
+ when Array
156
+ steps_val.map { |s| node s, inherited_devices }
157
+ when nil
158
+ errors << "Missing 'steps'"
159
+ []
160
+ else
161
+ errors << "'steps' should be an array but is a #{steps_val.class.name}"
162
+ []
163
+ end
164
+ end
165
+
166
+ def build_loop(t, errors, warnings)
167
+ loop_val = t.delete "loop"
168
+ case loop_val
169
+ when Hash
170
+ pause = loop_val.delete "pause"
171
+ errors << "'loop.pause' must be an integer. Found '#{pause.class.name}'" if pause and !pause.is_a? Integer
172
+
173
+ lp =
174
+ case loop_val.keys
175
+ when INFINITE_KEYS
176
+ loop_val["infinite"] == true ? Program::AST::InfiniteLoop.new : Program::AST::CountedLoop.new(1)
177
+ when COUNT_KEYS
178
+ num = loop_val["count"]
179
+ errors << "'loop.count' must be an integer. Found '#{num.class.name}'" unless num.is_a? Integer
180
+ Program::AST::CountedLoop.new(num)
181
+ when TIMER_KEYS
182
+ build_timer_loop loop_val["timer"], errors, warnings
183
+ when DEADLINE_KEYS
184
+ build_deadline_loop loop_val["until"], errors, warnings
185
+ else
186
+ errors << "'loop' must contain exactly one of: 'infinite', 'count', 'timer', or 'until', and optionally 'pause'. Found: #{loop_val.keys.join ", "}"
187
+ Program::AST::CountedLoop.new(1)
188
+ end
189
+ lp.pause = pause
190
+ lp
191
+ when nil
192
+ Program::AST::CountedLoop.new(1)
193
+ else
194
+ errors << "'loop' must be an object. Found '#{loop_val.class.name}'"
195
+ Program::AST::CountedLoop.new(1)
196
+ end
197
+ end
198
+
199
+ def build_timer_loop(t, errors, warnings)
200
+ hours = t.delete "hours"
201
+ minutes = t.delete "minutes"
202
+
203
+ errors << "'loop.hours' must be an integer" if hours and !hours.is_a? Integer
204
+ errors << "'loop.minutes' must be an integer" if minutes and !minutes.is_a? Integer
205
+ errors << "Unknown keys in 'loop.timer': #{t.keys.join ", "}" if t.keys.any?
206
+
207
+ Program::AST::TimerLoop.new(hours || 0, minutes || 0)
208
+ end
209
+
210
+ def build_deadline_loop(t, errors, warnings)
211
+ date = t.delete "date"
212
+ time = t.delete "time"
213
+ errors << "Unknown keys in 'loop.until': #{t.keys.join ", "}" if t.keys.any?
214
+
215
+ stop_time = build_stop_time(date, time, errors, warnings)
216
+ Program::AST::DeadlineLoop.new(stop_time)
217
+ end
218
+
219
+ def build_sleep(t, errors, warnings)
220
+ sleep_val = t.delete "pause"
221
+ case sleep_val
222
+ when Integer, Float
223
+ sleep_val
224
+ when nil
225
+ nil
226
+ else
227
+ errors << "'pause' must be an integer or float"
228
+ nil
229
+ end
230
+ end
231
+
232
+ def build_devices(t, errors, warnings, inherited_devices = nil)
233
+ devices_ref = t.delete("devices") || {}
234
+ return inherited_devices if devices_ref.empty? and inherited_devices
235
+
236
+ refs_val, lights_val, groups_val = devices_ref.delete("inputs"), devices_ref.delete("lights"), devices_ref.delete("groups")
237
+ lights = lights_val ? device_names(Program::AST::Light, "lights", lights_val, errors, warnings) : []
238
+ groups = groups_val ? device_names(Program::AST::Group, "groups", groups_val, errors, warnings) : []
239
+ refs =
240
+ case refs_val
241
+ when "$all"
242
+ [Program::AST::DeviceRef.new(:all)]
243
+ when nil
244
+ []
245
+ when Array
246
+ if refs_val.all? { |ref| ref.is_a?(String) && ref =~ DEVICE_REF }
247
+ refs_val.map { |ref|
248
+ n = ref.match(DEVICE_REF).captures[0].to_i
249
+ Program::AST::DeviceRef.new(n)
250
+ }
251
+ else
252
+ errors << "If 'inputs' is an array, it must be an array of input variables (e.g. [$1, $2, ...])"
253
+ []
254
+ end
255
+ else
256
+ errors << "'inputs' must be '$all' or an array of input variables (e.g. [$1, $2, ...])"
257
+ []
258
+ end
259
+
260
+ errors << "Unknown keys in 'devices': #{devices_ref.keys.join ", "}" if devices_ref.keys.any?
261
+ lights + groups + refs
262
+ end
263
+
264
+ def device_names(type, key, val, errors, warnings)
265
+ if val.is_a?(Array) and val.all? { |name| name.is_a? String }
266
+ val.map { |name| type.new(name) }
267
+ else
268
+ errors << "'#{key}' must be an array of names (found #{val.class.name})"
269
+ []
270
+ end
271
+ end
272
+
273
+ def build_stop_time(date_val, time_val, errors, warnings)
274
+ now = Time.now
275
+ d =
276
+ begin
277
+ date_val ? Date.iso8601(date_val) : now.to_date
278
+ rescue Date::Error
279
+ errors << "Invalid date '#{date_val}'. Use \"YYYY-MM-DD\" format."
280
+ Date.today
281
+ end
282
+
283
+ hrs, min =
284
+ if time_val.nil?
285
+ [now.hour, now.min]
286
+ elsif time_val.is_a?(String) and time_val =~ HHMM
287
+ time_val.split(":", 2).map(&:to_i)
288
+ else
289
+ errors << "Invalid time '#{time_val}'. Use \"HH:MM\" format."
290
+ [0, 0]
291
+ end
292
+
293
+ begin
294
+ t = Time.new(d.year, d.month, d.day, hrs, min, 0, now.utc_offset)
295
+ warnings << "Time (#{t.iso8601}) is already in the past" if t < now
296
+ t
297
+ rescue ArgumentError
298
+ errors << "Invalid datetime (year=#{d.year} month=#{d.month} day=#{d.day} hrs=#{hrs} min=#{min} sec=0 offset=#{now.utc_offset})"
299
+ now
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -1,161 +1,23 @@
1
1
  module Huebot
2
- class Compiler
3
- DEVICE_FIELDS = %i(light lights group groups device devices).freeze
2
+ module Compiler
3
+ Error = Class.new(Error)
4
4
 
5
- def initialize(device_mapper)
6
- @device_mapper = device_mapper
7
- end
5
+ autoload :ApiV1, 'huebot/compiler/api_v1'
8
6
 
9
7
  #
10
8
  # Build a huebot program from an intermediate representation (a Hash).
11
9
  #
12
- # @param ir [Hash]
13
- # @param default_name [String] A name to use if one isn't specified
10
+ # @param src [Huebot::Program::Src]
14
11
  # @return [Huebot::Program]
15
12
  #
16
- def build(ir, default_name = nil)
17
- ir = ir.clone
18
- prog = Huebot::Program.new
19
- prog.name = ir.delete("name") || default_name
20
-
21
- # loop/loops
22
- val_loop = ir.delete("loop") || ir.delete(:loop)
23
- prog.errors << "'loop' must be 'true' or 'false'." if !val_loop.nil? and ![true, false].include?(val_loop)
24
- prog.loop = val_loop == true
25
-
26
- val_loops = ir.delete("loops") || ir.delete(:loops)
27
- prog.errors << "'loops' must be a positive integer." if !val_loops.nil? and val_loops.to_i < 0
28
- prog.loops = val_loops.to_i
29
-
30
- prog.errors << "'loop' and 'loops' are mutually exclusive." if prog.loop? and prog.loops > 0
31
-
32
- # initial state
33
- if (val_init = ir.delete("initial") || ir.delete(:initial))
34
- errors, warnings, state = build_transition val_init
35
- prog.initial_state = state
36
- prog.errors += errors
37
- prog.warnings += warnings
38
- end
39
-
40
- # transitions
41
- if (val_trns = ir.delete("transitions") || ir.delete(:transitions))
42
- val_trns.each do |val_trn|
43
- errors, warnings, state = if val_trn["parallel"] || val_trn[:parallel]
44
- build_parallel_transition val_trn
45
- else
46
- build_transition val_trn
47
- end
48
- prog.transitions << state
49
- prog.errors += errors
50
- prog.warnings += warnings
51
- end
52
- end
53
-
54
- # final state
55
- if (val_fnl = ir.delete("final") || ir.delete(:final))
56
- errors, warnings, state = build_transition val_fnl
57
- prog.final_state = state
58
- prog.errors += errors
59
- prog.warnings += warnings
60
- end
61
-
62
- # be strict about extra crap
63
- if (unknown = ir.keys.map(&:to_s)).any?
64
- prog.errors << "Unrecognized values: #{unknown.join ', '}."
65
- end
66
-
67
- # Add any warnings
68
- prog.warnings << "'final' is defined but will never be reached because 'loop' is 'true'." if prog.final_state and prog.loop?
69
-
70
- prog
71
- end
72
-
73
- private
74
-
75
- def build_parallel_transition(t)
76
- errors, warnings = [], []
77
- transition = Huebot::Program::ParallelTransition.new(0, [])
78
-
79
- transition.wait = t.delete("wait") || t.delete(:wait)
80
- errors << "'wait' must be a positive integer." if transition.wait and transition.wait.to_i <= 0
81
-
82
- parallel = t.delete("parallel") || t.delete(:parallel)
83
- if !parallel.is_a? Array
84
- errors << "'parallel' must be an array of transitions"
85
- else
86
- parallel.each do |sub_t|
87
- sub_errors, sub_warnings, sub_transition = build_transition(sub_t)
88
- errors += sub_errors
89
- warnings += sub_warnings
90
- transition.children << sub_transition
13
+ def self.build(src)
14
+ compiler_class =
15
+ case src.api_version
16
+ when 1.0 then ApiV1
17
+ else raise Error, "Unknown API version '#{src.api_version}'"
91
18
  end
92
- end
93
-
94
- return errors, warnings, transition
95
- end
96
-
97
- def build_transition(t)
98
- errors, warnings = [], []
99
- transition = Huebot::Program::Transition.new
100
- transition.devices = []
101
-
102
- map_devices(t, :light, :lights, :light!) { |map_errors, devices|
103
- errors += map_errors
104
- transition.devices += devices
105
- }
106
-
107
- map_devices(t, :group, :groups, :group!) { |map_errors, devices|
108
- errors += map_errors
109
- transition.devices += devices
110
- }
111
-
112
- map_devices(t, :device, :devices, :var!) { |map_errors, devices|
113
- errors += map_errors
114
- transition.devices += devices
115
- }
116
- errors << "Missing light/lights, group/groups, or device/devices" if transition.devices.empty?
117
-
118
- transition.wait = t.delete("wait") || t.delete(:wait)
119
- errors << "'wait' must be a positive integer." if transition.wait and transition.wait.to_i <= 0
120
-
121
- state = {}
122
- switch = t.delete("switch")
123
- switch = t.delete(:switch) if switch.nil?
124
- if !switch.nil?
125
- state[:on] = case switch
126
- when true, :on then true
127
- when false, :off then false
128
- else
129
- errors << "Unrecognized 'switch' value '#{switch}'."
130
- nil
131
- end
132
- end
133
- state[:transitiontime] = t.delete("time") || t.delete(:time) || t.delete("transitiontime") || t.delete(:transitiontime) || 4
134
-
135
- transition.state = t.merge(state).each_with_object({}) { |(key, val), obj|
136
- key = key.to_sym
137
- obj[key] = val unless DEVICE_FIELDS.include? key
138
- }
139
- return errors, warnings, transition
140
- end
141
-
142
- private
143
-
144
- def map_devices(t, singular_key, plural_key, ref_type)
145
- errors, devices = [], []
146
-
147
- key = t[singular_key.to_s] || t[singular_key]
148
- keys = t[plural_key.to_s] || t[plural_key]
149
-
150
- (Array(key) + Array(keys)).each { |x|
151
- begin
152
- devices += Array(@device_mapper.send(ref_type, x))
153
- rescue Huebot::DeviceMapper::Unmapped => e
154
- errors << e.message
155
- end
156
- }
157
-
158
- yield errors, devices
19
+ compiler = compiler_class.new(src.api_version)
20
+ compiler.build(src.tokens, src.default_name)
159
21
  end
160
22
  end
161
23
  end