hue-lib 0.6.0 → 0.7.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/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