hue 0.2.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 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