hue-lib 0.5.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/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
|
+
}
|