huebot 0.4.0 → 1.0.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,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