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.
@@ -0,0 +1,35 @@
1
+ module Hue
2
+ module Colors
3
+ class Color
4
+
5
+ ERROR_METHOD_NOT_IMPLEMENTED = 'method-not-implemented'
6
+
7
+ def self.ranged(min, val, max)
8
+ [[min, val].max, max].min
9
+ end
10
+
11
+ public
12
+
13
+ def to_hash
14
+ raise ERROR_METHOD_NOT_IMPLEMENTED
15
+ end
16
+
17
+ def to_s
18
+ raise ERROR_METHOD_NOT_IMPLEMENTED
19
+ end
20
+
21
+ def to_rgb
22
+ raise ERROR_METHOD_NOT_IMPLEMENTED
23
+ end
24
+
25
+ protected
26
+
27
+
28
+ def ranged(min, val, max)
29
+ # For convinence and polymorphism
30
+ self.class.ranged(min, val, max)
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,85 @@
1
+ # Encoding: UTF-8
2
+
3
+ module Hue
4
+ module Colors
5
+ class ColorTemperature < Color
6
+
7
+ MEGA = 1e6
8
+ KELVIN_MIN = 2000
9
+ KELVIN_MAX = 6500
10
+ MIRED_MIN = 153
11
+ MIRED_MAX = 500
12
+
13
+ public
14
+
15
+ def initialize(temperature)
16
+ if scale = Hue.percent_to_unit_interval(temperature)
17
+ self.mired = unit_to_mired_interval(scale)
18
+ else
19
+ # Assume an integer value
20
+ temperature = temperature.to_i
21
+ if temperature >= KELVIN_MIN
22
+ self.kelvin = temperature
23
+ else
24
+ self.mired = temperature
25
+ end
26
+ end
27
+ end
28
+
29
+ def mired
30
+ @mired.floor
31
+ end
32
+
33
+ def mired=(t)
34
+ @mired = ranged(MIRED_MIN, t.to_f, MIRED_MAX)
35
+ end
36
+
37
+ def kelvin
38
+ ranged(KELVIN_MIN, MEGA / @mired, KELVIN_MAX).round
39
+ end
40
+
41
+ def kelvin=(t)
42
+ self.mired = (MEGA / ranged(KELVIN_MIN, t.to_f, KELVIN_MAX))
43
+ end
44
+
45
+ def to_hash
46
+ {ct: mired}
47
+ end
48
+
49
+ def to_s
50
+ "Temperature=#{self.kelvin.to_i}°K (#{self.mired} mired)"
51
+ end
52
+
53
+ def to_rgb
54
+ # using method described at
55
+ # http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
56
+ temp = kelvin / 100
57
+
58
+ red = temp <= 66 ? 255 : 329.698727446 * ((temp - 60) ** -0.1332047592)
59
+
60
+ green = if temp <= 66
61
+ 99.4708025861 * Math.log(temp) - 161.1195681661
62
+ else
63
+ 288.1221695283 * ((temp - 60) ** -0.0755148492)
64
+ end
65
+
66
+ blue = if temp >= 66
67
+ 255
68
+ elsif temp <= 19
69
+ 0
70
+ else
71
+ 138.5177312231 * Math.log(temp - 10) - 305.0447927307
72
+ end
73
+
74
+ RGB.new(red, green, blue)
75
+ end
76
+
77
+ private
78
+
79
+ def unit_to_mired_interval(unit_interval)
80
+ unit_interval * (MIRED_MAX - MIRED_MIN) + MIRED_MIN
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,104 @@
1
+ module Hue
2
+ module Colors
3
+ class HueSaturation < Color
4
+
5
+ HUE_MIN = 0
6
+ HUE_MAX = 65536.0
7
+ HUE_DEGREES = 360
8
+ HUE_SCALE = HUE_MAX / HUE_DEGREES
9
+ SATURATION_MIN = 0
10
+ SATURATION_MAX = 255
11
+
12
+ attr_reader :hue, :saturation
13
+ alias :sat :saturation
14
+
15
+ public
16
+
17
+ def initialize(hue, saturation)
18
+ self.hue = hue
19
+ self.saturation = saturation
20
+ end
21
+
22
+ def hue=(value)
23
+ if scale = Hue.percent_to_unit_interval(value)
24
+ @hue = unit_to_hue_interval(scale)
25
+ else
26
+ @hue = ranged(HUE_MIN, value.to_i, HUE_MAX)
27
+ end
28
+ end
29
+
30
+ def hue_in_degrees
31
+ self.hue.to_f / HUE_SCALE
32
+ end
33
+
34
+ def hue_in_unit_interval
35
+ hue_in_degrees / HUE_DEGREES
36
+ end
37
+
38
+ def saturation=(value)
39
+ if scale = Hue.percent_to_unit_interval(value)
40
+ @saturation = unit_to_saturation_interval(scale)
41
+ else
42
+ @saturation = ranged(SATURATION_MIN, value.to_i, SATURATION_MAX)
43
+ end
44
+ end
45
+ alias :sat= :saturation=
46
+
47
+ def saturation_in_unit_interval
48
+ self.saturation / SATURATION_MAX.to_f
49
+ end
50
+ alias :sat_in_unit_interval :saturation_in_unit_interval
51
+
52
+ def to_s
53
+ "Hue=#{self.hue}, Saturation=#{self.saturation}"
54
+ end
55
+
56
+ def to_hash
57
+ {hue: hue, sat: saturation}
58
+ end
59
+
60
+ def to_rgb(brightness_in_unit_interval = 1.0)
61
+ h, s, v = hue_in_unit_interval, saturation_in_unit_interval, brightness_in_unit_interval
62
+ if s == 0 #monochromatic
63
+ red = green = blue = v
64
+ else
65
+
66
+ v = 1.0 # We are setting the value to 1. Don't count brightness here
67
+ i = (h * 6).floor
68
+ f = h * 6 - i
69
+ p = v * (1 - s)
70
+ q = v * (1 - f * s)
71
+ t = v * (1 - (1 - f) * s)
72
+
73
+ case i % 6
74
+ when 0
75
+ red, green, blue = v, t, p
76
+ when 1
77
+ red, green, blue = q, v, p
78
+ when 2
79
+ red, green, blue = p, v, t
80
+ when 3
81
+ red, green, blue = p, q, v
82
+ when 4
83
+ red, green, blue = t, p, v
84
+ when 5
85
+ red, green, blue = v, p, q
86
+ end
87
+ end
88
+
89
+ RGB.new(red * RGB::MAX, green * RGB::MAX, blue * RGB::MAX)
90
+ end
91
+
92
+ private
93
+
94
+ def unit_to_hue_interval(value)
95
+ (value * (HUE_MAX - HUE_MIN) + HUE_MIN).round
96
+ end
97
+
98
+ def unit_to_saturation_interval(value)
99
+ (value * (SATURATION_MAX - SATURATION_MIN) + SATURATION_MIN).round
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,102 @@
1
+ # Encoding: UTF-8
2
+
3
+ module Hue
4
+ module Colors
5
+ class RGB < Color
6
+
7
+ MIN = 0
8
+ MAX = 255
9
+
10
+ def self.ranged(value)
11
+ super(MIN, value, MAX)
12
+ end
13
+
14
+ public
15
+
16
+ attr_reader :red, :green, :blue
17
+
18
+ def initialize(*rgb)
19
+ red, green, blue = rgb
20
+ self.red = red
21
+ self.green = green
22
+ self.blue = blue
23
+ end
24
+
25
+ def red=(value)
26
+ @red = parse(value)
27
+ end
28
+
29
+ def green=(value)
30
+ @green = parse(value)
31
+ end
32
+
33
+ def blue=(value)
34
+ @blue = parse(value)
35
+ end
36
+
37
+ def to_hash
38
+ max = MAX.to_f
39
+ red, green, blue = self.red / max, self.green / max, self.red / max
40
+
41
+ max = [red, green, blue].max
42
+ min = [red, green, blue].min
43
+ h, s, l = 0, 0, ((max + min) / 2 * 255)
44
+
45
+ d = max - min
46
+ s = max == 0 ? 0 : (d / max * 255)
47
+
48
+ h = case max
49
+ when min
50
+ 0 # monochromatic
51
+ when red
52
+ (green - blue) / d + (green < blue ? 6 : 0)
53
+ when green
54
+ (blue - red) / d + 2
55
+ when blue
56
+ (red - green) / d + 4
57
+ end * 60 # / 6 * 360
58
+
59
+ h = (h * HueSaturation::HUE_SCALE).to_i
60
+ {hue: h, sat: s.to_i, bri: Bulb::BRIGHTNESS_MAX}
61
+ end
62
+
63
+ def to_s
64
+ "RGB≈#{rgb}"
65
+ end
66
+
67
+ def to_rgb
68
+ self
69
+ end
70
+
71
+ def ==(rhs)
72
+ rhs.is_a?(RGB) &&
73
+ [:red, :green, :blue].all? { |m| self.send(m) == rhs.send(m) }
74
+ end
75
+
76
+ protected
77
+
78
+ def ranged(value)
79
+ self.class.ranged(value.to_i).round
80
+ end
81
+
82
+ private
83
+
84
+ def rgb
85
+ [red, green, blue]
86
+ end
87
+
88
+ def unit_to_rgb_scale(value)
89
+ (value * (MAX - MIN) + MIN).round
90
+ end
91
+
92
+ def parse(value)
93
+ if scale = Hue.percent_to_unit_interval(value)
94
+ unit_to_rgb_scale(scale)
95
+ else
96
+ ranged(value)
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,63 @@
1
+ require 'matrix'
2
+ module Hue
3
+ module Colors
4
+ class XY < Color
5
+
6
+ MIN = 0.0
7
+ MAX = 1.0
8
+ RGB_MATRIX = Matrix[
9
+ [ 3.233358361244897, -1.5262682428425947, 0.27916711262124544],
10
+ [-0.8268442148395835, 2.466767560486707, 0.3323241608108406 ],
11
+ [ 0.12942207487871885, 0.19839858329512317, 2.0280912276039635 ],
12
+ ]
13
+
14
+ public
15
+
16
+ attr_reader :x, :y
17
+
18
+ def initialize(*xy)
19
+ self.x = xy.first
20
+ self.y = xy.last
21
+ end
22
+
23
+ def x=(value)
24
+ @x = ranged(value)
25
+ end
26
+
27
+ def y=(value)
28
+ @y = ranged(value)
29
+ end
30
+
31
+ def to_hash
32
+ {xy: xy}
33
+ end
34
+
35
+ def to_s
36
+ "XY=#{xy}"
37
+ end
38
+
39
+ def to_rgb
40
+ z = 1 - x - y
41
+ xyz = [x, y, z]
42
+ values = (RGB_MATRIX * Matrix[xyz].transpose).to_a.flatten.map do |x|
43
+ RGB.ranged(x * RGB::MAX).to_i
44
+ end
45
+
46
+ RGB.new(*values)
47
+ end
48
+
49
+ protected
50
+
51
+ def ranged(val)
52
+ super(MIN, val.to_f, MAX)
53
+ end
54
+
55
+ private
56
+
57
+ def xy
58
+ [x,y]
59
+ end
60
+
61
+ end
62
+ end
63
+ end
data/lib/hue/colors.rb ADDED
@@ -0,0 +1,42 @@
1
+ require_relative 'colors/color'
2
+ require_relative 'colors/hue_saturation'
3
+ require_relative 'colors/color_temperature'
4
+ require_relative 'colors/xy'
5
+ require_relative 'colors/rgb'
6
+
7
+ module Hue
8
+ module Colors
9
+
10
+ def self.parse(*args)
11
+ case args.size
12
+ when 1
13
+ Colors::ColorTemperature.new(args.first)
14
+ when 2
15
+ a,b = args.first.to_f, args.last.to_f
16
+ if a > 1.0
17
+ Colors::HueSaturation.new(args.first, args.last)
18
+ else
19
+ Colors::XY.new(*args)
20
+ end
21
+ when 3
22
+ Colors::RGB.new(*args)
23
+ else
24
+ raise Error.new("Unable to parse to color: #{args.inspect}")
25
+ end
26
+ end
27
+
28
+ def self.parse_state(state)
29
+ case state['colormode']
30
+ when 'ct'
31
+ Colors::ColorTemperature.new(state['ct'])
32
+ when 'xy'
33
+ Colors::XY.new(*state['xy'])
34
+ when 'hs'
35
+ Colors::HueSaturation.new(state['hue'], state['sat'])
36
+ else
37
+ raise Error.new("Unknown or missing state: #{state.inspect}")
38
+ end
39
+ end
40
+
41
+ end
42
+ end
data/lib/hue.rb CHANGED
@@ -1,15 +1,8 @@
1
1
  require 'net/http'
