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.
@@ -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