hue 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 06f9987573ff54e807b7cb86f72237c6ed8c825f
4
- data.tar.gz: 1150180432174f58f9e19d4f9fa45cb8f98ddd99
2
+ SHA256:
3
+ metadata.gz: 17b9710dcfb0defef023c1adb4c4da4ca86fddc6f2e6c0d971bcb562c57ec61a
4
+ data.tar.gz: 8d8afd006ca7892d74f33af84f4040aedd0718b13583e1708a6994bb7803c3d9
5
5
  SHA512:
6
- metadata.gz: d5b848f3b17020b64327b235be4ef897b3c31d41aadb69b245b14ab8b0b7caa3a7b2e0469bd8cac8deb20cf1bfdaa67d27f52f5f7ec9e598d48b44f5e2a7a892
7
- data.tar.gz: a01dcd6e0385d5b327c34fa308d6fd37381073b22270af0a692e498ef2f2172dab8a045e3d90f24fd0850d29f2684765705f44b3a2eb68c7202f902f3b5d9b49
6
+ metadata.gz: 8d3a2e81c450c2d1dd5c94ade3cf146ea4f56f4102f710051c54185902c388f9c9494a6f597f3f0ee05b59d0b0ade9ba837c2a7983df9679ce14ea34dd07e9eb
7
+ data.tar.gz: eea449790732dbcfcd0795f1e01af3d6d1a19b800c3b41404fe0bd1a12a76fa706f59269f90320365dde650c6425811fca9bd0ea38558945e8d8c0800badcdc4
@@ -0,0 +1,29 @@
1
+ name: Tests
2
+ on: [push]
3
+ jobs:
4
+ test:
5
+ name: Test
6
+ runs-on: ubuntu-latest
7
+ timeout-minutes: 5
8
+ steps:
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
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
@@ -1,3 +1,4 @@
1
+ .DS_Store
1
2
  *.gem
2
3
  *.rbc
3
4
  .bundle
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/Gemfile CHANGED
@@ -1,5 +1,8 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rake'
5
+ gem "rake", "~> 13.0"
6
+ gem "minitest", "~> 5.0"
7
+ gem "standard", "~> 1.3"
8
+ gem "webmock", "~> 3.19"
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2014 Sam Soffes, http://soff.es
1
+ Copyright (c) 2013-2024 Sam Soffes, https://soff.es
2
2
 
3
3
  MIT License
4
4
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  Work with Philips Hue light bulbs from Ruby.
4
4
 
