nlhue 0.1.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.
@@ -0,0 +1,362 @@
1
+ # Discovery of Hue bridges using SSDP.
2
+ # There is *much* opportunity to clean this up. The mountains of asynchronous
3
+ # handlers make things rather messy.
4
+ # (C)2013 Mike Bourgeous
5
+
6
+ module NLHue
7
+ # TODO: Rewrite this from the ground up in a much simpler fashion. The
8
+ # current complexity allows things like two simultaneous disco
9
+ # processes running and makes things difficult to debug.
10
+
11
+ # Use #start_discovery and #stop_discovery for continuous bridge
12
+ # discovery. Use #send_discovery to perform discovery only once.
13
+ module Disco
14
+ # Number of times a bridge can be missing from discovery if not
15
+ # subscribed (approx. 5*15=1.25min if interval is 15)
16
+ MAX_BRIDGE_AGE = 5
17
+
18
+ # Number of times a bridge can be missing from discovery if
19
+ # subscribed (approximately one month if interval is 15). This
20
+ # number is higher than MAX_BRIDGE_AGE because update failures
21
+ # (see MAX_BRIDGE_ERR) will detect an offline bridge, and
22
+ # working bridges sometimes disappear from discovery.
23
+ MAX_SUBSCRIBED_AGE = 150000
24
+
25
+ # Number of times a bridge can fail to update
26
+ MAX_BRIDGE_ERR = 2
27
+
28
+ # Bridges discovered on the network
29
+ # [serial] => {
30
+ # :bridge => NLHue::Bridge,
31
+ # :age => [# of failed discos],
32
+ # :errcount => [# of failed updates]
33
+ # }
34
+ @@bridges = {}
35
+
36
+ @@disco_interval = 15
37
+ @@disco_timer = nil
38
+ @@disco_proc = nil
39
+ @@disco_done_timer = nil
40
+ @@disco_callbacks = []
41
+ @@bridges_changed = false
42
+ @@disco_running = false
43
+ @@disco_connection = nil
44
+
45
+ # Starts a timer that periodically discovers Hue bridges. If
46
+ # the username is a String or a Hash mapping bridge serial
47
+ # numbers (String or Symbol) to usernames, the Bridge objects'
48
+ # username will be set before trying to update.
49
+ #
50
+ # Using very short intervals may overload Hue bridges, causing
51
+ # light and group commands to be delayed or erratic.
52
+ def self.start_discovery username=nil, interval=15
53
+ raise 'Discovery is already running' if @@disco_timer || @@disco_running
54
+ raise 'Interval must be a number' unless interval.is_a? Numeric
55
+ raise 'Interval must be >= 1' unless interval >= 1
56
+
57
+ unless username.nil? || username.is_a?(String) || username.is_a?(Hash)
58
+ raise 'Username must be nil, a String, or a Hash'
59
+ end
60
+
61
+ @@disco_interval = interval
62
+
63
+ @@disco_proc = proc {
64
+ @@disco_timer = nil
65
+ @@disco_running = true
66
+
67
+ notify_callbacks :start
68
+
69
+ @@bridges.each do |k, v|
70
+ v[:age] += 1
71
+ end
72
+
73
+ reset_disco_timer nil, 5
74
+ @@disco_connection = send_discovery do |br|
75
+ if br.is_a?(NLHue::Bridge) && disco_started?
76
+ if @@bridges.include?(br.serial) && br.subscribed?
77
+ reset_disco_timer br
78
+ else
79
+ u = lookup_username(br, username)
80
+ br.username = u if u
81
+ br.update do |status, result|
82
+ if disco_started?
83
+ reset_disco_timer br
84
+ elsif !@@bridges.include?(br.serial)
85
+ # Ignore bridges if disco was shut down
86
+ br.clean
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ }
93
+
94
+ do_disco
95
+ end
96
+
97
+ # Stops periodic Hue bridge discovery and clears the list of
98
+ # bridges. Preserves disco callbacks.
99
+ def self.stop_discovery
100
+ @@disco_timer.cancel if @@disco_timer
101
+ @@disco_done_timer.cancel if @@disco_done_timer
102
+ @@disco_connection.shutdown if @@disco_connection
103
+
104
+ bridges = @@bridges.clone
105
+ bridges.each do |serial, info|
106
+ puts "Removing bridge #{serial} when stopping discovery" # XXX
107
+ @@bridges.delete serial
108
+ notify_callbacks :del, info[:bridge], 'Stopping Hue Bridge discovery.'
109
+ info[:bridge].clean
110
+ end
111
+ if @@disco_running
112
+ @@disco_running = false
113
+ notify_callbacks :end, !bridges.empty?
114
+ end
115
+
116
+ EM.next_tick do
117
+ @@disco_timer = nil
118
+ @@disco_done_timer = nil
119
+ @@disco_proc = nil
120
+ @@disco_connection = nil
121
+ end
122
+ end
123
+
124
+ # Indicates whether #start_discovery has been called.
125
+ def self.disco_started?
126
+ !!(@@disco_timer || @@disco_running)
127
+ end
128
+
129
+ # Adds the given block to be called with discovery events. The return
130
+ # value may be passed to remove_disco_callback.
131
+ #
132
+ # The callback will be called with the following events:
133
+ # :start - A discovery process has started.
134
+ # :add, bridge - The given bridge was recently discovered.
135
+ # :del, bridge, msg - The given bridge was recently removed
136
+ # because of [msg]. May be called even
137
+ # if no discovery process is running.
138
+ # :end, true/false - A discovery process has ended, and whether there
139
+ # were changes to the list of bridges.
140
+ def self.add_disco_callback cb=nil, &block
141
+ raise 'No callback was given.' unless block_given? || cb
142
+ raise 'Pass only a block or a Proc object, not both.' if block_given? && cb
143
+ @@disco_callbacks << (cb || block)
144
+ block
145
+ end
146
+
147
+ # Removes a discovery callback (call this with the return value from add_disco_callback)
148
+ def self.remove_disco_callback callback
149
+ @@disco_callbacks.delete callback
150
+ end
151
+
152
+ # Triggers a discovery process immediately (fails if #start_discovery
153
+ # has not been called), unless discovery is already in progress.
154
+ def self.do_disco
155
+ raise 'Call start_discovery() first.' unless @@disco_proc
156
+ return if @@disco_running
157
+
158
+ @@disco_timer.cancel if @@disco_timer
159
+ @@disco_proc.call if @@disco_proc
160
+ end
161
+
162
+ # Indicates whether a discovery process is currently running.
163
+ def self.disco_running?
164
+ @@disco_running
165
+ end
166
+
167
+ # Returns an array of the bridges previously discovered on the
168
+ # network.
169
+ def self.bridges
170
+ @@bridges.map do |serial, info|
171
+ info[:bridge]
172
+ end
173
+ end
174
+
175
+ # Returns a bridge with the given serial number, if present.
176
+ def self.get_bridge serial
177
+ serial &&= serial.downcase
178
+ @@bridges[serial] && @@bridges[serial][:bridge]
179
+ end
180
+
181
+ # Gets the number of consecutive times a bridge has been
182
+ # missing from discovery.
183
+ def self.get_missing_count serial
184
+ serial &&= serial.downcase
185
+ @@bridges[serial] && @@bridges[serial][:age]
186
+ end
187
+
188
+ # Gets the number of consecutive times a bridge has failed to
189
+ # update. This value is only updated if the Bridge object's
190
+ # subscribe or update methods are called.
191
+ def self.get_error_count serial
192
+ serial &&= serial.downcase
193
+ @@bridges[serial] && @@bridges[serial][:errcount]
194
+ end
195
+
196
+ # Sends an SSDP discovery request, then yields an NLHue::Bridge
197
+ # object for each Hue hub found on the network. Responses may
198
+ # come for more than timeout seconds after this function is
199
+ # called. Reuses Bridge objects from previously discovered
200
+ # bridges, if any. Returns the connection used by SSDP.
201
+ def self.send_discovery timeout=4, &block
202
+ # Even though we put 'upnp:rootdevice' in the ST: header, the
203
+ # Hue hub sends multiple matching and non-matching responses.
204
+ # We'll use a hash to track which IP addresses we've seen.
205
+ devs = {}
206
+
207
+ con = NLHue::SSDP.discover 'upnp:rootdevice', timeout do |ssdp|
208
+ if ssdp && ssdp['Location'].include?('description.xml') && ssdp['USN']
209
+ serial = ssdp['USN'].gsub(/.*([0-9A-Fa-f]{12}).*/, '\1')
210
+ unless devs.include?(ssdp.ip) && devs[ssdp.ip].serial == serial
211
+ dev = @@bridges.include?(serial) ?
212
+ @@bridges[serial][:bridge] :
213
+ Bridge.new(ssdp.ip, serial)
214
+
215
+ dev.addr = ssdp.ip unless dev.addr == ssdp.ip
216
+
217
+ if dev.verified?
218
+ yield dev
219
+ else
220
+ dev.verify do |result|
221
+ if result && !con.closed?
222
+ devs[ssdp.ip] = dev
223
+ begin
224
+ yield dev
225
+ rescue => e
226
+ log_e e, "Error notifying block with discovered bridge #{serial}"
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ con
236
+ end
237
+
238
+ private
239
+ # Calls each discovery callback with the given parameters.
240
+ def self.notify_callbacks *args
241
+ bench "Notify Hue disco callbacks: #{args[0].inspect}" do
242
+ @@disco_callbacks.each do |cb|
243
+ begin
244
+ cb.call *args
245
+ rescue => e
246
+ log_e e, "Error notifying Hue disco callback #{cb.inspect} about #{args[0]} event."
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ # Adds the given bridge to the list of bridges (or resets the
253
+ # timeout without adding a bridge if the bridge is nil). After
254
+ # timeout seconds, removes aged-out bridges from the internal
255
+ # list of bridges and notifies disco callbacks that discovery
256
+ # has ended. Cancels any previous timeout. This is called
257
+ # each time a new bridge is discovered so that discovery ends
258
+ # when no new bridges come in for timeout seconds.
259
+ def self.reset_disco_timer br, timeout=2
260
+ if br.is_a?(NLHue::Bridge)
261
+ if @@bridges[br.serial]
262
+ @@bridges[br.serial][:age] = 0
263
+ else
264
+ @@bridges[br.serial] = { :bridge => br, :age => 0, :errcount => 0 }
265
+ @@bridges = Hash[@@bridges.sort_by{|serial, info|
266
+ info[:bridge].registered? ? "000#{serial}" : serial
267
+ }]
268
+ @@bridges_changed = true
269
+
270
+ @@bridges[br.serial][:cb] = br.add_update_callback do |status, result|
271
+ if status
272
+ @@bridges[br.serial][:errcount] = 0
273
+ else
274
+ unless result.is_a? NLHue::NotRegisteredError
275
+ @@bridges[br.serial][:errcount] += 1
276
+
277
+ # Remove here instead of with *_AGE because
278
+ # *_AGE is only checked once per disco.
279
+ if @@bridges[br.serial][:errcount] > MAX_BRIDGE_ERR
280
+ info = @@bridges[br.serial]
281
+ @@bridges.delete br.serial
282
+ br.remove_update_callback info[:cb]
283
+ notify_bridge_removed br, 'Error updating bridge.'
284
+ EM.next_tick do
285
+ br.clean # next tick to ensure after notify*removed
286
+ do_disco
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ notify_bridge_added br
294
+ end
295
+ end
296
+
297
+ @@disco_done_timer.cancel if @@disco_done_timer
298
+ @@disco_done_timer = EM::Timer.new(timeout) do
299
+ update_bridges bridges
300
+ @@disco_done_timer = nil
301
+ @@disco_connection = nil
302
+
303
+ EM.next_tick do
304
+ @@disco_running = false
305
+ notify_callbacks :end, @@bridges_changed
306
+ @@disco_timer = EM::Timer.new(@@disco_interval, @@disco_proc) if @@disco_proc
307
+ @@bridges_changed = false
308
+ end
309
+ end
310
+ end
311
+
312
+ # Deletes aged-out bridges, sends disco :del events to
313
+ # callbacks. Called when the timer set by #reset_disco_timer
314
+ # expires.
315
+ def self.update_bridges bridges
316
+ bench 'update_bridges' do
317
+ @@bridges.select! do |k, br|
318
+ age_limit = br[:bridge].subscribed? ? MAX_SUBSCRIBED_AGE : MAX_BRIDGE_AGE
319
+
320
+ if br[:age] > age_limit
321
+ log "Bridge #{br[:bridge].serial} missing from #{br[:age]} rounds of discovery."
322
+ log "Bridge #{br[:bridge].serial} subscribed: #{br[:bridge].subscribed?}"
323
+
324
+ @@bridges_changed = true
325
+ notify_bridge_removed br[:bridge],
326
+ "Bridge missing from discovery #{br[:age]} times."
327
+
328
+ EM.next_tick do
329
+ br[:bridge].clean # next tick to ensure after notify*removed
330
+ end
331
+
332
+ false
333
+ else
334
+ true
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ # Notifies all disco callbacks that a bridge was added.
341
+ def self.notify_bridge_added br
342
+ EM.next_tick do
343
+ notify_callbacks :add, br
344
+ end
345
+ end
346
+
347
+ # Notifies all disco callbacks that a bridge was removed.
348
+ def self.notify_bridge_removed br, msg
349
+ EM.next_tick do
350
+ notify_callbacks :del, br, msg
351
+ end
352
+ end
353
+
354
+ # Finds a username in the given String or Hash of +usernames+
355
+ # that matches the given +bridge+ serial number (String or
356
+ # Symbol). Returns +usernames+ directly if it's a String.
357
+ def self.lookup_username(bridge, usernames)
358
+ return usernames if usernames.is_a?(String)
359
+ return (usernames[bridge.serial] || usernames[bridge.serial.to_sym]) if usernames.is_a?(Hash)
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,79 @@
1
+ # A class representing a group of lights on a Hue bridge.
2
+ # (C)2013-2015 Mike Bourgeous
3
+
4
+ module NLHue
5
+ # A class representing a designated group of lights on a Hue bridge.
6
+ # Recommended use is to call NLHue::Bridge#groups.
7
+ class Group < Target
8
+ # bridge - The Bridge that controls this light.
9
+ # id - The group's ID (integer >= 0).
10
+ # info - The Hash parsed from the JSON description of the group,
11
+ # if available. The group's lights will be unknown until the
12
+ # JSON from the bridge (/api/[username]/groups/[id]) is passed
13
+ # here or to #handle_json.
14
+ def initialize(bridge, id, info=nil)
15
+ @lights ||= Set.new
16
+
17
+ super bridge, id, info, :groups, 'action'
18
+ end
19
+
20
+ # Updates this Group with data parsed from the Hue bridge.
21
+ def handle_json(info)
22
+ super info
23
+
24
+ # A group returns no lights for a short time after creation
25
+ if @info['lights'].is_a?(Array) && !@info['lights'].empty?
26
+ @lights.replace @info['lights'].map(&:to_i)
27
+ end
28
+ end
29
+
30
+ # "Group: [ID]: [name] ([num] lights)"
31
+ def to_s
32
+ "Group: #{@id}: #{@name} (#{@lights.length} lights}"
33
+ end
34
+
35
+ # Returns an array containing this group's corresponding Light
36
+ # objects from the Bridge.
37
+ def lights
38
+ lights = @bridge.lights
39
+ @lights.map{|id| lights[id]}
40
+ end
41
+
42
+ # An array containing the IDs of the lights belonging to this
43
+ # group.
44
+ def light_ids
45
+ @lights.to_a
46
+ end
47
+
48
+ # Returns a Hash containing the group's info and most recently
49
+ # set state, with symbolized key names and hue scaled to 0..360.
50
+ # Example:
51
+ # {
52
+ # :id => 0,
53
+ # :name => 'Lightset 0',
54
+ # :type => 'LightGroup',
55
+ # :lights => [0, 1, 2],
56
+ # :on => false,
57
+ # :bri => 220,
58
+ # :ct => 500,
59
+ # :x => 0.5,
60
+ # :y => 0.5,
61
+ # :hue => 193.5,
62
+ # :sat => 255,
63
+ # :colormode => 'hs'
64
+ # }
65
+ def state
66
+ {lights: light_ids}.merge!(super)
67
+ end
68
+
69
+ # Recalls +scene+, which may be a Scene object or a scene ID
70
+ # String, on this group only. Any raw ID String will not be
71
+ # verified. If NLHue::Target#defer was called, the scene will
72
+ # not be recalled until #submit is called.
73
+ def recall_scene(scene)
74
+ scene = scene.id if scene.is_a?(Scene)
75
+ raise 'Scene ID to recall must be a String' unless scene.is_a?(String)
76
+ set('scene' => scene)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,20 @@
1
+ # A class representing a light known to a Hue bridge.
2
+ # (C)2013-2015 Mike Bourgeous
3
+
4
+ module NLHue
5
+ # A class representing a light known to a Hue bridge. Recommended use
6
+ # is to get a Light object by calling NLHue::Bridge#lights().
7
+ class Light < Target
8
+ # bridge - The Bridge that controls this light.
9
+ # id - The bulb number.
10
+ # info - Parsed Hash of the JSON light info object from the bridge.
11
+ def initialize(bridge, id, info=nil)
12
+ super bridge, id, info, :lights, 'state'
13
+ end
14
+
15
+ # "Light: [ID]: [name] ([type])"
16
+ def to_s
17
+ "Light: #{@id}: #{@name} (#{@type})"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,131 @@
1
+ # A class for managing HTTP requests to a host. Only allows one outstanding
2
+ # request to a given category, while allowing requests to unrelated categories.
3
+ # Categories default to path names.
4
+ # (C)2013 Mike Bourgeous
5
+
6
+ require 'eventmachine'
7
+ require 'em/protocols/httpclient'
8
+
9
+ module NLHue
10
+ class RequestQueue
11
+ attr_reader :host
12
+
13
+ # Initializes a request queue with the given host, default
14
+ # request timeout, and default POST/PUT content type.
15
+ def initialize host, timeout=5, content_type='application/json;charset=utf-8'
16
+ @host = host
17
+ @default_type = content_type
18
+ @default_timeout = timeout
19
+ @request_queue = {}
20
+ end
21
+
22
+ # Changes the host name used by requests.
23
+ def host= host
24
+ @host = host
25
+ end
26
+
27
+ # Makes a GET request to the given path, timing out after the
28
+ # given number of seconds, and calling the given block with a
29
+ # hash containing :content, :headers, and :status, or just
30
+ # false if there was an error. The default category is the
31
+ # path.
32
+ def get path, category=nil, timeout=nil, &block
33
+ # FIXME: Use em-http-request instead of HttpClient,
34
+ # which returns an empty :content field for /description.xml
35
+ request 'GET', path, category, nil, nil, timeout, &block
36
+ end
37
+
38
+ # Makes a POST request to the given path, with the given data
39
+ # and content type, timing out after the given number of
40
+ # seconds, and calling the given block with a hash containing
41
+ # :content, :headers, and :status, or just false if there was
42
+ # an error. The default category is the path.
43
+ def post path, data, category=nil, content_type=nil, timeout=nil, &block
44
+ request 'POST', path, category, data, content_type, timeout, &block
45
+ end
46
+
47
+ # Makes a PUT request to the given path, with the given data
48
+ # and content type, timing out after the given number of
49
+ # seconds, and calling the given block with a hash containing
50
+ # :content, :headers, and :status, or just false if there was
51
+ # an error.
52
+ def put path, data, category=nil, content_type=nil, timeout=nil, &block
53
+ request 'PUT', path, category, data, content_type, timeout, &block
54
+ end
55
+
56
+ # Makes a DELETE request to the given path, timing out after
57
+ # the given number of seconds, and calling the given block with
58
+ # a hash containing :content, :headers, and :status, or just
59
+ # false if there was an error.
60
+ def delete path, category=nil, timeout=nil, &block
61
+ request 'DELETE', path, category, nil, nil, timeout, &block
62
+ end
63
+
64
+ # Queues a request of the given type to the given path, using
65
+ # the given data and content type for e.g. PUT and POST. The
66
+ # request will time out after timeout seconds. The given block
67
+ # will be called with a hash containing :content, :headers, and
68
+ # :status if a response was received, or just false on error.
69
+ # This should be called from the EventMachine reactor thread.
70
+ def request verb, path, category=nil, data=nil, content_type=nil, timeout=nil, &block
71
+ raise 'A block must be given.' unless block_given?
72
+ raise 'Call from the EventMachine reactor thread.' unless EM.reactor_thread?
73
+
74
+ category ||= path
75
+ content_type ||= @default_type
76
+ timeout ||= @default_timeout
77
+
78
+ @request_queue[category] ||= []
79
+
80
+ req = [verb, path, category, data, content_type, timeout, block]
81
+ @request_queue[category] << req
82
+ do_next_request category if @request_queue[category].size == 1
83
+ end
84
+
85
+ private
86
+ # Sends a request with the given method/path/etc. Called by
87
+ # #do_next_request. See #request.
88
+ def do_request verb, path, category, data, content_type, timeout, &block
89
+ # FIXME: Use em-http-request
90
+ req = EM::P::HttpClient.request(
91
+ verb: verb,
92
+ host: @host,
93
+ request: path,
94
+ content: data,
95
+ contenttype: content_type,
96
+ )
97
+ req.callback {|response|
98
+ begin
99
+ yield response
100
+ rescue => e
101
+ log_e e, "Error calling a Hue bridge's request callback for #{category}."
102
+ end
103
+
104
+ @request_queue[category].shift
105
+ do_next_request category
106
+ }
107
+ req.errback {
108
+ req.close_connection # For timeout
109
+ begin
110
+ yield false
111
+ rescue => e
112
+ log_e e, "Error calling a Hue bridge's request callback with error for #{category}."
113
+ end
114
+
115
+ @request_queue[category].shift
116
+ do_next_request category
117
+ }
118
+ req.timeout timeout
119
+ end
120
+
121
+ # Shifts a request off the request queue (if it is not empty),
122
+ # then passes it to #do_request. See #request.
123
+ def do_next_request category
124
+ unless @request_queue[category].empty?
125
+ req = @request_queue[category].first
126
+ block = req.pop
127
+ do_request *req, &block
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,88 @@
1
+ # A class representing a preset scene on a Hue bridge.
2
+ # (C)2015 Mike Bourgeous
3
+
4
+ module NLHue
5
+ # Represents a preset scene on a Hue bridge, with a list of included
6
+ # lights. The actual values recalled by the scene are not available.
7
+ # TODO: Support creating new scenes
8
+ class Scene
9
+ attr_reader :id, :name, :bridge
10
+
11
+ # bridge - The Bridge that contains this scene.
12
+ # id - The scene's ID (a String).
13
+ # info - A Hash with scene info from the bridge.
14
+ def initialize(bridge, id, info)
15
+ @bridge = bridge
16
+ @id = id
17
+ @info = info
18
+ @lights = Set.new
19
+
20
+ handle_json(info)
21
+ end
22
+
23
+ # Updates this scene with any changes from the bridge.
24
+ def handle_json(info)
25
+ raise "Scene info must be a Hash" unless info.is_a?(Hash)
26
+
27
+ info['id'] = @id
28
+
29
+ @name = info['name'] || @name
30
+
31
+ if info['lights'].is_a?(Array) && !info['lights'].empty?
32
+ @lights.replace info['lights'].map(&:to_i)
33
+ end
34
+
35
+ @info = info
36
+ end
37
+
38
+ # Returns a copy of the last received info from the bridge, plus
39
+ # the scene's ID.
40
+ def to_h
41
+ @info.clone
42
+ end
43
+
44
+ # Returns a description of this scene.
45
+ def to_s
46
+ "Scene #{@id}: #{@name} (#{@lights.count} lights)"
47
+ end
48
+
49
+ # Returns a Hash containing basic information about the scene.
50
+ def state
51
+ {
52
+ id: @id,
53
+ name: @name,
54
+ lights: light_ids,
55
+ }
56
+ end
57
+
58
+ # Converts the Hash returned by #state to JSON.
59
+ def to_json(*args)
60
+ state.to_json(*args)
61
+ end
62
+
63
+ # Returns an array containing this scene's corresponding Light
64
+ # objects from the Bridge.
65
+ def lights
66
+ lights = @bridge.lights
67
+ @lights.map{|id| lights[id]}
68
+ end
69
+
70
+ # An array containing the IDs of the lights belonging to this
71
+ # scene.
72
+ def light_ids
73
+ @lights.to_a
74
+ end
75
+
76
+ # Recalls this scene on the next RequestQueue tick using group 0
77
+ # (all lights). The block, if given, will be called after the
78
+ # scene is recalled.
79
+ def recall(&block)
80
+ @bridge.add_target self, &block
81
+ end
82
+
83
+ # Recalls this scene immediately using NLHue::Bridge#put_api.
84
+ def send_changes(&block)
85
+ @bridge.put_api '/groups/0/action', {scene: @id}.to_json, :groups, &block
86
+ end
87
+ end
88
+ end