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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +91 -0
- data/Rakefile +1 -0
- data/bin/console +12 -0
- data/bin/setup +7 -0
- data/lib/nlhue/bridge.rb +830 -0
- data/lib/nlhue/disco.rb +362 -0
- data/lib/nlhue/group.rb +79 -0
- data/lib/nlhue/light.rb +20 -0
- data/lib/nlhue/request_queue.rb +131 -0
- data/lib/nlhue/scene.rb +88 -0
- data/lib/nlhue/ssdp.rb +106 -0
- data/lib/nlhue/target.rb +411 -0
- data/lib/nlhue/version.rb +3 -0
- data/lib/nlhue.rb +36 -0
- data/nlhue.gemspec +28 -0
- metadata +148 -0
data/lib/nlhue/ssdp.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Barebones asynchronous SSDP device discovery using EventMachine.
|
|
2
|
+
# Part of Nitrogen Logic's Ruby interface library for the Philips Hue.
|
|
3
|
+
# (C)2012 Mike Bourgeous
|
|
4
|
+
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'eventmachine'
|
|
7
|
+
|
|
8
|
+
module NLHue
|
|
9
|
+
module SSDP
|
|
10
|
+
SSDP_ADDR = '239.255.255.250'
|
|
11
|
+
SSDP_PORT = 1900
|
|
12
|
+
|
|
13
|
+
# TODO: Support passive discovery of other devices' announcements
|
|
14
|
+
|
|
15
|
+
# Eventually calls the given block with a NLHue::SSDP::Response
|
|
16
|
+
# for each matching device found on the network within timeout
|
|
17
|
+
# seconds. The block will be called with nil after the
|
|
18
|
+
# timeout. Returns the connection used for discovery; call
|
|
19
|
+
# #shutdown on the returned object to abort discovery.
|
|
20
|
+
def self.discover type='ssdp:all', timeout=5, &block
|
|
21
|
+
raise 'A block must be given to discover().' unless block_given?
|
|
22
|
+
|
|
23
|
+
con = EM::open_datagram_socket('0.0.0.0', 0, SSDPConnection, type, timeout, block)
|
|
24
|
+
EM.add_timer(timeout) do
|
|
25
|
+
con.close_connection
|
|
26
|
+
EM.next_tick do
|
|
27
|
+
yield nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# TODO: Structure this using EM::Deferrable instead?
|
|
32
|
+
con
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The HTTP response representing a service discovered by SSDP.
|
|
36
|
+
class Response
|
|
37
|
+
attr_reader :ip, :response, :headers
|
|
38
|
+
|
|
39
|
+
def initialize ip, response
|
|
40
|
+
@ip = ip
|
|
41
|
+
@response = response
|
|
42
|
+
@headers = {}
|
|
43
|
+
|
|
44
|
+
response.split(/\r?\n\r?\n/, 2)[0].lines.each do |line|
|
|
45
|
+
if line.include? ':'
|
|
46
|
+
key, value = line.split(/: ?/, 2)
|
|
47
|
+
@headers[key.downcase] = value.strip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
"#{@ip}:\n\t#{@response.lines.to_a.join("\t")}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Retrieves the value of a header, with case
|
|
57
|
+
# insensitive matching.
|
|
58
|
+
def [] header
|
|
59
|
+
@headers[header.downcase]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
# UDP connection used for SSDP by discover().
|
|
65
|
+
class SSDPConnection < EM::Connection
|
|
66
|
+
# type - the SSDP service type (used in the ST: field)
|
|
67
|
+
# timeout - the number of seconds to wait for responses (used in the MX: field)
|
|
68
|
+
# receiver is the block passed to discover().
|
|
69
|
+
def initialize type, timeout, receiver
|
|
70
|
+
super
|
|
71
|
+
@type = type
|
|
72
|
+
@timeout = timeout
|
|
73
|
+
@receiver = receiver
|
|
74
|
+
@msg = "M-SEARCH * HTTP/1.1\r\n" +
|
|
75
|
+
"HOST: #{SSDP_ADDR}:#{SSDP_PORT}\r\n" +
|
|
76
|
+
"MAN: ssdp:discover\r\n" +
|
|
77
|
+
"MX: #{timeout.to_i}\r\n" +
|
|
78
|
+
"ST: #{type}\r\n" +
|
|
79
|
+
"\r\n"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def post_init
|
|
83
|
+
send_datagram @msg, SSDP_ADDR, SSDP_PORT
|
|
84
|
+
EM.add_timer(0.5) do
|
|
85
|
+
send_datagram @msg, SSDP_ADDR, SSDP_PORT
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def receive_data data
|
|
90
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
|
91
|
+
@receiver.call Response.new(ip, data) if @receiver
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Closes the UDP socket and ignores any future SSDP messages.
|
|
95
|
+
def shutdown
|
|
96
|
+
@receiver = nil
|
|
97
|
+
close_connection
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Indicates whether shutdown has been called.
|
|
101
|
+
def closed?
|
|
102
|
+
@receiver.nil?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/nlhue/target.rb
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Base class representing a light or group known to a Hue bridge.
|
|
2
|
+
# (C)2015 Mike Bourgeous
|
|
3
|
+
|
|
4
|
+
module NLHue
|
|
5
|
+
# Base class representing a light or group known to a Hue bridge. See
|
|
6
|
+
# NLHue::Light and NLHue::Group.
|
|
7
|
+
class Target
|
|
8
|
+
attr_reader :id, :type, :name, :bridge, :transitiontime
|
|
9
|
+
|
|
10
|
+
# bridge - The Bridge that controls this light or group.
|
|
11
|
+
# id - The light or group's ID (>=0 for groups, >=1 for lights).
|
|
12
|
+
# info - Parsed Hash of the JSON light or group info object from the bridge.
|
|
13
|
+
# api_category - The category to pass to the bridge for rate limiting API
|
|
14
|
+
# requests. Also forms part of the API URL.
|
|
15
|
+
# api_target - @api_target for lights, @api_target for groups
|
|
16
|
+
def initialize(bridge, id, info, api_category, api_target)
|
|
17
|
+
@bridge = bridge
|
|
18
|
+
@id = id.to_i
|
|
19
|
+
@api_category = api_category
|
|
20
|
+
@api_target = api_target
|
|
21
|
+
|
|
22
|
+
@changes = Set.new
|
|
23
|
+
@defer = false
|
|
24
|
+
|
|
25
|
+
@info = {api_target => {}}
|
|
26
|
+
handle_json(info || {})
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Updates this light or group object using a Hash parsed from
|
|
30
|
+
# the JSON info from the Hue bridge.
|
|
31
|
+
def handle_json(info)
|
|
32
|
+
raise "Light/group info must be a Hash, not #{info.class}." unless info.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
# A group contains no 'xy' for a short time after creation.
|
|
35
|
+
# Add fake xy color for lamps that don't support color.
|
|
36
|
+
info[@api_target] = {} unless info[@api_target].is_a?(Hash)
|
|
37
|
+
info[@api_target]['xy'] ||= [0.33333, 0.33333]
|
|
38
|
+
|
|
39
|
+
info['id'] = @id
|
|
40
|
+
|
|
41
|
+
# Preserve deferred changes that have not yet been sent to the bridge
|
|
42
|
+
@changes.each do |key|
|
|
43
|
+
info[@api_target][key] = @info[@api_target][key]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@info = info
|
|
47
|
+
@type = @info['type']
|
|
48
|
+
@name = @info['name'] || @name || "Lightset #{@id}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Gets the current state of this light or group from the
|
|
52
|
+
# bridge. The block, if given, will be called with true and
|
|
53
|
+
# the response on success, or false and an Exception on error.
|
|
54
|
+
def update(&block)
|
|
55
|
+
tx = rand
|
|
56
|
+
@bridge.get_api "/#{@api_category}/#{@id}", @api_category do |response|
|
|
57
|
+
puts "#{tx} Target #{@id} update response: #{response}" # XXX
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
status, result = @bridge.check_json(response)
|
|
61
|
+
handle_json(result) if status
|
|
62
|
+
rescue => e
|
|
63
|
+
status = false
|
|
64
|
+
result = e
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
yield status, result if block_given?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns a copy of the hash representing the light or group's
|
|
72
|
+
# state as parsed from the JSON returned by the bridge, without
|
|
73
|
+
# any range scaling (e.g. so hue range is 0..65535).
|
|
74
|
+
def to_h
|
|
75
|
+
@info.clone
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Converts the Hash returned by #state to JSON.
|
|
79
|
+
def to_json(*args)
|
|
80
|
+
state.to_json(*args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Call to queue changes to be sent all at once. Updates will
|
|
84
|
+
# not be sent to the light or group until #submit is called.
|
|
85
|
+
# Call #nodefer to stop deferring changes.
|
|
86
|
+
def defer
|
|
87
|
+
@defer = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Stops deferring changes and sends any queued changes
|
|
91
|
+
# immediately.
|
|
92
|
+
def nodefer
|
|
93
|
+
@defer = false
|
|
94
|
+
set {}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Sets the transition time in centiseconds used for the next
|
|
98
|
+
# immediate parameter change or deferred batch parameter
|
|
99
|
+
# change. The transition time will be reset when send_changes
|
|
100
|
+
# is called. Call with nil to clear the transition time.
|
|
101
|
+
def transitiontime=(time)
|
|
102
|
+
if time.nil?
|
|
103
|
+
@transitiontime = nil
|
|
104
|
+
else
|
|
105
|
+
time = 0 if time < 0
|
|
106
|
+
@transitiontime = time.to_i
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Tells the Bridge object that this Light or Group is ready to
|
|
111
|
+
# have its deferred data sent. The NLHue::Bridge will schedule
|
|
112
|
+
# a rate-limited call to #send_changes, which sends all changes
|
|
113
|
+
# queued since the last call to defer. The block, if given,
|
|
114
|
+
# will be called with true and the response on success, or
|
|
115
|
+
# false and an Exception on error. The transition time sent to
|
|
116
|
+
# the bridge can be controlled with transitiontime=. If no
|
|
117
|
+
# transition time is set, the default transition time will be
|
|
118
|
+
# used by the bridge.
|
|
119
|
+
def submit(&block)
|
|
120
|
+
puts "Submitting changes to #{self}" # XXX
|
|
121
|
+
@bridge.add_target self, &block
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns a Hash containing the light's info and current state,
|
|
125
|
+
# with symbolized key names and hue scaled to 0..360. Example:
|
|
126
|
+
# {
|
|
127
|
+
# :id => 1,
|
|
128
|
+
# :name => 'Hue Lamp 2',
|
|
129
|
+
# :type => 'Extended color light',
|
|
130
|
+
# :on => false,
|
|
131
|
+
# :bri => 220,
|
|
132
|
+
# :ct => 500,
|
|
133
|
+
# :x => 0.5,
|
|
134
|
+
# :y => 0.5,
|
|
135
|
+
# :hue => 193.5,
|
|
136
|
+
# :sat => 255,
|
|
137
|
+
# :colormode => 'hs'
|
|
138
|
+
# }
|
|
139
|
+
def state
|
|
140
|
+
{
|
|
141
|
+
:id => id,
|
|
142
|
+
:name => name,
|
|
143
|
+
:type => type,
|
|
144
|
+
:on => on?,
|
|
145
|
+
:bri => bri,
|
|
146
|
+
:ct => ct,
|
|
147
|
+
:x => x,
|
|
148
|
+
:y => y,
|
|
149
|
+
:hue => hue,
|
|
150
|
+
:sat => sat,
|
|
151
|
+
:colormode => colormode
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Tells the light or group to flash once if repeat is false, or
|
|
156
|
+
# several times if repeat is true. Sets the 'alert' property.
|
|
157
|
+
def alert!(repeat=false)
|
|
158
|
+
set({ 'alert' => repeat ? 'select' : 'lselect' })
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Stops any existing flashing of the light or group.
|
|
162
|
+
def clear_alert
|
|
163
|
+
set({ 'alert' => 'none' })
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Sets the light or group's alert status to the given string
|
|
167
|
+
# (one of 'select' (flash once), 'lselect' (flash several
|
|
168
|
+
# times), or 'none' (stop flashing)). Any other value may
|
|
169
|
+
# result in an error from the bridge.
|
|
170
|
+
def alert=(alert)
|
|
171
|
+
set({ 'alert' => alert })
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Returns the current alert state of the light or group (or the
|
|
175
|
+
# stored state if defer() was called, but send() has not yet
|
|
176
|
+
# been called). Groups are not updated when their constituent
|
|
177
|
+
# lights are changed individually.
|
|
178
|
+
def alert
|
|
179
|
+
(@info[@api_target] || @info[@api_target])['alert']
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Sets the on/off state of this light or group (true or false).
|
|
183
|
+
# Lights must be on before other parameters can be changed.
|
|
184
|
+
def on=(on)
|
|
185
|
+
set({ 'on' => !!on })
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The light state most recently set with on=, #on! or #off!, or
|
|
189
|
+
# the last light state received from the bridge due to calling
|
|
190
|
+
# #update on the light/group or on the NLHue::Bridge.
|
|
191
|
+
def on?
|
|
192
|
+
@info[@api_target]['on']
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Turns the light or group on.
|
|
196
|
+
def on!
|
|
197
|
+
self.on = true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Turns the light or group off.
|
|
201
|
+
def off!
|
|
202
|
+
self.on = false
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Sets the brightness of this light or group (0-255 inclusive).
|
|
206
|
+
# Note that a brightness of 0 is not off. The light(s) must
|
|
207
|
+
# already be switched on for this to work, if not deferred.
|
|
208
|
+
def bri=(bri)
|
|
209
|
+
bri = 0 if bri < 0
|
|
210
|
+
bri = 255 if bri > 255
|
|
211
|
+
|
|
212
|
+
set({ 'bri' => bri.to_i })
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The brightness most recently set with #bri=, or the last
|
|
216
|
+
# brightness received from the light or group due to calling
|
|
217
|
+
# #update on the target or on the bridge.
|
|
218
|
+
def bri
|
|
219
|
+
# TODO: Field storing @api_target or @api_target
|
|
220
|
+
@info[@api_target]['bri'].to_i
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Switches the light or group into color temperature mode and
|
|
224
|
+
# sets the color temperature of the light in mireds (154-500
|
|
225
|
+
# inclusive, where 154 is highest temperature (bluer), 500 is
|
|
226
|
+
# lowest temperature (yellower)). The light(s) must be on for
|
|
227
|
+
# this to work, if not deferred.
|
|
228
|
+
def ct=(ct)
|
|
229
|
+
ct = 154 if ct < 154
|
|
230
|
+
ct = 500 if ct > 500
|
|
231
|
+
|
|
232
|
+
set({ 'ct' => ct.to_i, 'colormode' => 'ct' })
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The color temperature most recently set with ct=, or the last
|
|
236
|
+
# color temperature received from the light due to calling
|
|
237
|
+
# #update on the light or on the bridge.
|
|
238
|
+
def ct
|
|
239
|
+
@info[@api_target]['ct'].to_i
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Switches the light or group into CIE XYZ color mode and sets
|
|
243
|
+
# the X color coordinate to the given floating point value
|
|
244
|
+
# between 0 and 1, inclusive. Lights must be on for this to
|
|
245
|
+
# work.
|
|
246
|
+
def x=(x)
|
|
247
|
+
self.xy = [ x, @info[@api_target]['xy'][1] ]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The X color coordinate most recently set with #x= or #xy=, or
|
|
251
|
+
# the last X color coordinate received from the light or group.
|
|
252
|
+
def x
|
|
253
|
+
@info[@api_target]['xy'][0].to_f
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Switches the light or group into CIE XYZ color mode and sets
|
|
257
|
+
# the Y color coordinate to the given floating point value
|
|
258
|
+
# between 0 and 1, inclusive. Lights must be on for this to
|
|
259
|
+
# work.
|
|
260
|
+
def y=(y)
|
|
261
|
+
self.xy = [ @info[@api_target]['xy'][0], y ]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The Y color coordinate most recently set with #y= or #xy=, or
|
|
265
|
+
# the last Y color coordinate received from the light or group.
|
|
266
|
+
def y
|
|
267
|
+
@info[@api_target]['xy'][1].to_f
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Switches the light or group into CIE XYZ color mode and sets
|
|
271
|
+
# the XY color coordinates to the given two-element array of
|
|
272
|
+
# floating point values between 0 and 1, inclusive. Lights
|
|
273
|
+
# must be on for this to work, if not deferred.
|
|
274
|
+
def xy=(xy)
|
|
275
|
+
unless xy.is_a?(Array) && xy.length == 2 && xy[0].is_a?(Numeric) && xy[1].is_a?(Numeric)
|
|
276
|
+
raise 'Pass a two-element array of numbers to xy=.'
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
xy[0] = 0 if xy[0] < 0
|
|
280
|
+
xy[0] = 1 if xy[0] > 1
|
|
281
|
+
xy[1] = 0 if xy[1] < 0
|
|
282
|
+
xy[1] = 1 if xy[1] > 1
|
|
283
|
+
|
|
284
|
+
set({ 'xy' => xy, 'colormode' => 'xy' })
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The XY color coordinates most recently set with #x=, #y=, or
|
|
288
|
+
# #xy=, or the last color coordinates received from the light
|
|
289
|
+
# or group due to calling #update on the target or the bridge.
|
|
290
|
+
def xy
|
|
291
|
+
xy = @info[@api_target]['xy']
|
|
292
|
+
[ xy[0].to_f, xy[1].to_f ]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Switches the light or group into hue/saturation mode and sets
|
|
296
|
+
# the hue to the given value (floating point degrees, wrapped
|
|
297
|
+
# to 0-360). The light(s) must already be on for this to work.
|
|
298
|
+
def hue=(hue)
|
|
299
|
+
hue = (hue * 65536 / 360).to_i & 65535
|
|
300
|
+
set({ 'hue' => hue, 'colormode' => 'hs' })
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# The hue most recently set with #hue=, or the last hue
|
|
304
|
+
# received from the light or group due to calling #update on
|
|
305
|
+
# the target or on the bridge.
|
|
306
|
+
def hue
|
|
307
|
+
@info[@api_target]['hue'].to_i * 360 / 65536.0
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Switches the light into hue/saturation mode and sets the
|
|
311
|
+
# light's saturation to the given value (0-255 inclusive).
|
|
312
|
+
def sat=(sat)
|
|
313
|
+
sat = 0 if sat < 0
|
|
314
|
+
sat = 255 if sat > 255
|
|
315
|
+
|
|
316
|
+
set({ 'sat' => sat.to_i, 'colormode' => 'hs' })
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# The saturation most recently set with #saturation=, or the
|
|
320
|
+
# last saturation received from the light due to calling
|
|
321
|
+
# #update on the light or on the bridge.
|
|
322
|
+
def sat
|
|
323
|
+
@info[@api_target]['sat'].to_i
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Sets the light or group's special effect mode (either 'none'
|
|
327
|
+
# or 'colorloop').
|
|
328
|
+
def effect= effect
|
|
329
|
+
effect = 'none' unless effect == 'colorloop'
|
|
330
|
+
set({ 'effect' => effect })
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# The light or group's last set special effect mode.
|
|
334
|
+
def effect
|
|
335
|
+
@info[@api_target]['effect']
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Returns the light or group's current or last set color mode
|
|
339
|
+
# ('ct' for color temperature, 'hs' for hue/saturation, 'xy'
|
|
340
|
+
# for CIE XYZ).
|
|
341
|
+
def colormode
|
|
342
|
+
@info[@api_target]['colormode']
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Sends parameters named in @changes to the bridge. The block,
|
|
346
|
+
# if given, will be called with true and the response, or false
|
|
347
|
+
# and an Exception. This should only be called internally or
|
|
348
|
+
# by the NLHue::Bridge.
|
|
349
|
+
def send_changes(&block)
|
|
350
|
+
msg = {}
|
|
351
|
+
|
|
352
|
+
@changes.each do |param|
|
|
353
|
+
case param
|
|
354
|
+
when 'colormode'
|
|
355
|
+
case @info[@api_target]['colormode']
|
|
356
|
+
when 'hs'
|
|
357
|
+
msg['hue'] = @info[@api_target]['hue'] if @changes.include? 'hue'
|
|
358
|
+
msg['sat'] = @info[@api_target]['sat'] if @changes.include? 'sat'
|
|
359
|
+
when 'xy'
|
|
360
|
+
msg['xy'] = @info[@api_target]['xy']
|
|
361
|
+
when 'ct'
|
|
362
|
+
msg['ct'] = @info[@api_target]['ct']
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
when 'bri', 'on', 'alert', 'effect', 'scene'
|
|
366
|
+
msg[param] = @info[@api_target][param]
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
msg['transitiontime'] = @transitiontime if @transitiontime
|
|
371
|
+
|
|
372
|
+
put_target(msg) do |status, result|
|
|
373
|
+
rmsg = result.to_s
|
|
374
|
+
# TODO: Parse individual parameters' error messages? Example:
|
|
375
|
+
# [{"error":{"type":6,"address":"/lights/2/state/zfhue","description":"parameter, zfhue, not available"}},{"success":{"/lights/2/state/transitiontime":0}}]
|
|
376
|
+
@changes.delete('alert') if rmsg.include? 'Device is set to off'
|
|
377
|
+
@changes.clear if status || rmsg =~ /(invalid value|not available)/
|
|
378
|
+
yield status, result if block_given?
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
@transitiontime = nil
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
# Sets one or more parameters on the local light, then sends
|
|
386
|
+
# them to the bridge (unless defer was called).
|
|
387
|
+
def set(params)
|
|
388
|
+
params.each do |k, v|
|
|
389
|
+
@changes << k
|
|
390
|
+
@info[@api_target][k] = v
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
send_changes unless @defer
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# PUTs the given Hash or Array, converted to JSON, to this
|
|
397
|
+
# light or group's API endpoint. The given block will be
|
|
398
|
+
# called as described for NLHue::Bridge#put_api().
|
|
399
|
+
def put_target(msg, &block)
|
|
400
|
+
unless msg.is_a?(Hash) || msg.is_a?(Array)
|
|
401
|
+
raise "Message to PUT must be a Hash or an Array, not #{msg.class.inspect}."
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
api_path = "/#{@api_category}/#{@id}/#{@api_target}"
|
|
405
|
+
@bridge.put_api api_path, msg.to_json, @api_category do |response|
|
|
406
|
+
status, result = @bridge.check_json response
|
|
407
|
+
yield status, result if block_given?
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
data/lib/nlhue.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Nitrogen Logic's Ruby interface library for the Philips Hue.
|
|
2
|
+
# (C)2013 Mike Bourgeous
|
|
3
|
+
|
|
4
|
+
# Dummy benchmarking method that may be overridden by library users.
|
|
5
|
+
unless private_methods.include?(:bench)
|
|
6
|
+
def bench label, *args, &block
|
|
7
|
+
yield
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Logging method that may be overridden by library users.
|
|
12
|
+
unless private_methods.include?(:log)
|
|
13
|
+
def log msg
|
|
14
|
+
puts msg
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Exception logging method that may be overridden by library users.
|
|
19
|
+
unless private_methods.include?(:log_e)
|
|
20
|
+
def log_e e, msg=nil
|
|
21
|
+
e ||= StandardError.new('No exception given to log')
|
|
22
|
+
if msg
|
|
23
|
+
puts "#{msg}: #{e}", e.backtrace
|
|
24
|
+
else
|
|
25
|
+
puts e, e.backtrace
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative 'nlhue/ssdp'
|
|
31
|
+
require_relative 'nlhue/disco'
|
|
32
|
+
require_relative 'nlhue/bridge'
|
|
33
|
+
require_relative 'nlhue/target'
|
|
34
|
+
require_relative 'nlhue/light'
|
|
35
|
+
require_relative 'nlhue/group'
|
|
36
|
+
require_relative 'nlhue/scene'
|
data/nlhue.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'nlhue/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "nlhue"
|
|
8
|
+
spec.version = NLHue::VERSION
|
|
9
|
+
spec.authors = ["Mike Bourgeous"]
|
|
10
|
+
spec.email = ["mike@nitrogenlogic.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = %q{An EventMachine-based library for interfacing with the Philips Hue lighting system.}
|
|
13
|
+
spec.description = spec.summary
|
|
14
|
+
spec.homepage = "https://github.com/nitrogenlogic/nlhue"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
17
|
+
spec.bindir = "exe"
|
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
23
|
+
spec.add_development_dependency 'pry'
|
|
24
|
+
spec.add_development_dependency 'pry-byebug'
|
|
25
|
+
|
|
26
|
+
spec.add_runtime_dependency 'eventmachine', '~> 1.0'
|
|
27
|
+
spec.add_runtime_dependency 'UPnP', '~> 1.2'
|
|
28
|
+
end
|