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/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
RubyHue
|
2
|
+
================
|
3
|
+
|
4
|
+
This is a very early attempt to create a Ruby library for controlling the [Philips Hue](http://www.meethue.com) lighting system. The API has not yet been released, but there are [several](http://www.nerdblog.com/2012/10/a-day-with-philips-hue.html) [people](http://rsmck.co.uk/hue) working to figure it out.
|
5
|
+
|
6
|
+
# WARNING
|
7
|
+
All of this is very experimental and could permanently damage your awesome (but ridiculously expensive) lightbulbs. As such, exercise extreme caution.
|
8
|
+
|
9
|
+
## Getting Started
|
10
|
+
You can get a [great overview](http://rsmck.co.uk/hue) of the options and limitations of the lights from Ross McKillop.
|
11
|
+
|
12
|
+
You will need to find the IP address of your bridge unit and also generate a unique ID (UUID works great) for your controlling application and add them to the top of the `hue.rb` file.
|
13
|
+
|
14
|
+
You will need to use this information to register your app with the controller. This library does not do that at this time, so you will need to manually do that. I suggest following the *Registering Your Application* section of [Ross's overview](http://rsmck.co.uk/hue).
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
To begin using, fire up the irb console and load the `hue.rb` file.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
>> load 'hue.rb'
|
21
|
+
=> true
|
22
|
+
```
|
23
|
+
|
24
|
+
You can see all of the lights attached to your controller by querying the bridge.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
>> Hue::Bridge.identities
|
28
|
+
=> {"1"=>"Master Bedroom Dresser", "2"=>"Wife Bedside", "3"=>"Bedside (front)", "4"=>"Bedside (back)", "5"=>"Family Room Desk", "6"=>"Family Room", "7"=>"Living Room Square"}
|
29
|
+
```
|
30
|
+
|
31
|
+
If you know the ID number of a particular lamp, you can access it directly.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
>> b = Hue::Bulb.new(5)
|
35
|
+
=> #<Hue::Bulb:0x007fe35a3586b8 @id=5, @hub=#<Hue::Bridge:0x007fe35a358690 @light_id="5">>
|
36
|
+
|
37
|
+
# on/off
|
38
|
+
>> b.on?
|
39
|
+
=> false
|
40
|
+
|
41
|
+
>> b.on
|
42
|
+
=> true
|
43
|
+
|
44
|
+
>> b.on?
|
45
|
+
=> true
|
46
|
+
|
47
|
+
# settings
|
48
|
+
>> b.settings
|
49
|
+
=> {"ct"=>343, "on"=>true, "bri"=>240}
|
50
|
+
|
51
|
+
>> b.brightness = 128
|
52
|
+
=> 128
|
53
|
+
|
54
|
+
>> b.update hue: 45000, sat: 180
|
55
|
+
=> true
|
56
|
+
|
57
|
+
>> b.settings
|
58
|
+
=> {"hue"=>45000, "sat"=>180, "on"=>true, "bri"=>128}
|
59
|
+
|
60
|
+
# blinking
|
61
|
+
>> b.blinking?
|
62
|
+
=> false
|
63
|
+
|
64
|
+
>> b.blink
|
65
|
+
=> nil
|
66
|
+
|
67
|
+
>> b.blinking?
|
68
|
+
=> true
|
69
|
+
|
70
|
+
>> b.blink false
|
71
|
+
=> nil
|
72
|
+
|
73
|
+
>> b.blinking?
|
74
|
+
=> false
|
75
|
+
```
|
76
|
+
|
77
|
+
## Experimental
|
78
|
+
There is an experimental mode that attempts to simulate a candle flicker. This defaults to only flickering 15 times as it's really not the way the bridge or bulbs were designed to work. Additionally, this operates on the main thread so you really can't do anything else while it's running.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
>> b.candle
|
82
|
+
=> nil
|
83
|
+
```
|
84
|
+
|
85
|
+
The candle makes use of temporarily stashing the lamp's current settings before it starts and then restoring them upon completion. You can use this yourself with the `stash` and `restore` commands.
|
86
|
+
|
87
|
+
## Going Forward
|
88
|
+
There is still a lot of work to be done figuring out the various timer options of the hub, etc. Hopefully, the official API will be released in the near future and expose even more goodies that we're unaware of.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
7
|
+
t.rspec_opts = ["-fd", "-c"]
|
8
|
+
t.ruby_opts = ["-Ispec,lib"]
|
9
|
+
t.pattern = 'spec/**/*_spec.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
rescue LoadError
|
13
|
+
desc 'Spec rake task not available'
|
14
|
+
task :spec do
|
15
|
+
abort 'Spec rake task is not available. Be sure to install rspec as a gem or plugin'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => [:spec]
|
data/hue-lib.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "hue-lib"
|
6
|
+
s.version = '0.5.0'
|
7
|
+
s.authors = ["Birkir A. Barkarson", ""]
|
8
|
+
s.email = ["birkirb@stoicviking.net"]
|
9
|
+
s.homepage = "https://github.com/birkirb/hue-lib"
|
10
|
+
s.summary = %q{Ruby library for controlling Phillips Hue light bridge.}
|
11
|
+
s.description = s.summary
|
12
|
+
|
13
|
+
s.rubyforge_project = "hue-lib"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_runtime_dependency("json")
|
21
|
+
s.add_development_dependency("rspec", '>= 2.6.0')
|
22
|
+
s.add_development_dependency("mocha", '>= 0.9.0')
|
23
|
+
s.add_development_dependency("webmock", '>= 1.8.0')
|
24
|
+
end
|
data/lib/hue.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
APP_NAME = 'ruby_hue'
|
2
|
+
BASE = 'http://10.10.10.20/api'
|
3
|
+
# UUID = 'd79713a3433df3d972ba7c22cb1cc23e'
|
4
|
+
# Digest::MD5.hexdigest('aa4f6bc0-2045-0130-8cf0-0018de9ecdd0')
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'json'
|
8
|
+
require 'matrix'
|
9
|
+
|
10
|
+
RGB_MATRIX = Matrix[
|
11
|
+
[3.233358361244897, -1.5262682428425947, 0.27916711262124544],
|
12
|
+
[-0.8268442148395835, 2.466767560486707, 0.3323241608108406],
|
13
|
+
[0.12942207487871885, 0.19839858329512317, 2.0280912276039635]
|
14
|
+
]
|
15
|
+
|
16
|
+
require 'hue/bridge.rb'
|
17
|
+
require 'hue/bulb.rb'
|
18
|
+
require 'hue/config.rb'
|
19
|
+
|
20
|
+
module Hue
|
21
|
+
|
22
|
+
DEVICE_TYPE = "RubyHue"
|
23
|
+
|
24
|
+
def self.device_type
|
25
|
+
DEVICE_TYPE
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.config
|
29
|
+
Hue::Config.default
|
30
|
+
end
|
31
|
+
|
32
|
+
class Error < StandardError
|
33
|
+
attr_accessor :original_error
|
34
|
+
|
35
|
+
def initialize(message, original_error = nil)
|
36
|
+
super(message)
|
37
|
+
@original_error = original_error
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
if @original_error.nil?
|
42
|
+
super
|
43
|
+
else
|
44
|
+
"#{super}\nCause: #{@original_error.to_s}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module API
|
50
|
+
class Error < ::Hue::Error
|
51
|
+
def initialize(api_error)
|
52
|
+
@type = api_error['type']
|
53
|
+
@address = api_error['address']
|
54
|
+
super(api_error['description'])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/lib/hue/bridge.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'uuid'
|
3
|
+
# require 'singleton'
|
4
|
+
|
5
|
+
module Hue
|
6
|
+
class Bridge
|
7
|
+
# include Singleton
|
8
|
+
|
9
|
+
# # Remove
|
10
|
+
# def self.method_missing(method, *args, &block)
|
11
|
+
# if args.empty?
|
12
|
+
# self.instance.send method
|
13
|
+
# else
|
14
|
+
# self.instance.send method, *args
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
|
18
|
+
# Move to APP class
|
19
|
+
def self.register(host = BASE)
|
20
|
+
# TODO: Look for default config.
|
21
|
+
puts "Please press the button on bridge before continuing."
|
22
|
+
puts "Once done, press Enter to continue."
|
23
|
+
gets
|
24
|
+
secret = Digest::MD5.hexdigest(UUID.generate) # one time UUID
|
25
|
+
puts "Registering app...(#{secret})"
|
26
|
+
config = Hue::Config.new(host, secret)
|
27
|
+
instance.create(
|
28
|
+
URI.parse(config.base_uri),
|
29
|
+
{"username" => config.base_uri, "devicetype" => Hue.device_type}
|
30
|
+
)
|
31
|
+
config.write
|
32
|
+
end
|
33
|
+
|
34
|
+
# Move to APP class
|
35
|
+
def self.remove
|
36
|
+
config = Config.default
|
37
|
+
instance.delete(
|
38
|
+
URI.parse(config.base_uri),
|
39
|
+
{"username" => config.identifier}
|
40
|
+
)
|
41
|
+
config.delete
|
42
|
+
end
|
43
|
+
|
44
|
+
public
|
45
|
+
|
46
|
+
attr_reader :hue_config
|
47
|
+
|
48
|
+
def initialize(hue_config = Hue.config)
|
49
|
+
@hue_config = hue_config
|
50
|
+
end
|
51
|
+
|
52
|
+
def status
|
53
|
+
index(uri)
|
54
|
+
end
|
55
|
+
|
56
|
+
def lights
|
57
|
+
index(uri('lights'))
|
58
|
+
end
|
59
|
+
|
60
|
+
def light_names
|
61
|
+
lights.map { |k,v| "#{k}. #{v['name']}" }.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def config
|
65
|
+
index(uri('config'))
|
66
|
+
end
|
67
|
+
|
68
|
+
def schedules
|
69
|
+
index(uri('schedules'))
|
70
|
+
end
|
71
|
+
|
72
|
+
def bulbs
|
73
|
+
# puts status['lights'].inspect
|
74
|
+
@bulbs ||= lights.keys.map { |b| Bulb.new(self, b) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# def remove_schedule(schedule_id)
|
78
|
+
# delete uri('schedules', schedule_id)
|
79
|
+
# puts "Removed schedule #{schedule_id}"
|
80
|
+
# end
|
81
|
+
|
82
|
+
# def remove_all_schedules
|
83
|
+
# ids = schedules.keys.map(&:to_i).sort.reverse
|
84
|
+
# puts "Removing #{ids.size} schedule#{'s' if ids.size != 1}..."
|
85
|
+
# ids.each{|x| remove_schedule x}
|
86
|
+
# end
|
87
|
+
|
88
|
+
def get_light_state(id)
|
89
|
+
index(uri('lights', id))
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_light_state(id, state)
|
93
|
+
update(uri('lights', id, 'state'), state)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def uri(*args)
|
99
|
+
URI [hue_config.base_uri, hue_config.identifier, args].flatten.reject{|x| x.to_s.strip == ''}.join('/')
|
100
|
+
end
|
101
|
+
|
102
|
+
def index(url)
|
103
|
+
# json = Net::HTTP.get(url)
|
104
|
+
# JSON.parse(json)
|
105
|
+
request = Net::HTTP::Get.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
|
106
|
+
response = Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
|
107
|
+
display(response)
|
108
|
+
json = JSON.parse(response.body)
|
109
|
+
if json.is_a?(Array) && error = json.first['error']
|
110
|
+
raise Hue::API::Error.new(error)
|
111
|
+
else
|
112
|
+
json
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def update(url, settings = {})
|
117
|
+
request = Net::HTTP::Put.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
|
118
|
+
request.body = settings.to_json
|
119
|
+
display Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def delete(url, settings = {})
|
123
|
+
request = Net::HTTP::Delete.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
|
124
|
+
request.body = settings.to_json
|
125
|
+
display Net::HTTP.new(url.host, url.port).start{|http| http.request(request)}
|
126
|
+
end
|
127
|
+
|
128
|
+
def create(url, settings = {})
|
129
|
+
request = Net::HTTP::Post.new(url.request_uri, initheader = {'Content-Type' =>'application/json'})
|
130
|
+
request.body = settings.to_json
|
131
|
+
display Net::HTTP.new(url.host, url.port).start {|http| http.request(request) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def display(response = nil)
|
135
|
+
if response and response.code.to_s != '200'
|
136
|
+
# Output to logger
|
137
|
+
# puts "Response #{response.code} #{response.message}: #{JSON.parse(response.body).first}"
|
138
|
+
false
|
139
|
+
else
|
140
|
+
# Output to logger
|
141
|
+
# puts "Response #{response.code} #{response.message}: #{JSON.parse(response.body).first}"
|
142
|
+
true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
data/lib/hue/bulb.rb
ADDED
@@ -0,0 +1,458 @@
|
|
1
|
+
module Hue
|
2
|
+
class Bulb
|
3
|
+
|
4
|
+
public
|
5
|
+
|
6
|
+
attr_reader :id, :bridge
|
7
|
+
attr_accessor :options
|
8
|
+
|
9
|
+
def initialize(bridge, light_id, options = {})
|
10
|
+
@bridge = bridge
|
11
|
+
@id = light_id
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def refresh!
|
16
|
+
@status = bridge.get_light_state(id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def info
|
20
|
+
status.select do |k, value|
|
21
|
+
value.is_a?(String)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def state
|
26
|
+
status['state']
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](item)
|
30
|
+
state[item.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
status['name']
|
35
|
+
end
|
36
|
+
|
37
|
+
def name=(_name)
|
38
|
+
update(name: _name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def on?
|
42
|
+
self[:on]
|
43
|
+
end
|
44
|
+
|
45
|
+
def off?
|
46
|
+
!on?
|
47
|
+
end
|
48
|
+
|
49
|
+
def on
|
50
|
+
update(on: true)
|
51
|
+
on?
|
52
|
+
end
|
53
|
+
|
54
|
+
def off
|
55
|
+
update(on: false)
|
56
|
+
off?
|
57
|
+
end
|
58
|
+
|
59
|
+
def brightness
|
60
|
+
self[:bri]
|
61
|
+
end
|
62
|
+
|
63
|
+
alias :bri :brightness
|
64
|
+
|
65
|
+
def brightness=(bri)
|
66
|
+
update(bri: bri)
|
67
|
+
brightness
|
68
|
+
end
|
69
|
+
|
70
|
+
alias :bri= :brightness=
|
71
|
+
|
72
|
+
def hue
|
73
|
+
self[:hue]
|
74
|
+
end
|
75
|
+
|
76
|
+
HUE_MAX = 65536.0
|
77
|
+
HUE_DEGREES = 360
|
78
|
+
HUE_SCALE = HUE_MAX / HUE_DEGREES
|
79
|
+
|
80
|
+
def hue=(_hue)
|
81
|
+
_hue = (_hue * HUE_SCALE).to_i
|
82
|
+
update(hue: _hue)
|
83
|
+
hue
|
84
|
+
end
|
85
|
+
|
86
|
+
def saturation
|
87
|
+
self[:sat]
|
88
|
+
end
|
89
|
+
|
90
|
+
alias :sat :saturation
|
91
|
+
|
92
|
+
def saturation=(_sat)
|
93
|
+
update(sat: _sat)
|
94
|
+
sat
|
95
|
+
end
|
96
|
+
|
97
|
+
alias :sat= :saturation=
|
98
|
+
|
99
|
+
def color_temperature
|
100
|
+
self[:ct]
|
101
|
+
end
|
102
|
+
|
103
|
+
alias :ct :color_temperature
|
104
|
+
|
105
|
+
def color_temperature=(_ct)
|
106
|
+
update(ct: [[_ct, 154].max, 500].min)
|
107
|
+
colortemp
|
108
|
+
end
|
109
|
+
|
110
|
+
alias :ct= :color_temperature=
|
111
|
+
|
112
|
+
def color_mode
|
113
|
+
self[:colormode]
|
114
|
+
end
|
115
|
+
|
116
|
+
alias :colormode :color_mode
|
117
|
+
|
118
|
+
def blinking?
|
119
|
+
!solid?
|
120
|
+
end
|
121
|
+
|
122
|
+
def solid?
|
123
|
+
'none' == self['alert']
|
124
|
+
end
|
125
|
+
|
126
|
+
def blink
|
127
|
+
update(alert: 'lselect')
|
128
|
+
end
|
129
|
+
|
130
|
+
def solid
|
131
|
+
update(alert: 'none')
|
132
|
+
end
|
133
|
+
|
134
|
+
def flash
|
135
|
+
update(alert: 'select')
|
136
|
+
update(alert: 'none')
|
137
|
+
end
|
138
|
+
|
139
|
+
def transition_time
|
140
|
+
# transition time in seconds
|
141
|
+
(options[:transitiontime] || 1).to_f / 10
|
142
|
+
end
|
143
|
+
|
144
|
+
def transition_time=(time)
|
145
|
+
# transition time in seconds
|
146
|
+
self.options[:transitiontime] = (time * 10).to_i
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
def settings
|
152
|
+
options.merge case state['colormode']
|
153
|
+
when 'ct'
|
154
|
+
{'ct' => state['ct']}
|
155
|
+
when 'xy'
|
156
|
+
{'xy' => state['xy']}
|
157
|
+
when 'hs'
|
158
|
+
{'hue' => state['hue'], 'sat' => state['sat']}
|
159
|
+
end.merge('on' => state['on'], 'bri' => state['bri'])
|
160
|
+
end
|
161
|
+
|
162
|
+
def rgb
|
163
|
+
send %(#{colormode}_to_rgb)
|
164
|
+
end
|
165
|
+
|
166
|
+
def red
|
167
|
+
rgb[:red]
|
168
|
+
end
|
169
|
+
|
170
|
+
def green
|
171
|
+
rgb[:green]
|
172
|
+
end
|
173
|
+
|
174
|
+
def blue
|
175
|
+
rgb[:blue]
|
176
|
+
end
|
177
|
+
|
178
|
+
def red=(_red)
|
179
|
+
self.rgb = [_red, green, blue]
|
180
|
+
end
|
181
|
+
|
182
|
+
def green=(_green)
|
183
|
+
self.rgb = [red, _green, blue]
|
184
|
+
end
|
185
|
+
|
186
|
+
def blue=(_blue)
|
187
|
+
self.rgb = [red, green, _blue]
|
188
|
+
end
|
189
|
+
|
190
|
+
def kelvin
|
191
|
+
# convert colortemp setting to Kelvin
|
192
|
+
1000000 / self['ct']
|
193
|
+
end
|
194
|
+
|
195
|
+
def kelvin=(_temp)
|
196
|
+
self.colortemp = 1000000 / [_temp, 1].max
|
197
|
+
end
|
198
|
+
|
199
|
+
def ct_to_rgb
|
200
|
+
# using method described at
|
201
|
+
# http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
|
202
|
+
temp = kelvin / 100
|
203
|
+
|
204
|
+
red = temp <= 66 ? 255 : 329.698727446 * ((temp - 60) ** -0.1332047592)
|
205
|
+
|
206
|
+
green = if temp <= 66
|
207
|
+
99.4708025861 * Math.log(temp) - 161.1195681661
|
208
|
+
else
|
209
|
+
288.1221695283 * ((temp - 60) ** -0.0755148492)
|
210
|
+
end
|
211
|
+
|
212
|
+
blue = if temp >= 66
|
213
|
+
255
|
214
|
+
elsif temp <= 19
|
215
|
+
0
|
216
|
+
else
|
217
|
+
138.5177312231 * Math.log(temp - 10) - 305.0447927307
|
218
|
+
end
|
219
|
+
|
220
|
+
{ red: [[red, 0].max, 255].min.to_i,
|
221
|
+
green: [[green, 0].max, 255].min.to_i,
|
222
|
+
blue: [[blue, 0].max, 255].min.to_i
|
223
|
+
}
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
def xyz
|
228
|
+
vals = state['xy']
|
229
|
+
vals + [1 - vals.first - vals.last]
|
230
|
+
end
|
231
|
+
|
232
|
+
def xy_to_rgb
|
233
|
+
values = (RGB_MATRIX * Matrix[xyz].transpose).to_a.flatten.map{|x| [[x * 255, 0].max, 255].min.to_i}
|
234
|
+
{ red: values[0],
|
235
|
+
green: values[1],
|
236
|
+
blue: values[2]
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
def hue_in_degrees
|
241
|
+
self['hue'].to_f / HUE_SCALE
|
242
|
+
end
|
243
|
+
|
244
|
+
def hue_as_decimal
|
245
|
+
hue_in_degrees / HUE_DEGREES
|
246
|
+
end
|
247
|
+
|
248
|
+
def sat_as_decimal
|
249
|
+
self['sat'] / 255.0
|
250
|
+
end
|
251
|
+
|
252
|
+
def brightness_as_decimal
|
253
|
+
brightness / 255.0
|
254
|
+
end
|
255
|
+
|
256
|
+
def hs_to_rgb
|
257
|
+
h, s, v = hue_as_decimal, sat_as_decimal, brightness_as_decimal
|
258
|
+
if s == 0 #monochromatic
|
259
|
+
red = green = blue = v
|
260
|
+
else
|
261
|
+
|
262
|
+
v = 1.0 # We are setting the value to 1. Don't count brightness here
|
263
|
+
i = (h * 6).floor
|
264
|
+
f = h * 6 - i
|
265
|
+
p = v * (1 - s)
|
266
|
+
q = v * (1 - f * s)
|
267
|
+
t = v * (1 - (1 - f) * s)
|
268
|
+
|
269
|
+
case i % 6
|
270
|
+
when 0
|
271
|
+
red, green, blue = v, t, p
|
272
|
+
when 1
|
273
|
+
red, green, blue = q, v, p
|
274
|
+
when 2
|
275
|
+
red, green, blue = p, v, t
|
276
|
+
when 3
|
277
|
+
red, green, blue = p, q, v
|
278
|
+
when 4
|
279
|
+
red, green, blue = t, p, v
|
280
|
+
when 5
|
281
|
+
red, green, blue = v, p, q
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
{ red: [[red * 255, 0].max, 255].min.to_i,
|
286
|
+
green: [[green * 255, 0].max, 255].min.to_i,
|
287
|
+
blue: [[blue * 255, 0].max, 255].min.to_i
|
288
|
+
}
|
289
|
+
end
|
290
|
+
|
291
|
+
def rgb=(colors)
|
292
|
+
red, green, blue = colors[0] / 255.0, colors[1] / 255.0, colors[2] / 255.0
|
293
|
+
|
294
|
+
max = [red, green, blue].max
|
295
|
+
min = [red, green, blue].min
|
296
|
+
h, s, l = 0, 0, ((max + min) / 2 * 255)
|
297
|
+
|
298
|
+
d = max - min
|
299
|
+
s = max == 0 ? 0 : (d / max * 255)
|
300
|
+
|
301
|
+
h = case max
|
302
|
+
when min
|
303
|
+
0 # monochromatic
|
304
|
+
when red
|
305
|
+
(green - blue) / d + (green < blue ? 6 : 0)
|
306
|
+
when green
|
307
|
+
(blue - red) / d + 2
|
308
|
+
when blue
|
309
|
+
(red - green) / d + 4
|
310
|
+
end * 60 # / 6 * 360
|
311
|
+
|
312
|
+
h = (h * HUE_SCALE).to_i
|
313
|
+
update hue: h, sat: s.to_i#, bri: l.to_i
|
314
|
+
[h, s, 1.0]
|
315
|
+
end
|
316
|
+
|
317
|
+
def candle(repeat = 15)
|
318
|
+
# 0-65536 for hue, 182 per deg. Ideal 30-60 deg (5460-10920)
|
319
|
+
stash!
|
320
|
+
on if off?
|
321
|
+
|
322
|
+
repeat.times do
|
323
|
+
hue = ((rand * 3460) + 5460).to_i
|
324
|
+
sat = rand(64) + 170
|
325
|
+
bri = rand(32) + 16
|
326
|
+
|
327
|
+
delay = (rand * 0.35) + (@delay ||= 0)
|
328
|
+
update(hue: hue, sat: sat, bri: bri, transitiontime: (delay * 10).to_i)
|
329
|
+
sleep delay
|
330
|
+
end
|
331
|
+
restore!
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
def stash!
|
337
|
+
@stash ||= settings
|
338
|
+
end
|
339
|
+
|
340
|
+
def restore!
|
341
|
+
if stash
|
342
|
+
update(@stash)
|
343
|
+
unstash!
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def unstash!
|
348
|
+
@stash = nil
|
349
|
+
end
|
350
|
+
|
351
|
+
def status
|
352
|
+
@status || refresh!
|
353
|
+
end
|
354
|
+
|
355
|
+
def update(settings = {})
|
356
|
+
if bridge.set_light_state(id, options.merge(settings))
|
357
|
+
settings.each do |key, value|
|
358
|
+
@status['state'][key.to_s] = value # or refresh!
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Experimental Sunrise/Sunset action
|
364
|
+
# this will transition from off and warm light to on and daytime light
|
365
|
+
# in a curve that mimics the actual sunrise.
|
366
|
+
|
367
|
+
def perform_sunrise(total_time_in_minutes = 18)
|
368
|
+
# total_time / 18 steps == time_per_step
|
369
|
+
# the multiplier should be 600 * time per step
|
370
|
+
minutes_per_step = total_time_in_minutes / 18.0
|
371
|
+
multiplier = (minutes_per_step * 60 * 10).to_i
|
372
|
+
|
373
|
+
perform_sun_transition total_time_in_minutes, sunrise_steps(multiplier)
|
374
|
+
end
|
375
|
+
|
376
|
+
def perform_sunrise(total_time_in_minutes = 18)
|
377
|
+
multiplier = sunrise_multiplier total_time_in_minutes
|
378
|
+
steps = sunrise_steps(multiplier)
|
379
|
+
if on?
|
380
|
+
puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
|
381
|
+
while brightness >= steps[0][:bri]
|
382
|
+
steps.shift
|
383
|
+
end
|
384
|
+
end
|
385
|
+
steps.each_with_index do |step, i|
|
386
|
+
update step.merge(on: true)
|
387
|
+
sleep(step[:transitiontime] / 10.0)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def perform_sunset(total_time_in_minutes = 18)
|
392
|
+
multiplier = sunrise_multiplier total_time_in_minutes
|
393
|
+
steps = sunset_steps(multiplier)
|
394
|
+
if on?
|
395
|
+
puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
|
396
|
+
while brightness <= steps[0][:bri]
|
397
|
+
steps.shift
|
398
|
+
end
|
399
|
+
end
|
400
|
+
steps.each_with_index do |step, i|
|
401
|
+
update step.merge(on: true)
|
402
|
+
sleep(step[:transitiontime] / 10.0)
|
403
|
+
end
|
404
|
+
off
|
405
|
+
end
|
406
|
+
|
407
|
+
SUN_STEPS = [ 1.5, 2, 3, 1, 4, 2.5 ]
|
408
|
+
SUN_TIMES = [ 3, 3, 3, 1, 2, 1]
|
409
|
+
|
410
|
+
def sunrise_multiplier(total_time_in_minutes)
|
411
|
+
# total_time / 18 steps == time_per_step
|
412
|
+
# the multiplier should be 600 * time per step
|
413
|
+
minutes_per_step = total_time_in_minutes / 18.0
|
414
|
+
(minutes_per_step * 60 * 10).to_i
|
415
|
+
end
|
416
|
+
|
417
|
+
def sunrise_brightness
|
418
|
+
sun_bri_unit = 10
|
419
|
+
SUN_STEPS.inject([0]){|all, i| all << ((i * sun_bri_unit) + all[-1]).to_i } << 255
|
420
|
+
end
|
421
|
+
|
422
|
+
def sunrise_temps
|
423
|
+
sun_temp_unit = 16
|
424
|
+
SUN_STEPS.inject([500]){|all, i| all << (all[-1] - (i * sun_temp_unit)).to_i} << 200
|
425
|
+
end
|
426
|
+
|
427
|
+
def sunrise_times
|
428
|
+
[0, SUN_TIMES, 5].flatten
|
429
|
+
end
|
430
|
+
|
431
|
+
def sunset_times
|
432
|
+
[0, 5, SUN_TIMES.reverse].flatten
|
433
|
+
end
|
434
|
+
|
435
|
+
def sunrise_steps(multiplier = 600)
|
436
|
+
bri_steps = sunrise_brightness
|
437
|
+
tmp_steps = sunrise_temps
|
438
|
+
|
439
|
+
steps = []
|
440
|
+
sunrise_times.each_with_index do |t, i|
|
441
|
+
steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
|
442
|
+
end
|
443
|
+
steps
|
444
|
+
end
|
445
|
+
|
446
|
+
def sunset_steps(multiplier = 600)
|
447
|
+
bri_steps = sunrise_brightness.reverse
|
448
|
+
tmp_steps = sunrise_temps.reverse
|
449
|
+
|
450
|
+
steps = []
|
451
|
+
sunset_times.each_with_index do |t, i|
|
452
|
+
steps << {bri: bri_steps[i], ct: tmp_steps[i], transitiontime: (t * multiplier)}
|
453
|
+
end
|
454
|
+
steps
|
455
|
+
end
|
456
|
+
|
457
|
+
end
|
458
|
+
end
|