2
- require 'json'
3
- require 'matrix'
4
2
  require 'digest/md5'
3
+ require 'json'
5
4
  require 'uuid'
6
5
 
7
- RGB_MATRIX = Matrix[
8
- [3.233358361244897, -1.5262682428425947, 0.27916711262124544],
9
- [-0.8268442148395835, 2.466767560486707, 0.3323241608108406],
10
- [0.12942207487871885, 0.19839858329512317, 2.0280912276039635]
11
- ]
12
-
13
6
  module Hue
14
7
 
15
8
  DEVICE_TYPE = 'hue-lib'
@@ -131,10 +124,38 @@ ST: ssdp:all
131
124
  end
132
125
  end
133
126
 
127
+ def self.logger
128
+ if !defined?(@@logger)
129
+ log_dir_path = File.join('/var', 'log', 'hue')
130
+ begin
131
+ FileUtils.mkdir_p(log_dir_path)
132
+ rescue Errno::EACCES
133
+ log_dir_path = File.join(ENV['HOME'], ".#{device_type}")
134
+ FileUtils.mkdir_p(log_dir_path)
135
+ end
136
+
137
+ log_file_path = File.join(log_dir_path, 'hue-lib.log')
138
+ log_file = File.new(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
139
+ @@logger = Logger.new(log_file)
140
+ @@logger.level = Logger::INFO
141
+ end
142
+
143
+ @@logger
144
+ end
145
+
146
+ def self.percent_to_unit_interval(value)
147
+ if percent = /(\d+)%/.match(value.to_s)
148
+ percent.captures.first.to_i / 100.0
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
134
154
  end
135
155
 
136
156
  require 'hue/config/abstract'
137
157
  require 'hue/config/application'
138
158
  require 'hue/config/bridge'
139
159
  require 'hue/bridge'
160
+ require 'hue/colors'
140
161
  require 'hue/bulb'
@@ -33,21 +33,20 @@ describe Hue::Bulb do
33
33
  bulb.off?.should be_true
34
34
  end
35
35
 
36
- it "should report the hue, brightness and saturation" do
37
- bulb.hue.should == 13234
36
+ it "should report the brightness and color mode" do
38
37
  bulb.brightness.should == 146
39
38
  bulb.bri.should == 146
40
- bulb.saturation.should == 208
41
- bulb.sat.should == bulb.saturation
42
- end
43
-
44
- it "should report the color temperature and color mode" do
45
- bulb.color_temperature.should == 459
46
- bulb.ct.should == bulb.color_temperature
47
39
  bulb.color_mode.should == 'ct'
48
40
  bulb.color_mode.should == bulb.colormode
49
41
  end
50
42
 
43
+ it "should report the color" do
44
+ color = bulb.color
45
+ color.should be_a(Hue::Colors::ColorTemperature)
46
+ color.mired.should == 459
47
+ color.kelvin.should == 2179
48
+ end
49
+
51
50
  it "should report the alert state" do
52
51
  bulb.blinking?.should be_false
53
52
  bulb.solid?.should be_true
@@ -64,17 +63,28 @@ describe Hue::Bulb do
64
63
  end
65
64
 
66
65
  it 'should allow setting hue, saturation and brightness' do
67
- with_fake_update('lights/1/state', hue: 21845)
68
- bulb.hue = 120
69
- bulb.hue.should == 21845
66
+ color = Hue::Colors::HueSaturation.new(21845, 1293)
70
67
 
71
- with_fake_update('lights/1/state', sat: 1293)
72
- bulb.saturation = 1293
73
- bulb.saturation.should == 1293
68
+ with_fake_update('lights/1/state', hue: 21845, sat: 255)
69
+ set_color = (bulb.color = color)
70
+ set_color.hue.should == 21845
71
+ set_color.saturation.should == 255
72
+ end
74
73
 
74
+ it 'should allow setting brightness as a number, percentage or string' do
75
75
  with_fake_update('lights/1/state', bri: 233)
76
76
  bulb.brightness = 233
77
77
  bulb.brightness.should == 233
78
+
79
+ with_fake_update('lights/1/state', bri: 128)
80
+ bulb.brightness = "50%"
81
+ bulb.brightness.should == 128
82
+ bulb.brightness_in_unit_interval.should == 0.5019607843137255
83
+ bulb.brightness_percent.should == 50
84
+
85
+ with_fake_update('lights/1/state', bri: 128)
86
+ bulb.brightness = "128"
87
+ bulb.brightness.should == 128
78
88
  end
79
89
 
80
90
  it 'should allow setting blink, solid and flash alerts' do
@@ -0,0 +1,33 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Hue::Colors::Color do
4
+
5
+ context 'implements a ranged method: max > value > min' do
6
+ it 'should return the min when value < min' do
7
+ described_class.ranged(0, -1, 2).should == 0
8
+ end
9
+
10
+ it 'should return the max when value > max' do
11
+ described_class.ranged(0, 3, 2).should == 2
12
+ end
13
+
14
+ it 'should return the value when max > value > min' do
15
+ described_class.ranged(0, 1, 2).should == 1
16
+ end
17
+ end
18
+
19
+ def abstract_method(method)
20
+ abstract_color = described_class.new
21
+ abstract_color.should respond_to(method)
22
+ lambda do
23
+ abstract_color.send(method)
24
+ end.should raise_error(described_class::ERROR_METHOD_NOT_IMPLEMENTED)
25
+ end
26
+
27
+ context 'defines but does not implement methods:' do
28
+ it('#to_hash') { abstract_method(:to_hash) }
29
+ it('#to_s') { abstract_method(:to_s) }
30
+ it('#to_rgb') { abstract_method(:to_rgb) }
31
+ end
32
+
33
+ end
@@ -0,0 +1,106 @@
1
+ # Encoding: UTF-8
2
+ require 'spec_helper.rb'
3
+
4
+ describe Hue::Colors::ColorTemperature do
5
+
6
+ context 'when initialized with a valid value' do
7
+ color = described_class.new(500)
8
+
9
+ it 'should report the temperature in mireds and kelvins' do
10
+ color.mired.should == 500
11
+ color.kelvin.should == 2000
12
+ end
13
+
14
+ it 'should have a string representation' do
15
+ color.to_s.should == "Temperature=2000°K (500 mired)"
16
+ end
17
+
18
+ it 'should have a hash representation' do
19
+ color.to_hash.should == {ct: 500}
20
+ end
21
+
22
+ it 'should have an RGB representation' do
23
+ color.to_rgb.should == Hue::Colors::RGB.new(255,136,13)
24
+ end
25
+
26
+ context 'when allowing change to the temperature value' do
27
+ it 'should go to the max in kelvins' do
28
+ color.kelvin = 7000
29
+ color.kelvin.should == described_class::KELVIN_MAX
30
+ color.mired.should == described_class::MIRED_MIN
31
+ end
32
+
33
+ it 'should go to the max in mireds' do
34
+ color.mired = 600
35
+ color.mired.should == described_class::MIRED_MAX
36
+ color.kelvin.should == described_class::KELVIN_MIN
37
+ end
38
+
39
+ it 'should go to the min in mireds' do
40
+ color.mired = 100
41
+ color.mired.should == described_class::MIRED_MIN
42
+ color.kelvin.should == described_class::KELVIN_MAX
43
+ end
44
+
45
+ it 'should go to the min in kelvins' do
46
+ color.mired = 2000
47
+ color.mired.should == described_class::MIRED_MAX
48
+ color.kelvin.should == described_class::KELVIN_MIN
49
+ end
50
+
51
+ it 'should hit the middle' do
52
+ mired_middle = described_class::MIRED_MAX/2
53
+ color.mired = mired_middle
54
+ color.mired.should == mired_middle
55
+ color.kelvin.should == 4000
56
+
57
+ kelvin_middle = described_class::KELVIN_MAX/2
58
+ color.kelvin = kelvin_middle
59
+ color.kelvin.should == kelvin_middle
60
+ color.mired.should == 307
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ context 'when initializing' do
67
+ it 'should set the temperature depending on the value' do
68
+ color = described_class.new(100)
69
+ color.mired.should == described_class::MIRED_MIN
70
+
71
+ color = described_class.new(600)
72
+ color.mired.should == described_class::MIRED_MAX
73
+
74
+ color = described_class.new(described_class::KELVIN_MIN)
75
+ color.kelvin.should == described_class::KELVIN_MIN
76
+
77
+ color = described_class.new(described_class::KELVIN_MAX + 3000)
78
+ color.kelvin.should == described_class::KELVIN_MAX
79
+ end
80
+
81
+ it 'should report kelvins as an integer' do
82
+ color = described_class.new(459)
83
+ color.kelvin.should == 2179
84
+ end
85
+
86
+ it 'should accept a string value for the temperature' do
87
+ color = described_class.new("100")
88
+ color.mired.should == described_class::MIRED_MIN
89
+
90
+ color = described_class.new((described_class::KELVIN_MAX + 3000).to_s)
91
+ color.kelvin.should == described_class::KELVIN_MAX
92
+ end
93
+
94
+ it 'should allow a percentage value for the mired scale' do
95
+ color = described_class.new("0%")
96
+ color.mired.should == described_class::MIRED_MIN
97
+
98
+ color = described_class.new("50%")
99
+ color.mired.should == 326
100
+
101
+ color = described_class.new("100%")
102
+ color.mired.should == described_class::MIRED_MAX
103
+ end
104
+ end
105
+
106
+ end