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