huebot 0.5.0 → 1.1.0

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.
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