hue 0.3.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +21 -6
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -2
- data/Rakefile +8 -6
- data/bin/console +7 -0
- data/hue.gemspec +17 -22
- data/lib/hue/bridge.rb +18 -18
- data/lib/hue/cli.rb +30 -30
- data/lib/hue/client.rb +32 -35
- data/lib/hue/editable_state.rb +42 -5
- data/lib/hue/errors.rb +22 -8
- data/lib/hue/group.rb +28 -30
- data/lib/hue/light.rb +40 -31
- data/lib/hue/scene.rb +5 -7
- data/lib/hue/translate_keys.rb +1 -1
- data/lib/hue/version.rb +1 -1
- data/lib/hue.rb +9 -9
- data/test/hue/client_test.rb +9 -8
- data/test/hue/light_test.rb +29 -9
- data/test/test_helper.rb +5 -5
- metadata +6 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17b9710dcfb0defef023c1adb4c4da4ca86fddc6f2e6c0d971bcb562c57ec61a
|
4
|
+
data.tar.gz: 8d8afd006ca7892d74f33af84f4040aedd0718b13583e1708a6994bb7803c3d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d3a2e81c450c2d1dd5c94ade3cf146ea4f56f4102f710051c54185902c388f9c9494a6f597f3f0ee05b59d0b0ade9ba837c2a7983df9679ce14ea34dd07e9eb
|
7
|
+
data.tar.gz: eea449790732dbcfcd0795f1e01af3d6d1a19b800c3b41404fe0bd1a12a76fa706f59269f90320365dde650c6425811fca9bd0ea38558945e8d8c0800badcdc4
|
data/.github/workflows/main.yml
CHANGED
@@ -2,13 +2,28 @@ name: Tests
|
|
2
2
|
on: [push]
|
3
3
|
jobs:
|
4
4
|
test:
|
5
|
-
name: Test
|
5
|
+
name: Test
|
6
6
|
runs-on: ubuntu-latest
|
7
7
|
timeout-minutes: 5
|
8
8
|
steps:
|
9
|
-
-
|
10
|
-
|
11
|
-
- name:
|
12
|
-
|
13
|
-
|
9
|
+
- name: Checkout
|
10
|
+
uses: actions/checkout@v4
|
11
|
+
- name: Setup Ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
bundler-cache: true
|
15
|
+
- name: Test
|
14
16
|
run: bundle exec rake test
|
17
|
+
lint:
|
18
|
+
name: Lint
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
timeout-minutes: 5
|
21
|
+
steps:
|
22
|
+
- name: Checkout
|
23
|
+
uses: actions/checkout@v4
|
24
|
+
- name: Setup Ruby
|
25
|
+
uses: ruby/setup-ruby@v1
|
26
|
+
with:
|
27
|
+
bundler-cache: true
|
28
|
+
- name: Test
|
29
|
+
run: bundle exec rake standard
|
data/.gitignore
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.2
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
3
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
7
|
-
t.test_files = FileList[
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
require "standard/rake"
|
11
|
+
|
12
|
+
task default: :test
|
data/bin/console
ADDED
data/hue.gemspec
CHANGED
@@ -1,28 +1,23 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "hue/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
spec.description
|
12
|
-
spec.summary
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
6
|
+
spec.name = "hue"
|
7
|
+
spec.version = Hue::VERSION
|
8
|
+
spec.authors = ["Sam Soffes"]
|
9
|
+
spec.email = ["sam@soff.es"]
|
10
|
+
spec.description = "Work with Philips Hue light bulbs."
|
11
|
+
spec.summary = "Work with Philips Hue light bulbs from Ruby."
|
12
|
+
spec.homepage = "https://github.com/soffes/hue"
|
13
|
+
spec.license = "MIT"
|
15
14
|
|
16
|
-
spec.files
|
17
|
-
spec.executables
|
18
|
-
spec.
|
19
|
-
spec.require_paths = ['lib']
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.require_paths = ["lib"]
|
20
18
|
|
21
|
-
spec.required_ruby_version =
|
22
|
-
spec.add_dependency
|
23
|
-
spec.add_dependency
|
24
|
-
spec.add_dependency
|
25
|
-
spec.add_dependency 'curb'
|
26
|
-
spec.add_development_dependency 'minitest', '~> 5.0'
|
27
|
-
spec.add_development_dependency 'webmock'
|
19
|
+
spec.required_ruby_version = ">= 2.1.0"
|
20
|
+
spec.add_dependency "thor"
|
21
|
+
spec.add_dependency "json"
|
22
|
+
spec.add_dependency "color_conversion"
|
28
23
|
end
|
data/lib/hue/bridge.rb
CHANGED
@@ -46,21 +46,21 @@ module Hue
|
|
46
46
|
# Current time stored on the bridge.
|
47
47
|
def utc
|
48
48
|
json = get_configuration
|
49
|
-
DateTime.parse(json[
|
49
|
+
DateTime.parse(json["utc"])
|
50
50
|
end
|
51
51
|
|
52
52
|
# Indicates whether the link button has been pressed within the last 30
|
53
53
|
# seconds.
|
54
54
|
def link_button_pressed?
|
55
55
|
json = get_configuration
|
56
|
-
json[
|
56
|
+
json["linkbutton"]
|
57
57
|
end
|
58
58
|
|
59
59
|
# This indicates whether the bridge is registered to synchronize data with a
|
60
60
|
# portal account.
|
61
61
|
def has_portal_services?
|
62
62
|
json = get_configuration
|
63
|
-
json[
|
63
|
+
json["portalservices"]
|
64
64
|
end
|
65
65
|
|
66
66
|
def refresh
|
@@ -74,7 +74,7 @@ module Hue
|
|
74
74
|
def lights
|
75
75
|
@lights ||= begin
|
76
76
|
json = JSON(Net::HTTP.get(URI.parse(base_url)))
|
77
|
-
json[
|
77
|
+
json["lights"].map do |key, value|
|
78
78
|
Light.new(@client, self, key, value)
|
79
79
|
end
|
80
80
|
end
|
@@ -84,7 +84,7 @@ module Hue
|
|
84
84
|
uri = URI.parse("#{base_url}/lights")
|
85
85
|
http = Net::HTTP.new(uri.host)
|
86
86
|
response = http.request_post(uri.path, nil)
|
87
|
-
|
87
|
+
response.body.first
|
88
88
|
end
|
89
89
|
|
90
90
|
def groups
|
@@ -105,27 +105,27 @@ module Hue
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
|
108
|
+
private
|
109
109
|
|
110
110
|
KEYS_MAP = {
|
111
|
-
:
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:
|
115
|
-
:
|
116
|
-
:
|
117
|
-
:
|
118
|
-
:
|
119
|
-
:
|
120
|
-
:
|
121
|
-
:
|
111
|
+
id: :id,
|
112
|
+
ip: :internalipaddress,
|
113
|
+
name: :name,
|
114
|
+
proxy_port: :proxyport,
|
115
|
+
software_update: :swupdate,
|
116
|
+
ip_whitelist: :whitelist,
|
117
|
+
software_version: :swversion,
|
118
|
+
proxy_address: :proxyaddress,
|
119
|
+
mac_address: :macaddress,
|
120
|
+
network_mask: :netmask,
|
121
|
+
portal_services: :portalservices
|
122
122
|
}
|
123
123
|
|
124
124
|
def unpack(hash)
|
125
125
|
KEYS_MAP.each do |local_key, remote_key|
|
126
126
|
value = hash[remote_key.to_s]
|
127
127
|
next unless value
|
128
|
-
instance_variable_set("@#{local_key}", value)
|
128
|
+
instance_variable_set(:"@#{local_key}", value)
|
129
129
|
end
|
130
130
|
end
|
131
131
|
|
data/lib/hue/cli.rb
CHANGED
@@ -1,67 +1,67 @@
|
|
1
|
-
require
|
1
|
+
require "thor"
|
2
2
|
|
3
3
|
module Hue
|
4
4
|
class Cli < Thor
|
5
|
-
desc
|
5
|
+
desc "lights", "Find all of the lights on your network"
|
6
6
|
def lights
|
7
7
|
client.lights.each do |light|
|
8
8
|
puts light.id.to_s.ljust(6) + light.name
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
desc
|
12
|
+
desc "add LIGHTS", "Search for new lights"
|
13
13
|
def add(thing)
|
14
14
|
case thing
|
15
|
-
when
|
15
|
+
when "lights"
|
16
16
|
client.add_lights
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
desc
|
20
|
+
desc "light ID STATE [COLOR]", "Access a light"
|
21
21
|
long_desc <<-LONGDESC
|
22
22
|
Examples: \n
|
23
23
|
hue all on \n
|
24
24
|
hue all off \n
|
25
25
|
hue all --hue 12345 \n
|
26
|
-
hue all --
|
27
|
-
hue all --hue 50000 --
|
26
|
+
hue all --brightness 25 \n
|
27
|
+
hue all --hue 50000 --brightness 200 --saturation 240 \n
|
28
28
|
hue all --alert lselect \n
|
29
29
|
LONGDESC
|
30
|
-
option :hue, :
|
31
|
-
option :sat, :
|
32
|
-
option :bri, :
|
33
|
-
option :alert, :
|
34
|
-
desc
|
35
|
-
def all(state =
|
30
|
+
option :hue, type: :numeric
|
31
|
+
option :sat, type: :numeric, aliases: "--saturation"
|
32
|
+
option :bri, type: :numeric, aliases: "--brightness"
|
33
|
+
option :alert, type: :string
|
34
|
+
desc "all STATE", "Send commands to all lights"
|
35
|
+
def all(state = "on")
|
36
36
|
body = options.dup
|
37
|
-
body[:on] = state ==
|
37
|
+
body[:on] = state == "on"
|
38
38
|
client.lights.each do |light|
|
39
39
|
puts light.set_state body
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
desc
|
43
|
+
desc "light ID STATE [COLOR]", "Access a light"
|
44
44
|
long_desc <<-LONGDESC
|
45
45
|
Examples: \n
|
46
46
|
hue light 1 on --hue 12345 \n
|
47
|
-
hue light 1 --
|
47
|
+
hue light 1 --brightness 25 \n
|
48
48
|
hue light 1 --alert lselect \n
|
49
49
|
hue light 1 off
|
50
50
|
LONGDESC
|
51
|
-
option :hue, :
|
52
|
-
option :sat, :
|
53
|
-
option :
|
54
|
-
option :alert, :
|
51
|
+
option :hue, type: :numeric
|
52
|
+
option :sat, type: :numeric, aliases: "--saturation"
|
53
|
+
option :bri, type: :numeric, aliases: "--brightness"
|
54
|
+
option :alert, type: :string
|
55
55
|
def light(id, state = nil)
|
56
56
|
light = client.light(id)
|
57
57
|
puts light.name
|
58
58
|
|
59
59
|
body = options.dup
|
60
|
-
body[:on] = (state ==
|
60
|
+
body[:on] = (state == "on" || !(state == "off"))
|
61
61
|
puts light.set_state(body) if body.length > 0
|
62
62
|
end
|
63
63
|
|
64
|
-
desc
|
64
|
+
desc "groups", "Find all light groups on your network"
|
65
65
|
def groups
|
66
66
|
client.groups.each do |group|
|
67
67
|
puts group.id.to_s.ljust(6) + group.name
|
@@ -71,28 +71,28 @@ module Hue
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
desc
|
74
|
+
desc "group ID STATE [COLOR]", "Update a group of lights"
|
75
75
|
long_desc <<-LONGDESC
|
76
76
|
Examples: \n
|
77
77
|
hue groups 1 on --hue 12345
|
78
|
-
hue groups 1 --
|
78
|
+
hue groups 1 --brightness 25
|
79
79
|
hue groups 1 --alert lselect
|
80
80
|
hue groups 1 off
|
81
81
|
LONGDESC
|
82
|
-
option :hue, :
|
83
|
-
option :sat, :
|
84
|
-
option :
|
85
|
-
option :alert, :
|
82
|
+
option :hue, type: :numeric
|
83
|
+
option :sat, type: :numeric, aliases: "--saturation"
|
84
|
+
option :bri, type: :numeric, aliases: "--brightness"
|
85
|
+
option :alert, type: :string
|
86
86
|
def group(id, state = nil)
|
87
87
|
group = client.group(id)
|
88
88
|
puts group.name
|
89
89
|
|
90
90
|
body = options.dup
|
91
|
-
body[:on] = (state ==
|
91
|
+
body[:on] = (state == "on" || !(state == "off"))
|
92
92
|
puts group.set_state(body) if body.length > 0
|
93
93
|
end
|
94
94
|
|
95
|
-
|
95
|
+
private
|
96
96
|
|
97
97
|
def client
|
98
98
|
@client ||= Hue::Client.new
|
data/lib/hue/client.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require 'curb'
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require "resolv"
|
5
4
|
|
6
5
|
module Hue
|
7
6
|
class Client
|
@@ -24,11 +23,11 @@ module Hue
|
|
24
23
|
end
|
25
24
|
|
26
25
|
def bridge
|
27
|
-
@bridge_id
|
28
|
-
if @bridge_id
|
29
|
-
|
26
|
+
@bridge_id ||= find_bridge_id
|
27
|
+
bridge = if @bridge_id
|
28
|
+
bridges.find { |b| b.id == @bridge_id }
|
30
29
|
else
|
31
|
-
|
30
|
+
bridges.first
|
32
31
|
end
|
33
32
|
raise NoBridgeFound unless bridge
|
34
33
|
bridge
|
@@ -53,7 +52,7 @@ module Hue
|
|
53
52
|
|
54
53
|
def light(id)
|
55
54
|
id = id.to_s
|
56
|
-
lights.
|
55
|
+
lights.find { |l| l.id == id }
|
57
56
|
end
|
58
57
|
|
59
58
|
def groups
|
@@ -64,7 +63,7 @@ module Hue
|
|
64
63
|
return Group.new(self, bridge) if id.nil?
|
65
64
|
|
66
65
|
id = id.to_s
|
67
|
-
groups.
|
66
|
+
groups.find { |g| g.id == id }
|
68
67
|
end
|
69
68
|
|
70
69
|
def scenes
|
@@ -73,18 +72,18 @@ module Hue
|
|
73
72
|
|
74
73
|
def scene(id)
|
75
74
|
id = id.to_s
|
76
|
-
scenes.
|
75
|
+
scenes.find { |s| s.id == id }
|
77
76
|
end
|
78
77
|
|
79
78
|
private
|
80
79
|
|
81
80
|
def find_username
|
82
|
-
return ENV[
|
81
|
+
return ENV["HUE_USERNAME"] if ENV["HUE_USERNAME"]
|
83
82
|
|
84
|
-
json = JSON(File.read(File.expand_path(
|
85
|
-
json[
|
83
|
+
json = JSON(File.read(File.expand_path("~/.hue")))
|
84
|
+
json["username"]
|
86
85
|
rescue
|
87
|
-
|
86
|
+
nil
|
88
87
|
end
|
89
88
|
|
90
89
|
def validate_user
|
@@ -94,38 +93,38 @@ module Hue
|
|
94
93
|
response = response.first
|
95
94
|
end
|
96
95
|
|
97
|
-
if error = response[
|
96
|
+
if (error = response["error"])
|
98
97
|
raise get_error(error)
|
99
98
|
end
|
100
99
|
|
101
|
-
response[
|
100
|
+
response["success"]
|
102
101
|
end
|
103
102
|
|
104
103
|
def register_user
|
105
104
|
body = JSON.dump({
|
106
|
-
devicetype:
|
105
|
+
devicetype: "Ruby"
|
107
106
|
})
|
108
107
|
|
109
108
|
uri = URI.parse("http://#{bridge.ip}/api")
|
110
109
|
http = Net::HTTP.new(uri.host)
|
111
110
|
response = JSON(http.request_post(uri.path, body).body).first
|
112
111
|
|
113
|
-
if error = response[
|
112
|
+
if (error = response["error"])
|
114
113
|
raise get_error(error)
|
115
114
|
end
|
116
115
|
|
117
|
-
if @username = response[
|
118
|
-
File.write(File.expand_path(
|
116
|
+
if (@username = response["success"]["username"])
|
117
|
+
File.write(File.expand_path("~/.hue"), JSON.dump({username: @username}))
|
119
118
|
end
|
120
119
|
end
|
121
120
|
|
122
121
|
def find_bridge_id
|
123
|
-
return ENV[
|
122
|
+
return ENV["HUE_BRIDGE_ID"] if ENV["HUE_BRIDGE_ID"]
|
124
123
|
|
125
|
-
json = JSON(File.read(File.expand_path(
|
126
|
-
json[
|
124
|
+
json = JSON(File.read(File.expand_path("~/.hue")))
|
125
|
+
json["bridge_id"]
|
127
126
|
rescue
|
128
|
-
|
127
|
+
nil
|
129
128
|
end
|
130
129
|
|
131
130
|
def discovery_mdns(bs)
|
@@ -136,8 +135,8 @@ module Hue
|
|
136
135
|
bridge_target = resolver.getresource(bridge_ptr.name, Resolv::DNS::Resource::IN::SRV).target
|
137
136
|
|
138
137
|
bridge_hash = {
|
139
|
-
|
140
|
-
|
138
|
+
"id" => resolver.getresource(bridge_ptr.name, Resolv::DNS::Resource::IN::TXT).strings[0].split("=")[1],
|
139
|
+
"internalipaddress" => resolver.getresource(bridge_target, Resolv::DNS::Resource::IN::A).address
|
141
140
|
}
|
142
141
|
|
143
142
|
bs << Bridge.new(self, bridge_hash)
|
@@ -145,20 +144,18 @@ module Hue
|
|
145
144
|
end
|
146
145
|
|
147
146
|
def discovery_meethue(bs)
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
easy.perform
|
153
|
-
JSON(easy.body).each do |hash|
|
147
|
+
uri = URI("https://discovery.meethue.com/")
|
148
|
+
response = Net::HTTP.get(uri)
|
149
|
+
|
150
|
+
JSON(response).each do |hash|
|
154
151
|
bs << Bridge.new(self, hash)
|
155
152
|
end
|
156
153
|
end
|
157
154
|
|
158
155
|
def get_error(error)
|
159
156
|
# Find error class and return instance
|
160
|
-
klass
|
161
|
-
klass.new(error[
|
157
|
+
klass ||= Hue::ERROR_MAP[error["type"]] || UnknownError
|
158
|
+
klass.new(error["description"])
|
162
159
|
end
|
163
160
|
end
|
164
161
|
end
|
data/lib/hue/editable_state.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
+
require "color_conversion"
|
2
|
+
|
1
3
|
module Hue
|
2
4
|
module EditableState
|
5
|
+
HUE_RANGE = 0..65535
|
6
|
+
SATURATION_RANGE = 0..254
|
7
|
+
BRIGHTNESS_RANGE = 0..254
|
8
|
+
|
3
9
|
def on?
|
4
|
-
@
|
10
|
+
@on || false
|
5
11
|
end
|
6
12
|
|
7
13
|
def on!
|
@@ -12,16 +18,47 @@ module Hue
|
|
12
18
|
self.on = false
|
13
19
|
end
|
14
20
|
|
15
|
-
|
16
|
-
|
21
|
+
# Turn the light on if it's off and vice versa
|
22
|
+
def toggle!
|
23
|
+
if @on
|
24
|
+
off!
|
25
|
+
else
|
26
|
+
on!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
%w[on hue saturation brightness color_temperature alert effect].each do |key|
|
31
|
+
define_method :"#{key}=" do |value|
|
17
32
|
set_state({key.to_sym => value})
|
18
|
-
instance_variable_set("@#{key}"
|
33
|
+
instance_variable_set(:"@#{key}", value)
|
19
34
|
end
|
20
35
|
end
|
21
36
|
|
22
37
|
def set_xy(x, y)
|
23
|
-
set_state({:
|
38
|
+
set_state({xy: [x, y]})
|
24
39
|
@x, @y = x, y
|
25
40
|
end
|
41
|
+
|
42
|
+
def hex
|
43
|
+
ColorConversion::Color.new(h: hue, s: saturation, b: brightness).hex
|
44
|
+
end
|
45
|
+
|
46
|
+
def hex=(hex)
|
47
|
+
hex = "##{hex}" unless hex.start_with?("#")
|
48
|
+
hsb = ColorConversion::Color.new(hex).hsb
|
49
|
+
|
50
|
+
# Map values from standard HSB to what Hue wants and update state
|
51
|
+
state = {
|
52
|
+
hue: ((hsb[:h].to_f / 360.0) * HUE_RANGE.last.to_f).to_i,
|
53
|
+
saturation: ((hsb[:s].to_f / 100.0) * SATURATION_RANGE.last.to_f).to_i,
|
54
|
+
brightness: ((hsb[:b].to_f / 100.0) * BRIGHTNESS_RANGE.last.to_f).to_i
|
55
|
+
}
|
56
|
+
|
57
|
+
set_state(state)
|
58
|
+
|
59
|
+
@hue = state[:hue]
|
60
|
+
@saturation = state[:saturation]
|
61
|
+
@brightness = state[:brightness]
|
62
|
+
end
|
26
63
|
end
|
27
64
|
end
|
data/lib/hue/errors.rb
CHANGED
@@ -2,33 +2,47 @@ module Hue
|
|
2
2
|
class Error < StandardError; end
|
3
3
|
|
4
4
|
class UnauthorizedUser < Error; end
|
5
|
+
|
5
6
|
class InvalidJSON < Error; end
|
7
|
+
|
6
8
|
class ResourceNotAvailable < Error; end
|
9
|
+
|
7
10
|
class MethodNotAvailable < Error; end
|
11
|
+
|
8
12
|
class MissingBody < Error; end
|
13
|
+
|
9
14
|
class ParameterNotAvailable < Error; end
|
15
|
+
|
10
16
|
class InvalidValueForParameter < Error; end
|
17
|
+
|
11
18
|
class ParameterNotModifiable < Error; end
|
19
|
+
|
12
20
|
class InternalError < Error; end
|
21
|
+
|
13
22
|
class LinkButtonNotPressed < Error; end
|
23
|
+
|
14
24
|
class ParameterNotModifiableWhileOff < ParameterNotModifiable; end
|
25
|
+
|
15
26
|
class TooManyGroups < Error; end
|
27
|
+
|
16
28
|
class GroupTooFull < Error; end
|
17
29
|
|
18
30
|
class InvalidUsername < Error; end
|
31
|
+
|
19
32
|
class UnknownError < Error; end
|
33
|
+
|
20
34
|
class NoBridgeFound < Error; end
|
21
35
|
|
22
36
|
# Status code to exception map
|
23
37
|
ERROR_MAP = {
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
38
|
+
1 => Hue::UnauthorizedUser,
|
39
|
+
2 => Hue::InvalidJSON,
|
40
|
+
3 => Hue::ResourceNotAvailable,
|
41
|
+
4 => Hue::MethodNotAvailable,
|
42
|
+
5 => Hue::MissingBody,
|
43
|
+
6 => Hue::ParameterNotAvailable,
|
44
|
+
7 => Hue::InvalidValueForParameter,
|
45
|
+
8 => Hue::ParameterNotModifiable,
|
32
46
|
901 => Hue::InternalError,
|
33
47
|
101 => Hue::LinkButtonNotPressed,
|
34
48
|
201 => Hue::ParameterNotModifiableWhileOff,
|
data/lib/hue/group.rb
CHANGED
@@ -11,19 +11,19 @@ module Hue
|
|
11
11
|
attr_reader :bridge
|
12
12
|
|
13
13
|
# A unique, editable name given to the group.
|
14
|
-
|
14
|
+
attr_reader :name
|
15
15
|
|
16
16
|
# Hue of the group. This is a wrapping value between 0 and 65535.
|
17
17
|
# Both 0 and 65535 are red, 25500 is green and 46920 is blue.
|
18
18
|
attr_accessor :hue
|
19
19
|
|
20
|
-
# Saturation of the group.
|
20
|
+
# Saturation of the group. 254 is the most saturated (colored)
|
21
21
|
# and 0 is the least saturated (white).
|
22
22
|
attr_accessor :saturation
|
23
23
|
|
24
24
|
# Brightness of the group. This is a scale from the minimum
|
25
25
|
# brightness the group is capable of, 0, to the maximum capable
|
26
|
-
# brightness,
|
26
|
+
# brightness, 254. Note a brightness of 0 is not off.
|
27
27
|
attr_accessor :brightness
|
28
28
|
|
29
29
|
# The x coordinate of a color in CIE color space. Between 0 and 1.
|
@@ -58,16 +58,14 @@ module Hue
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def lights
|
61
|
-
@lights ||=
|
62
|
-
@
|
63
|
-
@client.light(light_id)
|
64
|
-
end
|
61
|
+
@lights ||= @light_ids.map do |light_id|
|
62
|
+
@client.light(light_id)
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
68
66
|
def name=(name)
|
69
|
-
resp = set_group_state({:
|
70
|
-
@name = new? ? name : resp[0][
|
67
|
+
resp = set_group_state({name: name})
|
68
|
+
@name = new? ? name : resp[0]["success"]["/groups/#{id}/name"]
|
71
69
|
end
|
72
70
|
|
73
71
|
def lights=(light_ids)
|
@@ -78,17 +76,17 @@ module Hue
|
|
78
76
|
@light_ids = light_ids.uniq
|
79
77
|
@lights = nil # resets the memoization
|
80
78
|
|
81
|
-
set_group_state({:
|
79
|
+
set_group_state({lights: @light_ids})
|
82
80
|
end
|
83
81
|
|
84
82
|
def scene=(scene)
|
85
83
|
scene_id = scene.is_a?(Scene) ? scene.id : scene
|
86
|
-
set_group_state({:
|
84
|
+
set_group_state({scene: scene_id})
|
87
85
|
end
|
88
86
|
|
89
87
|
def <<(light_id)
|
90
88
|
@light_ids << light_id
|
91
|
-
set_group_state({:
|
89
|
+
set_group_state({lights: @light_ids})
|
92
90
|
end
|
93
91
|
alias_method :add_light, :<<
|
94
92
|
|
@@ -120,8 +118,8 @@ module Hue
|
|
120
118
|
|
121
119
|
def create!
|
122
120
|
body = {
|
123
|
-
:
|
124
|
-
:
|
121
|
+
name: @name,
|
122
|
+
lights: @light_ids
|
125
123
|
}
|
126
124
|
|
127
125
|
uri = URI.parse("http://#{@bridge.ip}/api/#{@client.username}/groups")
|
@@ -129,7 +127,7 @@ module Hue
|
|
129
127
|
response = http.request_post(uri.path, JSON.dump(body))
|
130
128
|
json = JSON(response.body)
|
131
129
|
|
132
|
-
@id = json[0][
|
130
|
+
@id = json[0]["success"]["id"]
|
133
131
|
end
|
134
132
|
|
135
133
|
def destroy!
|
@@ -137,7 +135,7 @@ module Hue
|
|
137
135
|
http = Net::HTTP.new(uri.host)
|
138
136
|
response = http.delete(uri.path)
|
139
137
|
json = JSON(response.body)
|
140
|
-
@id = nil if json[0][
|
138
|
+
@id = nil if json[0]["success"]
|
141
139
|
end
|
142
140
|
|
143
141
|
def new?
|
@@ -147,22 +145,22 @@ module Hue
|
|
147
145
|
private
|
148
146
|
|
149
147
|
GROUP_KEYS_MAP = {
|
150
|
-
:
|
151
|
-
:
|
152
|
-
:
|
153
|
-
:
|
148
|
+
name: :name,
|
149
|
+
light_ids: :lights,
|
150
|
+
type: :type,
|
151
|
+
state: :action
|
154
152
|
}
|
155
153
|
|
156
154
|
STATE_KEYS_MAP = {
|
157
|
-
:
|
158
|
-
:
|
159
|
-
:
|
160
|
-
:
|
161
|
-
:
|
162
|
-
:
|
163
|
-
:
|
164
|
-
:
|
165
|
-
:
|
155
|
+
on: :on,
|
156
|
+
brightness: :bri,
|
157
|
+
hue: :hue,
|
158
|
+
saturation: :sat,
|
159
|
+
xy: :xy,
|
160
|
+
color_temperature: :ct,
|
161
|
+
alert: :alert,
|
162
|
+
effect: :effect,
|
163
|
+
color_mode: :colormode
|
166
164
|
}
|
167
165
|
|
168
166
|
def unpack(data)
|
@@ -170,7 +168,7 @@ module Hue
|
|
170
168
|
|
171
169
|
unless new?
|
172
170
|
unpack_hash(@state, STATE_KEYS_MAP)
|
173
|
-
@x, @y = @state[
|
171
|
+
@x, @y = @state["xy"]
|
174
172
|
end
|
175
173
|
end
|
176
174
|
|
data/lib/hue/light.rb
CHANGED
@@ -3,9 +3,6 @@ module Hue
|
|
3
3
|
include TranslateKeys
|
4
4
|
include EditableState
|
5
5
|
|
6
|
-
HUE_RANGE = 0..65535
|
7
|
-
SATURATION_RANGE = 0..255
|
8
|
-
BRIGHTNESS_RANGE = 0..255
|
9
6
|
COLOR_TEMPERATURE_RANGE = 153..500
|
10
7
|
|
11
8
|
# Unique identification number.
|
@@ -15,19 +12,19 @@ module Hue
|
|
15
12
|
attr_reader :bridge
|
16
13
|
|
17
14
|
# A unique, editable name given to the light.
|
18
|
-
|
15
|
+
attr_reader :name
|
19
16
|
|
20
17
|
# Hue of the light. This is a wrapping value between 0 and 65535.
|
21
18
|
# Both 0 and 65535 are red, 25500 is green and 46920 is blue.
|
22
19
|
attr_reader :hue
|
23
20
|
|
24
|
-
# Saturation of the light.
|
21
|
+
# Saturation of the light. 254 is the most saturated (colored)
|
25
22
|
# and 0 is the least saturated (white).
|
26
23
|
attr_reader :saturation
|
27
24
|
|
28
25
|
# Brightness of the light. This is a scale from the minimum
|
29
26
|
# brightness the light is capable of, 0, to the maximum capable
|
30
|
-
# brightness,
|
27
|
+
# brightness, 254. Note a brightness of 0 is not off.
|
31
28
|
attr_reader :brightness
|
32
29
|
|
33
30
|
# The x coordinate of a color in CIE color space. Between 0 and 1.
|
@@ -84,6 +81,15 @@ module Hue
|
|
84
81
|
# Reserved for future functionality.
|
85
82
|
attr_reader :point_symbol
|
86
83
|
|
84
|
+
# The unique ID of the light.
|
85
|
+
attr_reader :uid
|
86
|
+
|
87
|
+
# The hash of capabilities of the light
|
88
|
+
attr_reader :capabilities
|
89
|
+
|
90
|
+
# The config hash
|
91
|
+
attr_reader :config
|
92
|
+
|
87
93
|
def initialize(client, bridge, id, hash)
|
88
94
|
@client = client
|
89
95
|
@bridge = bridge
|
@@ -92,21 +98,21 @@ module Hue
|
|
92
98
|
end
|
93
99
|
|
94
100
|
def name=(new_name)
|
95
|
-
unless (1..32).
|
96
|
-
raise InvalidValueForParameter,
|
101
|
+
unless (1..32).cover?(new_name.length)
|
102
|
+
raise InvalidValueForParameter, "name must be between 1 and 32 characters."
|
97
103
|
end
|
98
104
|
|
99
105
|
body = {
|
100
|
-
:
|
106
|
+
name: new_name
|
101
107
|
}
|
102
108
|
|
103
109
|
uri = URI.parse(base_url)
|
104
110
|
http = Net::HTTP.new(uri.host)
|
105
111
|
response = http.request_put(uri.path, JSON.dump(body))
|
106
112
|
response = JSON(response.body).first
|
107
|
-
if response[
|
113
|
+
if response["success"]
|
108
114
|
@name = new_name
|
109
|
-
|
115
|
+
# else
|
110
116
|
# TODO: Error
|
111
117
|
end
|
112
118
|
end
|
@@ -115,7 +121,7 @@ module Hue
|
|
115
121
|
# always returns true, functionality will be added in a future
|
116
122
|
# patch.
|
117
123
|
def reachable?
|
118
|
-
@state[
|
124
|
+
@state["reachable"]
|
119
125
|
end
|
120
126
|
|
121
127
|
# @param transition The duration of the transition from the light’s current
|
@@ -126,7 +132,7 @@ module Hue
|
|
126
132
|
body = translate_keys(attributes, STATE_KEYS_MAP)
|
127
133
|
|
128
134
|
# Add transition
|
129
|
-
body
|
135
|
+
body[:transitiontime] = transition if transition
|
130
136
|
|
131
137
|
uri = URI.parse("#{base_url}/state")
|
132
138
|
http = Net::HTTP.new(uri.host)
|
@@ -140,34 +146,37 @@ module Hue
|
|
140
146
|
unpack(json)
|
141
147
|
end
|
142
148
|
|
143
|
-
|
149
|
+
private
|
144
150
|
|
145
151
|
KEYS_MAP = {
|
146
|
-
:
|
147
|
-
:
|
148
|
-
:
|
149
|
-
:
|
150
|
-
:
|
151
|
-
:
|
152
|
+
state: :state,
|
153
|
+
type: :type,
|
154
|
+
name: :name,
|
155
|
+
model: :modelid,
|
156
|
+
software_version: :swversion,
|
157
|
+
point_symbol: :pointsymbol,
|
158
|
+
uid: :uniqueid,
|
159
|
+
capabilities: :capabilities,
|
160
|
+
config: :config
|
152
161
|
}
|
153
162
|
|
154
163
|
STATE_KEYS_MAP = {
|
155
|
-
:
|
156
|
-
:
|
157
|
-
:
|
158
|
-
:
|
159
|
-
:
|
160
|
-
:
|
161
|
-
:
|
162
|
-
:
|
163
|
-
:
|
164
|
-
:
|
164
|
+
on: :on,
|
165
|
+
brightness: :bri,
|
166
|
+
hue: :hue,
|
167
|
+
saturation: :sat,
|
168
|
+
xy: :xy,
|
169
|
+
color_temperature: :ct,
|
170
|
+
alert: :alert,
|
171
|
+
effect: :effect,
|
172
|
+
color_mode: :colormode,
|
173
|
+
reachable: :reachable
|
165
174
|
}
|
166
175
|
|
167
176
|
def unpack(hash)
|
168
177
|
unpack_hash(hash, KEYS_MAP)
|
169
178
|
unpack_hash(@state, STATE_KEYS_MAP)
|
170
|
-
@x, @y = @state[
|
179
|
+
@x, @y = @state["xy"]
|
171
180
|
end
|
172
181
|
|
173
182
|
def base_url
|
data/lib/hue/scene.rb
CHANGED
@@ -24,19 +24,17 @@ module Hue
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def lights
|
27
|
-
@lights ||=
|
28
|
-
@
|
29
|
-
@client.light(light_id)
|
30
|
-
end
|
27
|
+
@lights ||= @light_ids.map do |light_id|
|
28
|
+
@client.light(light_id)
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
34
32
|
private
|
35
33
|
|
36
34
|
SCENE_KEYS_MAP = {
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
35
|
+
name: :name,
|
36
|
+
light_ids: :lights,
|
37
|
+
active: :active
|
40
38
|
}
|
41
39
|
|
42
40
|
def unpack(data)
|
data/lib/hue/translate_keys.rb
CHANGED
data/lib/hue/version.rb
CHANGED
data/lib/hue.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
1
|
+
require "hue/version"
|
2
|
+
require "hue/errors"
|
3
|
+
require "hue/client"
|
4
|
+
require "hue/bridge"
|
5
|
+
require "hue/editable_state"
|
6
|
+
require "hue/translate_keys"
|
7
|
+
require "hue/light"
|
8
|
+
require "hue/group"
|
9
|
+
require "hue/scene"
|
10
10
|
|
11
11
|
module Hue
|
12
12
|
USERNAME_RANGE = 10..40
|
data/test/hue/client_test.rb
CHANGED
@@ -1,25 +1,26 @@
|
|
1
|
-
require
|
1
|
+
require "test_helper"
|
2
2
|
|
3
3
|
class ClientTest < Minitest::Test
|
4
4
|
def before_setup
|
5
5
|
super
|
6
6
|
|
7
|
-
stub_request(:get, "https://discovery.meethue.com/")
|
8
|
-
to_return(:
|
7
|
+
stub_request(:get, "https://discovery.meethue.com/")
|
8
|
+
.to_return(body: '[{"id":"ffa57b3b257200065704","internalipaddress":"192.168.0.1"},{"id":"63c2fc01391276a319f9","internalipaddress":"192.168.0.2"}]')
|
9
9
|
|
10
|
-
stub_request(:get, %r{http://192.168.0.1/api
|
11
|
-
stub_request(:get, %r{http://192.168.0.
|
10
|
+
stub_request(:get, %r{http://192.168.0.1/api}).to_return(body: '[{"success":true}]')
|
11
|
+
stub_request(:get, %r{http://192.168.0.1/api/*}).to_return(body: '[{"success":true}]')
|
12
|
+
stub_request(:get, %r{http://192.168.0.2/api/*}).to_return(body: '[{"success":true}]')
|
12
13
|
end
|
13
14
|
|
14
15
|
def test_with_bridge_id
|
15
16
|
client = Hue::Client.new(use_mdns: false)
|
16
|
-
client.stub :find_bridge_id,
|
17
|
-
assert_equal
|
17
|
+
client.stub :find_bridge_id, "63c2fc01391276a319f9" do
|
18
|
+
assert_equal "63c2fc01391276a319f9", client.bridge.id
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
22
|
def test_without_bridge_id
|
22
23
|
client = Hue::Client.new(use_mdns: false)
|
23
|
-
assert_equal
|
24
|
+
assert_equal "ffa57b3b257200065704", client.bridge.id
|
24
25
|
end
|
25
26
|
end
|
data/test/hue/light_test.rb
CHANGED
@@ -1,24 +1,44 @@
|
|
1
|
-
require
|
1
|
+
require "test_helper"
|
2
2
|
|
3
3
|
class LightTest < Minitest::Test
|
4
4
|
def before_setup
|
5
5
|
super
|
6
6
|
|
7
|
-
stub_request(:get, "https://discovery.meethue.com/")
|
8
|
-
to_return(:
|
7
|
+
stub_request(:get, "https://discovery.meethue.com/")
|
8
|
+
.to_return(body: '[{"internalipaddress":"localhost"}]')
|
9
9
|
|
10
|
-
stub_request(:get, %r{http://localhost/api/*}).to_return(:
|
11
|
-
stub_request(:post,
|
12
|
-
stub_request(:put, %r{http://localhost/api*}).to_return(:
|
10
|
+
stub_request(:get, %r{http://localhost/api/*}).to_return(body: '[{"success":true}]')
|
11
|
+
stub_request(:post, "http://localhost/api").to_return(body: '[{"success":{"username":"ruby"}}]')
|
12
|
+
stub_request(:put, %r{http://localhost/api*}).to_return(body: "[{}]")
|
13
13
|
end
|
14
14
|
|
15
|
-
%w
|
16
|
-
define_method "test_setting_#{attribute}" do
|
15
|
+
%w[on hue saturation brightness color_temperature alert effect].each do |attribute|
|
16
|
+
define_method :"test_setting_#{attribute}" do
|
17
17
|
client = Hue::Client.new(use_mdns: false)
|
18
18
|
light = Hue::Light.new(client, client.bridge, 0, {"state" => {}})
|
19
19
|
|
20
|
-
light.send("#{attribute}=", 24)
|
20
|
+
light.send(:"#{attribute}=", 24)
|
21
21
|
assert_requested :put, %r{http://localhost/api/.*/lights/0}
|
22
22
|
end
|
23
23
|
end
|
24
|
+
|
25
|
+
def test_toggle_while_off
|
26
|
+
client = Hue::Client.new(use_mdns: false)
|
27
|
+
light = Hue::Light.new(client, client.bridge, 0, {"state" => {}})
|
28
|
+
assert_equal false, light.on?
|
29
|
+
|
30
|
+
light.toggle!
|
31
|
+
assert_requested :put, %r{http://localhost/api/.*/lights/0}
|
32
|
+
assert_equal true, light.on?
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_toggle_while_on
|
36
|
+
client = Hue::Client.new(use_mdns: false)
|
37
|
+
light = Hue::Light.new(client, client.bridge, 0, {"state" => {"on" => true}})
|
38
|
+
assert_equal true, light.on?
|
39
|
+
|
40
|
+
light.toggle!
|
41
|
+
assert_requested :put, %r{http://localhost/api/.*/lights/0}
|
42
|
+
assert_equal false, light.on?
|
43
|
+
end
|
24
44
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
$LOAD_PATH.unshift File.expand_path(
|
2
|
-
require
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require "hue"
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
4
|
+
require "minitest"
|
5
|
+
require "webmock/minitest"
|
6
|
+
require "minitest/autorun"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Soffes
|
@@ -39,21 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - '='
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 0.4.0
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - '='
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 0.4.0
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: curb
|
42
|
+
name: color_conversion
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
58
44
|
requirements:
|
59
45
|
- - ">="
|
@@ -66,48 +52,23 @@ dependencies:
|
|
66
52
|
- - ">="
|
67
53
|
- !ruby/object:Gem::Version
|
68
54
|
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: minitest
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '5.0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '5.0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: webmock
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
55
|
description: Work with Philips Hue light bulbs.
|
98
56
|
email:
|
99
57
|
- sam@soff.es
|
100
58
|
executables:
|
59
|
+
- console
|
101
60
|
- hue
|
102
61
|
extensions: []
|
103
62
|
extra_rdoc_files: []
|
104
63
|
files:
|
105
64
|
- ".github/workflows/main.yml"
|
106
65
|
- ".gitignore"
|
66
|
+
- ".ruby-version"
|
107
67
|
- Gemfile
|
108
68
|
- LICENSE
|
109
69
|
- README.md
|
110
70
|
- Rakefile
|
71
|
+
- bin/console
|
111
72
|
- bin/hue
|
112
73
|
- hue.gemspec
|
113
74
|
- lib/hue.rb
|
@@ -147,7 +108,4 @@ rubygems_version: 3.4.22
|
|
147
108
|
signing_key:
|
148
109
|
specification_version: 4
|
149
110
|
summary: Work with Philips Hue light bulbs from Ruby.
|
150
|
-
test_files:
|
151
|
-
- test/hue/client_test.rb
|
152
|
-
- test/hue/light_test.rb
|
153
|
-
- test/test_helper.rb
|
111
|
+
test_files: []
|