hue-lib 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.md +88 -0
- data/Rakefile +19 -0
- data/hue-lib.gemspec +24 -0
- data/lib/hue.rb +59 -0
- data/lib/hue/bridge.rb +147 -0
- data/lib/hue/bulb.rb +458 -0
- data/lib/hue/config.rb +94 -0
- data/spec/config/bridges.yml +4 -0
- data/spec/hue/bridge_spec.rb +75 -0
- data/spec/hue/bulb_spec.rb +107 -0
- data/spec/hue/config_spec.rb +65 -0
- data/spec/hue_spec.rb +13 -0
- data/spec/json/base.json +117 -0
- data/spec/json/config.json +27 -0
- data/spec/json/lights.json +14 -0
- data/spec/json/lights/1.json +28 -0
- data/spec/json/schedules.json +14 -0
- data/spec/json/unauthorized.json +10 -0
- data/spec/spec_helper.rb +44 -0
- metadata +131 -0
data/lib/hue/config.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module Hue
|
2
|
+
class Config
|
3
|
+
STRING_DEFAULT = 'default'
|
4
|
+
STRING_BASE_URI = 'base_uri'
|
5
|
+
STRING_IDENTIFIER = 'identifier'
|
6
|
+
|
7
|
+
require 'yaml'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
def self.bridges_config_path
|
11
|
+
File.join(ENV['HOME'], ".#{APP_NAME}", 'bridges.yml')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.default
|
15
|
+
named(STRING_DEFAULT)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.named(name)
|
19
|
+
yaml = read_file
|
20
|
+
if named_yaml = yaml[name]
|
21
|
+
Config.new(named_yaml[STRING_BASE_URI], named_yaml[STRING_IDENTIFIER], name)
|
22
|
+
else
|
23
|
+
raise Error.new("Config named '#{name}' not found.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
public
|
28
|
+
|
29
|
+
attr_reader :base_uri, :identifier, :name
|
30
|
+
|
31
|
+
def initialize(base_uri, identifier, name = STRING_DEFAULT)
|
32
|
+
@base_uri = base_uri
|
33
|
+
@identifier = identifier
|
34
|
+
@name = name
|
35
|
+
end
|
36
|
+
|
37
|
+
def write(config_file = self.class.bridges_config_path)
|
38
|
+
yaml = YAML.load_file(self.class.bridges_config_path) rescue Hash::New
|
39
|
+
if yaml.key?(name)
|
40
|
+
raise "Configuration named '#{name}' already exists in #{config_file}\nPlease de-register before creating a new one with the same name."
|
41
|
+
else
|
42
|
+
yaml[name] = {
|
43
|
+
STRING_BASE_URI => self.base_uri,
|
44
|
+
STRING_IDENTIFIER => identifier.force_encoding('ASCII') # Avoid binary encoded YAML
|
45
|
+
}
|
46
|
+
self.class.setup_config_path(config_file)
|
47
|
+
File.open(config_file, 'w+' ) do |out|
|
48
|
+
YAML.dump(yaml, out)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete
|
54
|
+
config_file = self.class.bridges_config_path
|
55
|
+
yaml = YAML.load_file(config_file) rescue Hash::New
|
56
|
+
|
57
|
+
if yaml.key?(name)
|
58
|
+
yaml.delete(name)
|
59
|
+
end
|
60
|
+
|
61
|
+
if yaml.size > 0
|
62
|
+
self.class.setup_config_path(config_file)
|
63
|
+
File.open(config_file, 'w+' ) do |out|
|
64
|
+
YAML.dump(yaml, out)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def ==(rhs)
|
70
|
+
lhs = self
|
71
|
+
|
72
|
+
lhs.class == rhs.class &&
|
73
|
+
lhs.name == rhs.name &&
|
74
|
+
lhs.base_uri == rhs.base_uri &&
|
75
|
+
lhs.identifier == rhs.identifier
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def self.setup_config_path(path)
|
81
|
+
dir = File.dirname(path)
|
82
|
+
FileUtils.mkdir_p(dir) unless Dir.exists?(dir)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.read_file(config_file = bridges_config_path)
|
86
|
+
begin
|
87
|
+
yaml = YAML.load_file(config_file)
|
88
|
+
rescue => err
|
89
|
+
raise Error.new("Failed to read configuration file", err)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hue::Bridge do
|
4
|
+
|
5
|
+
def self.klass
|
6
|
+
Hue::Bridge
|
7
|
+
end
|
8
|
+
|
9
|
+
def klass
|
10
|
+
self.class.klass
|
11
|
+
end
|
12
|
+
|
13
|
+
# it 'should acts as a singleton and give access to the instance' do
|
14
|
+
# klass.instance.should be_a_kind_of(Hue::Bridge)
|
15
|
+
# end
|
16
|
+
|
17
|
+
it 'should allow registering a new bridge' do
|
18
|
+
pending
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should allow un-registering a bridge' do
|
22
|
+
pending
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when instantiated with a given config' do
|
26
|
+
bridge = klass.new
|
27
|
+
|
28
|
+
# before(:each) do
|
29
|
+
# with_fake_index_request
|
30
|
+
# end
|
31
|
+
|
32
|
+
it 'should report the bridge status' do
|
33
|
+
with_fake_request_base
|
34
|
+
bridge.status.should == api_reply(:base)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should report errors' do
|
38
|
+
with_fake_request(:lights, :unauthorized)
|
39
|
+
lambda do
|
40
|
+
bridge.lights
|
41
|
+
end.should raise_error(Hue::API::Error, 'unauthorized user')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should report the bridge lights' do
|
45
|
+
with_fake_request(:lights)
|
46
|
+
bridge.lights.should == api_reply(:lights)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should report a simple string of light names' do
|
50
|
+
with_fake_request(:lights)
|
51
|
+
bridge.light_names.should == "1. Dining\n2. Bedroom Far\n3. Bedroom Near"
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should report the bridge config' do
|
55
|
+
with_fake_request(:config)
|
56
|
+
bridge.config.should == api_reply(:config)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should report the light schedules' do
|
60
|
+
with_fake_request(:schedules)
|
61
|
+
bridge.schedules.should == api_reply(:schedules)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should return instance of all the bulbs' do
|
65
|
+
with_fake_request(:lights)
|
66
|
+
bulbs = bridge.bulbs
|
67
|
+
bulbs.size.should == 3
|
68
|
+
bulbs.each do |bulb|
|
69
|
+
bulb.should be_a(Hue::Bulb)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hue::Bulb do
|
4
|
+
|
5
|
+
def self.klass
|
6
|
+
Hue::Bulb
|
7
|
+
end
|
8
|
+
|
9
|
+
def klass
|
10
|
+
self.class.klass
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when instantiated with a given bridge and id' do
|
14
|
+
bulb = klass.new(Hue::Bridge.new, 1)
|
15
|
+
|
16
|
+
before(:all) do
|
17
|
+
with_fake_request('lights/1')
|
18
|
+
@test_status = api_reply('lights/1')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should report the bulb state' do
|
22
|
+
bulb.state.should == @test_status['state']
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should report the bulb info' do
|
26
|
+
info = api_reply('lights/1')
|
27
|
+
info.delete('state')
|
28
|
+
info.delete('pointsymbol')
|
29
|
+
bulb.info.should == info
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should report it's name" do
|
33
|
+
bulb.name.should == @test_status['name']
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should report if it's on" do
|
37
|
+
bulb.on?.should be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should report if it's off" do
|
41
|
+
bulb.off?.should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should report the hue, brightness and saturation" do
|
45
|
+
bulb.hue.should == 13234
|
46
|
+
bulb.brightness.should == 146
|
47
|
+
bulb.bri.should == 146
|
48
|
+
bulb.saturation.should == 208
|
49
|
+
bulb.sat.should == bulb.saturation
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should report the color temperature and color mode" do
|
53
|
+
bulb.color_temperature.should == 459
|
54
|
+
bulb.ct.should == bulb.color_temperature
|
55
|
+
bulb.color_mode.should == 'ct'
|
56
|
+
bulb.color_mode.should == bulb.colormode
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should report the alert state" do
|
60
|
+
bulb.blinking?.should be_false
|
61
|
+
bulb.solid?.should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'by changing state' do
|
65
|
+
|
66
|
+
it 'should allow turning bulps on and off' do
|
67
|
+
with_fake_update('lights/1/state', on: true)
|
68
|
+
bulb.on.should be_true
|
69
|
+
|
70
|
+
with_fake_update('lights/1/state', on: false)
|
71
|
+
bulb.off.should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should allow setting hue, saturation and brightness' do
|
75
|
+
with_fake_update('lights/1/state', hue: 21845)
|
76
|
+
bulb.hue = 120
|
77
|
+
bulb.hue.should == 21845
|
78
|
+
|
79
|
+
with_fake_update('lights/1/state', sat: 1293)
|
80
|
+
bulb.saturation = 1293
|
81
|
+
bulb.saturation.should == 1293
|
82
|
+
|
83
|
+
with_fake_update('lights/1/state', bri: 233)
|
84
|
+
bulb.brightness = 233
|
85
|
+
bulb.brightness.should == 233
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should allow setting blink, solid and flash alerts' do
|
89
|
+
with_fake_update('lights/1/state', alert: 'lselect')
|
90
|
+
bulb.blink
|
91
|
+
bulb.blinking?.should be_true
|
92
|
+
|
93
|
+
with_fake_update('lights/1/state', alert: 'none')
|
94
|
+
bulb.solid
|
95
|
+
bulb.solid?.should be_true
|
96
|
+
|
97
|
+
with_fake_update('lights/1/state', alert: 'select') do
|
98
|
+
with_fake_update('lights/1/state', alert: 'none')
|
99
|
+
bulb.flash
|
100
|
+
bulb.solid?.should be_true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hue::Config do
|
4
|
+
|
5
|
+
TEST_IDENTIFIER = 'test_identifier'
|
6
|
+
|
7
|
+
def self.klass
|
8
|
+
Hue::Config
|
9
|
+
end
|
10
|
+
|
11
|
+
def klass
|
12
|
+
self.class.klass
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:all) do
|
16
|
+
File.open(TEST_BRIDGE_CONFIG_PATH, 'w' ) do |out|
|
17
|
+
YAML.dump(TEST_BRIDGE_CONFIG, out)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should report the bridge config file location' do
|
22
|
+
klass.bridges_config_path.should == TEST_BRIDGE_CONFIG_PATH
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should throw and error if a named config doesn't exist" do
|
26
|
+
lambda do
|
27
|
+
klass.named('not_default')
|
28
|
+
end.should raise_error(Hue::Error, /Config named (.*) not found/)
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with a bridge config file, containing the default bridge' do
|
32
|
+
it "should give the default config and report it's values" do
|
33
|
+
config = klass.default
|
34
|
+
config.name == klass::STRING_DEFAULT
|
35
|
+
config.base_uri == TEST_BRIDGE_CONFIG[config.name][klass::STRING_BASE_URI]
|
36
|
+
config.identifier == TEST_BRIDGE_CONFIG[config.name][klass::STRING_IDENTIFIER]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'given an new config' do
|
41
|
+
config = klass.new('http://someip/api', 'some_id', 'not_default')
|
42
|
+
|
43
|
+
it 'should report the values' do
|
44
|
+
config.name == 'not_default'
|
45
|
+
config.base_uri == 'http://someip/api'
|
46
|
+
config.identifier == 'not_default'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should allow writing the new config to file' do
|
50
|
+
config.write
|
51
|
+
YAML.load_file(klass.bridges_config_path)['not_default'].should be_a(Hash)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should allow fetching that name config' do
|
55
|
+
named_config = klass.named('not_default')
|
56
|
+
named_config.should == config
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should allow deleting that named config from the file' do
|
60
|
+
config.delete
|
61
|
+
YAML.load_file(klass.bridges_config_path)['not_default'].should be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/spec/hue_spec.rb
ADDED
data/spec/json/base.json
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
{
|
2
|
+
"lights" : {
|
3
|
+
"1" : {
|
4
|
+
"state" : {
|
5
|
+
"on" : false,
|
6
|
+
"bri" : 146,
|
7
|
+
"hue" : 13234,
|
8
|
+
"sat" : 208,
|
9
|
+
"xy" : [ "0.5090", "0.4149" ],
|
10
|
+
"ct" : 459,
|
11
|
+
"alert" : "none",
|
12
|
+
"effect" : "none",
|
13
|
+
"colormode" : "ct",
|
14
|
+
"reachable" : true
|
15
|
+
},
|
16
|
+
"type" : "Extended color light",
|
17
|
+
"name" : "Living",
|
18
|
+
"modelid" : "LCT001",
|
19
|
+
"swversion" : "65003148",
|
20
|
+
"pointsymbol" : {
|
21
|
+
"1" : "none",
|
22
|
+
"2" : "none",
|
23
|
+
"3" : "none",
|
24
|
+
"4" : "none",
|
25
|
+
"5" : "none",
|
26
|
+
"6" : "none",
|
27
|
+
"7" : "none",
|
28
|
+
"8" : "none"
|
29
|
+
}
|
30
|
+
},
|
31
|
+
"2" : {
|
32
|
+
"state" : {
|
33
|
+
"on" : false,
|
34
|
+
"bri" : 162,
|
35
|
+
"hue" : 2213,
|
36
|
+
"sat" : 238,
|
37
|
+
"xy" : [ "0.6349", "0.3413" ],
|
38
|
+
"ct" : 500,
|
39
|
+
"alert" : "none",
|
40
|
+
"effect" : "none",
|
41
|
+
"colormode" : "xy",
|
42
|
+
"reachable" : true
|
43
|
+
},
|
44
|
+
"type" : "Extended color light",
|
45
|
+
"name" : "Bedroom Far",
|
46
|
+
"modelid" : "LCT001",
|
47
|
+
"swversion" : "65003148",
|
48
|
+
"pointsymbol" : {
|
49
|
+
"1" : "none",
|
50
|
+
"2" : "none",
|
51
|
+
"3" : "none",
|
52
|
+
"4" : "none",
|
53
|
+
"5" : "none",
|
54
|
+
"6" : "none",
|
55
|
+
"7" : "none",
|
56
|
+
"8" : "none"
|
57
|
+
}
|
58
|
+
},
|
59
|
+
"3" : {
|
60
|
+
"state" : {
|
61
|
+
"on" : false,
|
62
|
+
"bri" : 146,
|
63
|
+
"hue" : 13122,
|
64
|
+
"sat" : 211,
|
65
|
+
"xy" : [ "0.3565", "0.1775" ],
|
66
|
+
"ct" : 462,
|
67
|
+
"alert" : "none",
|
68
|
+
"effect" : "none",
|
69
|
+
"colormode" : "ct",
|
70
|
+
"reachable" : true
|
71
|
+
},
|
72
|
+
"type" : "Extended color light",
|
73
|
+
"name" : "Bedroom Near",
|
74
|
+
"modelid" : "LCT001",
|
75
|
+
"swversion" : "65003148",
|
76
|
+
"pointsymbol" : {
|
77
|
+
"1" : "none",
|
78
|
+
"2" : "none",
|
79
|
+
"3" : "none",
|
80
|
+
"4" : "none",
|
81
|
+
"5" : "none",
|
82
|
+
"6" : "none",
|
83
|
+
"7" : "none",
|
84
|
+
"8" : "none"
|
85
|
+
}
|
86
|
+
}
|
87
|
+
},
|
88
|
+
"groups" : { },
|
89
|
+
"config" : {
|
90
|
+
"name" : "Philips hue",
|
91
|
+
"mac" : "00:17:88:09:26:9d",
|
92
|
+
"dhcp" : true,
|
93
|
+
"ipaddress" : "10.255.255.44",
|
94
|
+
"netmask" : "255.255.255.0",
|
95
|
+
"gateway" : "10.255.255.254",
|
96
|
+
"proxyaddress" : "",
|
97
|
+
"proxyport" : 0,
|
98
|
+
"UTC" : "2012-11-02T18:34:38",
|
99
|
+
"whitelist" : {
|
100
|
+
"0123456789abdcef0123456789abcdef" : {
|
101
|
+
"last use date" : "2012-11-02T18:34:38",
|
102
|
+
"create date" : "2012-11-01T21:04:48",
|
103
|
+
"name" : "iPhone 5"
|
104
|
+
}
|
105
|
+
},
|
106
|
+
"swversion" : "01003542",
|
107
|
+
"swupdate" : {
|
108
|
+
"updatestate" : 0,
|
109
|
+
"url" : "",
|
110
|
+
"text" : "",
|
111
|
+
"notify" : false
|
112
|
+
},
|
113
|
+
"linkbutton" : false,
|
114
|
+
"portalservices" : false
|
115
|
+
},
|
116
|
+
"schedules" : { }
|
117
|
+
}
|