hue-lib 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -64,18 +64,19 @@ If you know the ID number of a particular lamp, you can access it directly.
64
64
  >> b.on?
65
65
  => true
66
66
 
67
- # settings
68
- >> b.settings
69
- => {"ct"=>343, "on"=>true, "bri"=>240}
70
-
67
+ # brightness
71
68
  >> b.brightness = 128
72
69
  => 128
73
70
 
74
- >> b.update hue: 45000, sat: 180
75
- => true
71
+ # color
72
+ >> b.color = Hue::Colors::ColorTemperature.new(6500)
73
+ => Temperature=6500°K (153 mired)
74
+
75
+ >> b.color = Hue::Colors::HueSaturation.new(10_000, 255)
76
+ => Hue=10000, Saturation=255
76
77
 
77
- >> b.settings
78
- => {"hue"=>45000, "sat"=>180, "on"=>true, "bri"=>128}
78
+ >> b.color = Hue::Colors::XY.new(0.5, 0.5)
79
+ => XY=[0.5, 0.5]
79
80
 
80
81
  # blinking
81
82
  >> b.blinking?
data/hue-lib.gemspec CHANGED
@@ -3,12 +3,13 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "hue-lib"
6
- s.version = '0.6.0'
6
+ s.version = '0.7.0'
7
7
  s.authors = ["Birkir A. Barkarson", "Aaron Hurley"]
8
8
  s.email = ["birkirb@stoicviking.net"]
9
9
  s.homepage = "https://github.com/birkirb/hue-lib"