5
- [![Code Climate](https://codeclimate.com/github/soffes/hue.png)](https://codeclimate.com/github/soffes/hue) [![Dependency Status](https://gemnasium.com/soffes/hue.png)](https://gemnasium.com/soffes/hue) [![Gem Version](https://badge.fury.io/rb/hue.png)](http://badge.fury.io/rb/hue)
6
5
 
7
6
  ## Installation
8
7
 
@@ -78,7 +77,3 @@ group.new? # => false
78
77
  # Destroying groups
79
78
  client.groups.last.destroy!
80
79
  ```
81
-
82
- ## Contributing
83
-
84
- See the [contributing guide](Contributing.markdown).
data/Rakefile CHANGED
@@ -1,10 +1,12 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
3
 
3
- begin
4
- require 'rspec/core/rake_task'
5
- RSpec::Core::RakeTask.new(:spec)
6
-
7
- task :default => :spec
8
- rescue LoadError
9
- # no rspec available
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
10
8
  end
9
+
10
+ require "standard/rake"
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'hue'
5
+
6
+ require 'irb'
7
+ IRB.start(__FILE__)
data/bin/hue CHANGED
@@ -5,4 +5,8 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'hue'
6
6
  require 'hue/cli'
7
7
 
8
- Hue::Cli.start
8
+ begin
9
+ Hue::Cli.start
10
+ rescue Hue::LinkButtonNotPressed
11
+ abort("Error: Press the link button on your bridge and then run this command again within 30 seconds of pressing it.")
12
+ end
data/hue.gemspec CHANGED
@@ -1,28 +1,23 @@
1
- # coding: utf-8
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 'hue/version'
3
+ require "hue/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = 'hue'
8
- spec.version = Hue::VERSION
9
- spec.authors = ['Sam Soffes']
10
- spec.email = ['sam@soff.es']
11
- spec.description = 'Work with Philips Hue light bulbs.'
12
- spec.summary = 'Work with Philips Hue light bulbs from Ruby.'
13
- spec.homepage = 'https://github.com/soffes/hue'
14
- spec.license = 'MIT'
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 = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
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 = '>= 1.9.3'
22
- spec.add_dependency 'thor'
23
- spec.add_dependency 'json'
24
- spec.add_dependency 'log_switch', '0.4.0'
25
- spec.add_dependency 'curb'
26
- spec.add_development_dependency 'rspec', '~> 3.2.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,32 +46,35 @@ module Hue
46
46
  # Current time stored on the bridge.
47
47
  def utc
48
48
  json = get_configuration
49
- DateTime.parse(json['utc'])
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['linkbutton']
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['portalservices']
63
+ json["portalservices"]
64
64
  end
65
65
 
66
66
  def refresh
67
67
  json = get_configuration
68
68
  unpack(json)
69
+ @lights = nil
70
+ @groups = nil
71
+ @scenes = nil
69
72
  end
70
73
 
71
74
  def lights
72
75
  @lights ||= begin
73
76
  json = JSON(Net::HTTP.get(URI.parse(base_url)))
74
- json['lights'].map do |key, value|
77
+ json["lights"].map do |key, value|
75
78
  Light.new(@client, self, key, value)
76
79
  end
77
80
  end
@@ -81,7 +84,7 @@ module Hue
81
84
  uri = URI.parse("#{base_url}/lights")
82
85
  http = Net::HTTP.new(uri.host)
83
86
  response = http.request_post(uri.path, nil)
84
- (response.body).first
87
+ response.body.first
85
88
  end
86
89
 
87
90
  def groups
@@ -102,27 +105,27 @@ module Hue
102
105
  end
103
106
  end
104
107
 
105
- private
108
+ private
106
109
 
107
110
  KEYS_MAP = {
108
- :id => :id,
109
- :ip => :internalipaddress,
110
- :name => :name,
111
- :proxy_port => :proxyport,
112
- :software_update => :swupdate,
113
- :ip_whitelist => :whitelist,
114
- :software_version => :swversion,
115
- :proxy_address => :proxyaddress,
116
- :mac_address => :macaddress,
117
- :network_mask => :netmask,
118
- :portal_services => :portalservices,
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
119
122
  }
120
123
 
121
124
  def unpack(hash)
122
125
  KEYS_MAP.each do |local_key, remote_key|
123
126
  value = hash[remote_key.to_s]
124
127
  next unless value
125
- instance_variable_set("@#{local_key}", value)
128
+ instance_variable_set(:"@#{local_key}", value)
126
129
  end
127
130
  end
128
131
 
data/lib/hue/cli.rb CHANGED
@@ -1,67 +1,67 @@
1
- require 'thor'
1
+ require "thor"
2
2
 
3
3
  module Hue
4
4
  class Cli < Thor
5
- desc 'lights', 'Find all of the lights on your network'
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 'add LIGHTS', 'Search for new lights'
12
+ desc "add LIGHTS", "Search for new lights"
13
13
  def add(thing)
14
14
  case thing
15
- when 'lights'
15
+ when "lights"
16
16
  client.add_lights
17
17
  end
18
18
  end
19
19
 
20
- desc 'light ID STATE [COLOR]', 'Access a light'
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 --bri 25 \n
27
- hue all --hue 50000 --bri 200 --sat 240 \n
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, :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')
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 == 'on'
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 'light ID STATE [COLOR]', 'Access a light'
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 --bri 25 \n
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, :type => :numeric
52
- option :sat, :type => :numeric, :aliases => '--saturation'
53
- option :brightness, :type => :numeric, :aliases => '--brightness'
54
- option :alert, :type => :string
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 == 'on' || !(state == 'off'))
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 'groups', 'Find all light groups on your network'
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 'group ID STATE [COLOR]', 'Update a group of lights'
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 --bri 25
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, :type => :numeric
83
- option :sat, :type => :numeric, :aliases => '--saturation'
84
- option :brightness, :type => :numeric, :aliases => '--brightness'
85
- option :alert, :type => :string
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 == 'on' || !(state == 'off'))
91
+ body[:on] = (state == "on" || !(state == "off"))
92
92
  puts group.set_state(body) if body.length > 0
93
93
  end
94
94
 
95
- private
95
+ private
96
96
 
97
97
  def client
98
98
  @client ||= Hue::Client.new
data/lib/hue/client.rb CHANGED
@@ -1,15 +1,15 @@
1
- require 'net/http'
2
- require 'json'
3
- require 'curb'
1
+ require "net/http"
2
+ require "json"
3
+ require "resolv"
4
4
 
5
5
  module Hue
6
6
  class Client
7
7
  attr_reader :username
8
8
 
9
- def initialize(username = nil)
10
- username = find_username unless @username
11
-
12
- @username = username
9
+ def initialize(username = nil, use_mdns: true)
10
+ @bridge_id = nil
11
+ @username = username || find_username
12
+ @use_mdns = use_mdns
13
13
 
14
14
  if @username
15
15
  begin
@@ -23,8 +23,12 @@ module Hue
23
23
  end
24
24
 
25
25
  def bridge
26
- # Pick the first one for now. In theory, they should all do the same thing.
27
- bridge = bridges.first
26
+ @bridge_id ||= find_bridge_id
27
+ bridge = if @bridge_id
28
+ bridges.find { |b| b.id == @bridge_id }
29
+ else
30
+ bridges.first
31
+ end
28
32
  raise NoBridgeFound unless bridge
29
33
  bridge
30
34
  end
@@ -32,14 +36,8 @@ module Hue
32
36
  def bridges
33
37
  @bridges ||= begin
34
38
  bs = []
35
- easy = Curl::Easy.new
36
- easy.follow_location = true
37
- easy.max_redirects = 10
38
- easy.url = 'https://www.meethue.com/api/nupnp'
39
- easy.perform
40
- JSON(easy.body).each do |hash|
41
- bs << Bridge.new(self, hash)
42
- end
39
+ discovery_mdns(bs) if @use_mdns
40
+ discovery_meethue(bs) if bs.empty?
43
41
  bs
44
42
  end
45
43
  end
@@ -54,7 +52,7 @@ module Hue
54
52
 
55
53
  def light(id)
56
54
  id = id.to_s
57
- lights.select { |l| l.id == id }.first
55
+ lights.find { |l| l.id == id }
58
56
  end
59
57
 
60
58
  def groups
@@ -65,7 +63,7 @@ module Hue
65
63
  return Group.new(self, bridge) if id.nil?
66
64
 
67
65
  id = id.to_s
68
- groups.select { |g| g.id == id }.first
66
+ groups.find { |g| g.id == id }
69
67
  end
70
68
 
71
69
  def scenes
@@ -74,18 +72,18 @@ module Hue
74
72
 
75
73
  def scene(id)
76
74
  id = id.to_s
77
- scenes.select { |s| s.id == id }.first
75
+ scenes.find { |s| s.id == id }
78
76
  end
79
77
 
80
78
  private
81
79
 
82
80
  def find_username
83
- return ENV['HUE_USERNAME'] if ENV['HUE_USERNAME']
81
+ return ENV["HUE_USERNAME"] if ENV["HUE_USERNAME"]
84
82
 
85
- json = JSON(File.read(File.expand_path('~/.hue')))
86
- json['username']
83
+ json = JSON(File.read(File.expand_path("~/.hue")))
84
+ json["username"]
87
85
  rescue
88
- return nil
86
+ nil
89
87
  end
90
88
 
91
89
  def validate_user
@@ -95,35 +93,69 @@ module Hue
95
93
  response = response.first
96
94
  end
97
95
 
98
- if error = response['error']
96
+ if (error = response["error"])
99
97
  raise get_error(error)
100
98
  end
101
99
 
102
- response['success']
100
+ response["success"]
103
101
  end
104
102
 
105
103
  def register_user
106
104
  body = JSON.dump({
107
- devicetype: 'Ruby'
105
+ devicetype: "Ruby"
108
106
  })
109
107
 
110
108
  uri = URI.parse("http://#{bridge.ip}/api")
111
109
  http = Net::HTTP.new(uri.host)
112
110
  response = JSON(http.request_post(uri.path, body).body).first
113
111
 
114
- if error = response['error']
112
+ if (error = response["error"])
115
113
  raise get_error(error)
116
114
  end
117
115
 
118
- if @username = response['success']['username']
119
- File.write(File.expand_path('~/.hue'), JSON.dump({username: @username}))
116
+ if (@username = response["success"]["username"])
117
+ File.write(File.expand_path("~/.hue"), JSON.dump({username: @username}))
118
+ end
119
+ end
120
+
121
+ def find_bridge_id
122
+ return ENV["HUE_BRIDGE_ID"] if ENV["HUE_BRIDGE_ID"]
123
+
124
+ json = JSON(File.read(File.expand_path("~/.hue")))
125
+ json["bridge_id"]
126
+ rescue
127
+ nil
128
+ end
129
+
130
+ def discovery_mdns(bs)
131
+ resolver = Resolv::MDNS.new
132
+ resolver.timeouts = 10
133
+
134
+ resolver.each_resource("_hue._tcp.local", Resolv::DNS::Resource::IN::PTR) do |bridge_ptr|
135
+ bridge_target = resolver.getresource(bridge_ptr.name, Resolv::DNS::Resource::IN::SRV).target
136
+
137
+ bridge_hash = {
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
140
+ }
141
+
142
+ bs << Bridge.new(self, bridge_hash)
143
+ end
144
+ end
145
+
146
+ def discovery_meethue(bs)
147
+ uri = URI("https://discovery.meethue.com/")
148
+ response = Net::HTTP.get(uri)
149
+
150
+ JSON(response).each do |hash|
151
+ bs << Bridge.new(self, hash)
120
152
  end
121
153
  end
122
154
 
123
155
  def get_error(error)
124
156
  # Find error class and return instance
125
- klass = Hue::ERROR_MAP[error['type']] || UnknownError unless klass
126
- klass.new(error['description'])
157
+ klass ||= Hue::ERROR_MAP[error["type"]] || UnknownError
158
+ klass.new(error["description"])
127
159
  end
128
160
  end
129
161
  end
@@ -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
- @state['on']
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
- %w{on hue saturation brightness color_temperature alert effect}.each do |key|
16
- define_method "#{key}=".to_sym do |value|
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}".to_sym, value)
33
+ instance_variable_set(:"@#{key}", value)
19
34
  end
20
35
  end
21
36
 
22
37
  def set_xy(x, y)
23
- set_state({:xy => [x, y]})
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