10
- s.summary = %q{Ruby library for controlling Phillips Hue light bridge.}
11
- s.description = s.summary
10
+ s.summary = %q{Ruby library for controlling the Philips Hue system's lights and bridge.}
11
+ s.description = %q{Library allowing registration and invocation of a registered Philips Hue app.
12
+ Convinient objects allow executing commands on the bridge or individual bulbs.}
12
13
 
13
14
  s.rubyforge_project = "hue-lib"
14
15
 
@@ -0,0 +1,54 @@
1
+ module Hue
2
+ module Animations
3
+ module Candle
4
+
5
+ public
6
+
7
+ def candle(repeat = 15)
8
+ # 0-65536 for hue, 182 per deg. Ideal 30-60 deg (5460-10920)
9
+ stash!
10
+ on if off?
11
+
12
+ repeat.times do
13
+ hue = ((rand * 3460) + 5460).to_i
14
+ sat = rand(64) + 170
15
+ bri = rand(32) + 16
16
+
17
+ delay = (rand * 0.35) + (@delay ||= 0)
18
+ update(hue: hue, sat: sat, bri: bri, transitiontime: (delay * 10).to_i)
19
+ sleep delay
20
+ end
21
+ restore!
22
+ end
23
+
24
+ private
25
+
26
+ def options_with_colorstate
27
+ options.merge case state['colormode']
28
+ when 'ct'
29
+ {'ct' => state['ct']}
30
+ when 'xy'
31
+ {'xy' => state['xy']}
32
+ when 'hs'
33
+ {'hue' => state['hue'], 'sat' => state['sat']}
34
+ end.merge('on' => state['on'], 'bri' => state['bri'])
35
+ end
36
+
37
+ def restore!
38
+ if stash
39
+ update(@stash)
40
+ unstash!
41
+ end
42
+ end
43
+
44
+ def stash!
45
+ @stash ||= options_with_colorstate
46
+ end
47
+
48
+ def unstash!
49
+ @stash = nil
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,106 @@
1
+ module Hue
2
+ module Animations
3
+ module Sunrise
4
+
5
+ SUN_STEPS = [ 1.5, 2, 3, 1, 4, 2.5 ]
6
+ SUN_TIMES = [ 3, 3, 3, 1, 2, 1]
7
+
8
+ public
9
+
10
+ # Experimental Sunrise/Sunset action
11
+ # this will transition from off and warm light to on and daytime light
12
+ # in a curve that mimics the actual sunrise.
13
+
14
+ def perform_sunrise(total_time_in_minutes = 18)
15
+ # total_time / 18 steps == time_per_step
16
+ # the multiplier should be 600 * time per step
17
+ minutes_per_step = total_time_in_minutes / 18.0
18
+ multiplier = (minutes_per_step * 60 * 10).to_i
19
+
20
+ perform_sun_transition total_time_in_minutes, sunrise_steps(multiplier)
21
+ end
22
+
23
+ def perform_sunrise(total_time_in_minutes = 18)
24
+ multiplier = sunrise_multiplier total_time_in_minutes
25
+ steps = sunrise_steps(multiplier)
26
+ if on?
27
+ puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
28
+ while brightness >= steps[0][:bri]
29
+ steps.shift
30
+ end
31
+ end
32
+ steps.each_with_index do |step, i|
33
+ update step.merge(on: true)
34
+ sleep(step[:transitiontime] / 10.0)
35
+ end
36
+ end
37
+
38
+ def perform_sunset(total_time_in_minutes = 18)
39
+ multiplier = sunrise_multiplier total_time_in_minutes
40
+ steps = sunset_steps(multiplier)
41
+ if on?
42
+ puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
43
+ while brightness <= steps[0][:bri]
44
+ steps.shift
45
+ end
46
+ end
47
+ steps.each_with_index do |step, i|
48
+ update step.merge(on: true)
49
+ sleep(step[:transitiontime] / 10.0)
50
+ end
51
+ off
52
+ end
53
+
54
+ private
55
+
56
+
57
+ def sunrise_multiplier(total_time_in_minutes)
58
+ # total_time / 18 steps == time_per_step
59
+ # the multiplier should be 600 * time per step
60
+ minutes_per_step = total_time_in_minutes / 18.0
61
+ (minutes_per_step * 60 * 10).to_i
62
+ end
63
+
64
+ def sunrise_brightness
65
+ sun_bri_unit = 10
66
+ SUN_STEPS.inject([0]){|all, i| all << ((i * sun_bri_unit) + all[-1]).to_i } << 255
67
+ end
68
+
69
+ def sunrise_temps
70
+ sun_temp_unit = 16
71
+ SUN_STEPS.inject([500]){|all, i| all << (all[-1] - (i * sun_temp_unit)).to_i} << 200
72
+ end
73
+
74
+ def sunrise_times
75
+ [0, SUN_TIMES, 5].flatten
76
+ end
77
+
78
+ def sunset_times
79
+ [0, 5, SUN_TIMES.reverse].flatten
80
+ end
81
+
82
+ def sunrise_steps(multiplier = 600)
83
+ bri_steps = sunrise_brightness
84
+ tmp_steps = sunrise_temps
85
+
86
+ steps = []
87
+ sunrise_times.each_with_index do |t, i|
88
+ steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
89
+ end
90
+ steps
91
+ end
92
+
93
+ def sunset_steps(multiplier = 600)
94
+ bri_steps = sunrise_brightness.reverse
95
+ tmp_steps = sunrise_temps.reverse
96
+
97
+ steps = []
98
+ sunset_times.each_with_index do |t, i|
99
+ steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
100
+ end
101
+ steps
102
+ end
103
+
104
+ end
105
+ end
106
+ end
data/lib/hue/bridge.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'socket'
2
2
  require 'timeout'
3
+ require 'logger'
3
4
 
4
5
  module Hue
5
6
  class Bridge
@@ -73,50 +74,44 @@ module Hue
73
74
  end
74
75
 
75
76
  def index(url)
76
- request = Net::HTTP::Get.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
77
- parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
77
+ receive(Net::HTTP::Get, url)
78
78
  end
79
79
 
80
80
  def update(url, settings = {})
81
- request = Net::HTTP::Put.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
82
- request.body = settings.to_json
83
- parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
81
+ receive(Net::HTTP::Put, url, settings.to_json)
84
82
  end
85
83
 
86
84
  def delete(url, settings = {})
87
- request = Net::HTTP::Delete.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
88
- request.body = settings.to_json
89
- parse_and_check_response(Net::HTTP.new(url.host, url.port).start{ |http| http.request(request) })
85
+ receive(Net::HTTP::Delete, url, settings.to_json)
90
86
  end
91
87
 
92
88
  def create(url, settings = {})
93
- request = Net::HTTP::Post.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
94
- request.body = settings.to_json
95
- parse_and_check_response(Net::HTTP.new(url.host, url.port).start { |http| http.request(request) })
96
- end
89
+ receive(Net::HTTP::Post, url, settings.to_json)
90
+ end
91
+
92
+ def receive(request_class, url, payload = nil)
93
+ request = request_class.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
94
+ request.body = payload if payload
95
+ Hue.logger.info("Sending #{payload.to_s if payload} to #{url.to_s}")
96
+ response = nil
97
+ begin
98
+ response = Net::HTTP.new(url.host, url.port).start { |http| http.request(request) }
99
+ rescue => err
100
+ Hue.logger.error(err.message)
101
+ raise Hue::Error.new("Problem reaching bridge.", err)
102
+ end
97
103
 
98
- def parse_and_check_response(response)
99
- if display(response)
104
+ if response && response.code.to_s != '200'
105
+ Hue.logger.info("Error with response #{response.code} #{response.message}")
106
+ raise Hue::Error.new("Unexpected response: #{response.code}, #{response.message}")
107
+ else
100
108
  json = JSON.parse(response.body)
109
+ Hue.logger.info("Response #{response.code} #{response.message}: #{json}")
101
110
  if json.is_a?(Array) && error = json.first['error']
102
111
  raise Hue::API::Error.new(error)
103
112
  else
104
113
  json
105
114
  end
106
- else
107
- raise Hue::Error.new("Unexpected response: #{response.code}, #{response.message}")
108
- end
109
- end
110
-
111
- def display(response = nil)
112
- if response and response.code.to_s != '200'
113
- # Output to logger
114
- # puts "Response #{response.code} #{response.message}: #{JSON.parse(response.body).first}"
115
- false
116
- else
117
- # Output to logger
118
- # puts "Response #{response.code} #{response.message}: #{JSON.parse(response.body).first}"
119
- true
120
115
  end
121
116
  end
122
117
 
data/lib/hue/bulb.rb CHANGED
@@ -1,6 +1,14 @@
1
+ require_relative 'animations/candle'
2
+ require_relative 'animations/sunrise'
3
+
1
4
  module Hue
2
5
  class Bulb
3
6
 
7
+ BRIGHTNESS_MAX = 255
8
+
9
+ include Animations::Candle
10
+ include Animations::Sunrise
11
+
4
12
  public
5
13
 
6
14
  attr_reader :id, :bridge
@@ -63,58 +71,39 @@ module Hue
63
71
  alias :bri :brightness
64
72
 
65
73
  def brightness=(bri)
66
- update(bri: bri)
74
+ if scale = Hue.percent_to_unit_interval(bri)
75
+ update(bri: (scale * BRIGHTNESS_MAX).round)
76
+ else
77
+ update(bri: bri.to_i)
78
+ end
67
79
  brightness
68
80
  end
69
81
 
70
82
  alias :bri= :brightness=
71
83
 
72
- def hue
73
- self[:hue]
74
- end
75
-
76
- HUE_MAX = 65536.0
77
- HUE_DEGREES = 360
78
- HUE_SCALE = HUE_MAX / HUE_DEGREES
79
-
80
- def hue=(_hue)
81
- _hue = (_hue * HUE_SCALE).to_i
82
- update(hue: _hue)
83
- hue
84
+ def brightness_in_unit_interval
85
+ brightness / BRIGHTNESS_MAX.to_f
84
86
  end
85
87
 
86
- def saturation
87
- self[:sat]
88
+ def brightness_percent
89
+ (brightness_in_unit_interval * 100).round
88
90
  end
89
91
 
90
- alias :sat :saturation
91
-
92
- def saturation=(_sat)
93
- update(sat: _sat)
94
- sat
92
+ def color_mode
93
+ self[:colormode]
95
94
  end
96
95
 
97
- alias :sat= :saturation=
96
+ alias :colormode :color_mode
98
97
 
99
- def color_temperature
100
- self[:ct]
98
+ def color
99
+ set_color
101
100
  end
102
101
 
103
- alias :ct :color_temperature
104
-
105
- def color_temperature=(_ct)
106
- update(ct: [[_ct, 154].max, 500].min)
107
- colortemp
102
+ def color=(col)
103
+ update(col.to_hash)
104
+ set_color
108
105
  end
109
106
 
110
- alias :ct= :color_temperature=
111
-
112
- def color_mode
113
- self[:colormode]
114
- end
115
-
116
- alias :colormode :color_mode
117
-
118
107
  def blinking?
119
108
  !solid?
120
109
  end
@@ -146,312 +135,24 @@ module Hue
146
135
  self.options[:transitiontime] = (time * 10).to_i
147
136
  end
148
137
 
149
- protected
150
-
151
- def settings
152
- options.merge case state['colormode']
153
- when 'ct'
154
- {'ct' => state['ct']}
155
- when 'xy'
156
- {'xy' => state['xy']}
157
- when 'hs'
158
- {'hue' => state['hue'], 'sat' => state['sat']}
159
- end.merge('on' => state['on'], 'bri' => state['bri'])
160
- end
161
-
162
- def rgb
163
- send %(#{colormode}_to_rgb)
164
- end
165
-
166
- def red
167
- rgb[:red]
168
- end
169
-
170
- def green
171
- rgb[:green]
172
- end
173
-
174
- def blue
175
- rgb[:blue]
176
- end
177
-
178
- def red=(_red)
179
- self.rgb = [_red, green, blue]
180
- end
181
-
182
- def green=(_green)
183
- self.rgb = [red, _green, blue]
184
- end
185
-
186
- def blue=(_blue)
187
- self.rgb = [red, green, _blue]
188
- end
189
-
190
- def kelvin
191
- # convert colortemp setting to Kelvin
192
- 1000000 / self['ct']
193
- end
194
-
195
- def kelvin=(_temp)
196
- self.colortemp = 1000000 / [_temp, 1].max
197
- end
198
-
199
- def ct_to_rgb
200
- # using method described at
201
- # http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
202
- temp = kelvin / 100
203
-
204
- red = temp <= 66 ? 255 : 329.698727446 * ((temp - 60) ** -0.1332047592)
205
-
206
- green = if temp <= 66
207
- 99.4708025861 * Math.log(temp) - 161.1195681661
208
- else
209
- 288.1221695283 * ((temp - 60) ** -0.0755148492)
210
- end
211
-
212
- blue = if temp >= 66
213
- 255
214
- elsif temp <= 19
215
- 0
216
- else
217
- 138.5177312231 * Math.log(temp - 10) - 305.0447927307
218
- end
219
-
220
- { red: [[red, 0].max, 255].min.to_i,
221
- green: [[green, 0].max, 255].min.to_i,
222
- blue: [[blue, 0].max, 255].min.to_i
223
- }
224
-
225
- end
226
-
227
- def xyz
228
- vals = state['xy']
229
- vals + [1 - vals.first - vals.last]
230
- end
231
-
232
- def xy_to_rgb
233
- values = (RGB_MATRIX * Matrix[xyz].transpose).to_a.flatten.map{|x| [[x * 255, 0].max, 255].min.to_i}
234
- { red: values[0],
235
- green: values[1],
236
- blue: values[2]
237
- }
238
- end
239
-
240
- def hue_in_degrees
241
- self['hue'].to_f / HUE_SCALE
242
- end
243
-
244
- def hue_as_decimal
245
- hue_in_degrees / HUE_DEGREES
246
- end
247
-
248
- def sat_as_decimal
249
- self['sat'] / 255.0
250
- end
251
-
252
- def brightness_as_decimal
253
- brightness / 255.0
254
- end
255
-
256
- def hs_to_rgb
257
- h, s, v = hue_as_decimal, sat_as_decimal, brightness_as_decimal
258
- if s == 0 #monochromatic
259
- red = green = blue = v
260
- else
261
-
262
- v = 1.0 # We are setting the value to 1. Don't count brightness here
263
- i = (h * 6).floor
264
- f = h * 6 - i
265
- p = v * (1 - s)
266
- q = v * (1 - f * s)
267
- t = v * (1 - (1 - f) * s)
268
-
269
- case i % 6
270
- when 0
271
- red, green, blue = v, t, p
272
- when 1
273
- red, green, blue = q, v, p
274
- when 2
275
- red, green, blue = p, v, t
276
- when 3
277
- red, green, blue = p, q, v
278
- when 4
279
- red, green, blue = t, p, v
280
- when 5
281
- red, green, blue = v, p, q
282
- end
283
- end
284
-
285
- { red: [[red * 255, 0].max, 255].min.to_i,
286
- green: [[green * 255, 0].max, 255].min.to_i,
287
- blue: [[blue * 255, 0].max, 255].min.to_i
288
- }
289
- end
290
-
291
- def rgb=(colors)
292
- red, green, blue = colors[0] / 255.0, colors[1] / 255.0, colors[2] / 255.0
293
-
294
- max = [red, green, blue].max
295
- min = [red, green, blue].min
296
- h, s, l = 0, 0, ((max + min) / 2 * 255)
297
-
298
- d = max - min
299
- s = max == 0 ? 0 : (d / max * 255)
300
-
301
- h = case max
302
- when min
303
- 0 # monochromatic
304
- when red
305
- (green - blue) / d + (green < blue ? 6 : 0)
306
- when green
307
- (blue - red) / d + 2
308
- when blue
309
- (red - green) / d + 4
310
- end * 60 # / 6 * 360
311
-
312
- h = (h * HUE_SCALE).to_i
313
- update hue: h, sat: s.to_i#, bri: l.to_i
314
- [h, s, 1.0]
315
- end
316
-
317
- def candle(repeat = 15)
318
- # 0-65536 for hue, 182 per deg. Ideal 30-60 deg (5460-10920)
319
- stash!
320
- on if off?
321
-
322
- repeat.times do
323
- hue = ((rand * 3460) + 5460).to_i
324
- sat = rand(64) + 170
325
- bri = rand(32) + 16
326
-
327
- delay = (rand * 0.35) + (@delay ||= 0)
328
- update(hue: hue, sat: sat, bri: bri, transitiontime: (delay * 10).to_i)
329
- sleep delay
330
- end
331
- restore!
332
- end
333
-
334
138
  private
335
139
 
336
- def stash!
337
- @stash ||= settings
338
- end
339
-
340
- def restore!
341
- if stash
342
- update(@stash)
343
- unstash!
344
- end
345
- end
346
-
347
- def unstash!
348
- @stash = nil
349
- end
350
-
351
140
  def status
352
141
  @status || refresh!
353
142
  end
354
143
 
355
- def update(settings = {})
356
- if bridge.set_light_state(id, options.merge(settings))
357
- settings.each do |key, value|
358
- @status['state'][key.to_s] = value # or refresh!
359
- end
360
- end
144
+ def set_color
145
+ @color = Colors.parse_state(state)
361
146
  end
362
147
 
363
- # Experimental Sunrise/Sunset action
364
- # this will transition from off and warm light to on and daytime light
365
- # in a curve that mimics the actual sunrise.
366
-
367
- def perform_sunrise(total_time_in_minutes = 18)
368
- # total_time / 18 steps == time_per_step
369
- # the multiplier should be 600 * time per step
370
- minutes_per_step = total_time_in_minutes / 18.0
371
- multiplier = (minutes_per_step * 60 * 10).to_i
372
-
373
- perform_sun_transition total_time_in_minutes, sunrise_steps(multiplier)
374
- end
375
-
376
- def perform_sunrise(total_time_in_minutes = 18)
377
- multiplier = sunrise_multiplier total_time_in_minutes
378
- steps = sunrise_steps(multiplier)
379
- if on?
380
- puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
381
- while brightness >= steps[0][:bri]
382
- steps.shift
383
- end
384
- end
385
- steps.each_with_index do |step, i|
386
- update step.merge(on: true)
387
- sleep(step[:transitiontime] / 10.0)
388
- end
389
- end
390
-
391
- def perform_sunset(total_time_in_minutes = 18)
392
- multiplier = sunrise_multiplier total_time_in_minutes
393
- steps = sunset_steps(multiplier)
394
- if on?
395
- puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
396
- while brightness <= steps[0][:bri]
397
- steps.shift
148
+ def update(settings = {})
149
+ if bridge.set_light_state(id, options.merge(settings))
150
+ if @status
151
+ settings.each do |key, value|
152
+ @status['state'][key.to_s] = value # or refresh!
153
+ end
398
154
  end
399
155
  end
400
- steps.each_with_index do |step, i|
401
- update step.merge(on: true)
402
- sleep(step[:transitiontime] / 10.0)
403
- end
404
- off
405
- end
406
-
407
- SUN_STEPS = [ 1.5, 2, 3, 1, 4, 2.5 ]
408
- SUN_TIMES = [ 3, 3, 3, 1, 2, 1]
409
-
410
- def sunrise_multiplier(total_time_in_minutes)
411
- # total_time / 18 steps == time_per_step
412
- # the multiplier should be 600 * time per step
413
- minutes_per_step = total_time_in_minutes / 18.0
414
- (minutes_per_step * 60 * 10).to_i
415
- end
416
-
417
- def sunrise_brightness
418
- sun_bri_unit = 10
419
- SUN_STEPS.inject([0]){|all, i| all << ((i * sun_bri_unit) + all[-1]).to_i } << 255
420
- end
421
-
422
- def sunrise_temps
423
- sun_temp_unit = 16
424
- SUN_STEPS.inject([500]){|all, i| all << (all[-1] - (i * sun_temp_unit)).to_i} << 200
425
- end
426
-
427
- def sunrise_times
428
- [0, SUN_TIMES, 5].flatten
429
- end
430
-
431
- def sunset_times
432
- [0, 5, SUN_TIMES.reverse].flatten
433
- end
434
-
435
- def sunrise_steps(multiplier = 600)
436
- bri_steps = sunrise_brightness
437
- tmp_steps = sunrise_temps
438
-
439
- steps = []
440
- sunrise_times.each_with_index do |t, i|
441
- steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
442
- end
443
- steps
444
- end
445
-
446
- def sunset_steps(multiplier = 600)
447
- bri_steps = sunrise_brightness.reverse
448
- tmp_steps = sunrise_temps.reverse
449
-
450
- steps = []
451
- sunset_times.each_with_index do |t, i|
452
- steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
453
- end
454
- steps
455
156
  end
456
157
 
457
158
  end