huebot 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -6
- data/bin/huebot +2 -2
- data/lib/hue.rb +13 -0
- data/lib/hue/LICENSE +22 -0
- data/lib/hue/bridge.rb +140 -0
- data/lib/hue/client.rb +141 -0
- data/lib/hue/editable_state.rb +27 -0
- data/lib/hue/errors.rb +38 -0
- data/lib/hue/group.rb +181 -0
- data/lib/hue/light.rb +177 -0
- data/lib/hue/scene.rb +50 -0
- data/lib/hue/translate_keys.rb +21 -0
- data/lib/hue/version.rb +3 -0
- data/lib/huebot/version.rb +1 -1
- metadata +14 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec0452c2e2f31fbbf2fac6b5bfe817f19f9f312ed85eca34c5dcbf47d4280b58
|
4
|
+
data.tar.gz: a5412c05d81ba53ca81a2461d19d408e025c78298237459746c6ede4f376614b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58124a0670d16dd043a050a044f6f5e378a29ec5b95d28919e7bfafdc953f9bbcaf7c469bbe318a5710fecd4e6cfcfd07bd0366ee9fcbf63774841a0a2c022b1
|
7
|
+
data.tar.gz: 6fd6d42ee5c40e64a47a4b82916e980a828f0152ea8b541d95ec7a29a35f9222114e9fc2e9fb9ee7c810f0860be26878d886525dd46cf096820ae65e3de598b4
|
data/README.md
CHANGED
@@ -8,12 +8,6 @@ Orchestration and automation for Philips Hue devices. Huebot can be used as a Ru
|
|
8
8
|
|
9
9
|
This (very simple) program starts with the light(s) on at full brightness, then enters an infinite loop of slowly dimming and raising the light(s). Since no color is specified, the light(s) will retain whatever color they last had.
|
10
10
|
|
11
|
-
## Install
|
12
|
-
|
13
|
-
gem install huebot
|
14
|
-
|
15
|
-
The curl library headers are required. On Ubuntu they can be installed with `apt-get install libcurl4-openssl-dev`.
|
16
|
-
|
17
11
|
```yaml
|
18
12
|
initial:
|
19
13
|
switch: on
|
@@ -36,6 +30,16 @@ transitions:
|
|
36
30
|
|
37
31
|
The variable `$all` refers to all lights and/or groups passed in on the command line. They can be also referred to individually as `$1`, `$2`, `$3`, etc. The names of lights and groups can also be hard-coded into your program. [See examples in the Wiki.](https://github.com/jhollinger/huebot/wiki)
|
38
32
|
|
33
|
+
## Install
|
34
|
+
|
35
|
+
gem install huebot
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
Huebot is licensed under the MIT license (see LICENSE file).
|
40
|
+
|
41
|
+
A patched version of the "hue" gem is bundled in huebot's codebase (to remove a dependency that's unnecessarily annoying to install). The license for it can be found at `lib/hue/LICENSE`.
|
42
|
+
|
39
43
|
## UNDER ACTIVE DEVELOPMENT
|
40
44
|
|
41
45
|
**TODO**
|
data/bin/huebot
CHANGED
data/lib/hue.rb
ADDED
@@ -0,0 +1,13 @@
|
|
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
|
+
|
11
|
+
module Hue
|
12
|
+
USERNAME_RANGE = 10..40
|
13
|
+
end
|
data/lib/hue/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013-2014 Sam Soffes, http://soff.es
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/hue/bridge.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
module Hue
|
2
|
+
class Bridge
|
3
|
+
# ID of the bridge.
|
4
|
+
attr_reader :id
|
5
|
+
|
6
|
+
# Name of the bridge. This is also its uPnP name, so will reflect the
|
7
|
+
# actual uPnP name after any conflicts have been resolved.
|
8
|
+
attr_accessor :name
|
9
|
+
|
10
|
+
# IP address of the bridge.
|
11
|
+
attr_reader :ip
|
12
|
+
|
13
|
+
# MAC address of the bridge.
|
14
|
+
attr_reader :mac_address
|
15
|
+
|
16
|
+
# IP Address of the proxy server being used.
|
17
|
+
attr_reader :proxy_address
|
18
|
+
|
19
|
+
# Port of the proxy being used by the bridge. If set to 0 then a proxy is
|
20
|
+
# not being used.
|
21
|
+
attr_reader :proxy_port
|
22
|
+
|
23
|
+
# Software version of the bridge.
|
24
|
+
attr_reader :software_version
|
25
|
+
|
26
|
+
# Contains information related to software updates.
|
27
|
+
attr_reader :software_update
|
28
|
+
|
29
|
+
# An array of whitelisted user IDs.
|
30
|
+
attr_reader :ip_whitelist
|
31
|
+
|
32
|
+
# Network mask of the bridge.
|
33
|
+
attr_reader :network_mask
|
34
|
+
|
35
|
+
# Gateway IP address of the bridge.
|
36
|
+
attr_reader :gateway
|
37
|
+
|
38
|
+
# Whether the IP address of the bridge is obtained with DHCP.
|
39
|
+
attr_reader :dhcp
|
40
|
+
|
41
|
+
def initialize(client, hash)
|
42
|
+
@client = client
|
43
|
+
unpack(hash)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Current time stored on the bridge.
|
47
|
+
def utc
|
48
|
+
json = get_configuration
|
49
|
+
DateTime.parse(json['utc'])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Indicates whether the link button has been pressed within the last 30
|
53
|
+
# seconds.
|
54
|
+
def link_button_pressed?
|
55
|
+
json = get_configuration
|
56
|
+
json['linkbutton']
|
57
|
+
end
|
58
|
+
|
59
|
+
# This indicates whether the bridge is registered to synchronize data with a
|
60
|
+
# portal account.
|
61
|
+
def has_portal_services?
|
62
|
+
json = get_configuration
|
63
|
+
json['portalservices']
|
64
|
+
end
|
65
|
+
|
66
|
+
def refresh
|
67
|
+
json = get_configuration
|
68
|
+
unpack(json)
|
69
|
+
@lights = nil
|
70
|
+
@groups = nil
|
71
|
+
@scenes = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def lights
|
75
|
+
@lights ||= begin
|
76
|
+
json = JSON(Net::HTTP.get(URI.parse(base_url)))
|
77
|
+
json['lights'].map do |key, value|
|
78
|
+
Light.new(@client, self, key, value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def add_lights
|
84
|
+
uri = URI.parse("#{base_url}/lights")
|
85
|
+
http = Net::HTTP.new(uri.host)
|
86
|
+
response = http.request_post(uri.path, nil)
|
87
|
+
(response.body).first
|
88
|
+
end
|
89
|
+
|
90
|
+
def groups
|
91
|
+
@groups ||= begin
|
92
|
+
json = JSON(Net::HTTP.get(URI.parse("#{base_url}/groups")))
|
93
|
+
json.map do |id, data|
|
94
|
+
Group.new(@client, self, id, data)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def scenes
|
100
|
+
@scenes ||= begin
|
101
|
+
json = JSON(Net::HTTP.get(URI.parse("#{base_url}/scenes")))
|
102
|
+
json.map do |id, data|
|
103
|
+
Scene.new(@client, self, id, data)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
KEYS_MAP = {
|
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
|
+
}
|
123
|
+
|
124
|
+
def unpack(hash)
|
125
|
+
KEYS_MAP.each do |local_key, remote_key|
|
126
|
+
value = hash[remote_key.to_s]
|
127
|
+
next unless value
|
128
|
+
instance_variable_set("@#{local_key}", value)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_configuration
|
133
|
+
JSON(Net::HTTP.get(URI.parse("#{base_url}/config")))
|
134
|
+
end
|
135
|
+
|
136
|
+
def base_url
|
137
|
+
"http://#{ip}/api/#{@client.username}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/hue/client.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Hue
|
5
|
+
class Client
|
6
|
+
attr_reader :username
|
7
|
+
|
8
|
+
def initialize(username = nil)
|
9
|
+
@bridge_id = nil
|
10
|
+
@username = username || find_username
|
11
|
+
|
12
|
+
if @username
|
13
|
+
begin
|
14
|
+
validate_user
|
15
|
+
rescue Hue::UnauthorizedUser
|
16
|
+
register_user
|
17
|
+
end
|
18
|
+
else
|
19
|
+
register_user
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def bridge
|
24
|
+
@bridge_id = find_bridge_id unless @bridge_id
|
25
|
+
if @bridge_id
|
26
|
+
bridge = bridges.select { |b| b.id == @bridge_id }.first
|
27
|
+
else
|
28
|
+
bridge = bridges.first
|
29
|
+
end
|
30
|
+
raise NoBridgeFound unless bridge
|
31
|
+
bridge
|
32
|
+
end
|
33
|
+
|
34
|
+
# Patched by Jordan Hollinger to remove use of "curb" gem
|
35
|
+
# TODO handle redirects? That's what curb was used for.
|
36
|
+
def bridges
|
37
|
+
req = Net::HTTP::Get.new(URI("https://www.meethue.com/api/nupnp"))
|
38
|
+
resp = Net::HTTP.start req.uri.host, req.uri.port, {use_ssl: true} do |http|
|
39
|
+
http.request req
|
40
|
+
end
|
41
|
+
|
42
|
+
JSON(resp.body).lazy.map { |x|
|
43
|
+
Bridge.new(self, x)
|
44
|
+
}
|
45
|
+
|
46
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, JSON::ParserError
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
|
50
|
+
def lights
|
51
|
+
bridge.lights
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_lights
|
55
|
+
bridge.add_lights
|
56
|
+
end
|
57
|
+
|
58
|
+
def light(id)
|
59
|
+
id = id.to_s
|
60
|
+
lights.select { |l| l.id == id }.first
|
61
|
+
end
|
62
|
+
|
63
|
+
def groups
|
64
|
+
bridge.groups
|
65
|
+
end
|
66
|
+
|
67
|
+
def group(id = nil)
|
68
|
+
return Group.new(self, bridge) if id.nil?
|
69
|
+
|
70
|
+
id = id.to_s
|
71
|
+
groups.select { |g| g.id == id }.first
|
72
|
+
end
|
73
|
+
|
74
|
+
def scenes
|
75
|
+
bridge.scenes
|
76
|
+
end
|
77
|
+
|
78
|
+
def scene(id)
|
79
|
+
id = id.to_s
|
80
|
+
scenes.select { |s| s.id == id }.first
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def find_username
|
86
|
+
return ENV['HUE_USERNAME'] if ENV['HUE_USERNAME']
|
87
|
+
|
88
|
+
json = JSON(File.read(File.expand_path('~/.hue')))
|
89
|
+
json['username']
|
90
|
+
rescue
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_user
|
95
|
+
response = JSON(Net::HTTP.get(URI.parse("http://#{bridge.ip}/api/#{@username}")))
|
96
|
+
|
97
|
+
if response.is_a? Array
|
98
|
+
response = response.first
|
99
|
+
end
|
100
|
+
|
101
|
+
if error = response['error']
|
102
|
+
raise get_error(error)
|
103
|
+
end
|
104
|
+
|
105
|
+
response['success']
|
106
|
+
end
|
107
|
+
|
108
|
+
def register_user
|
109
|
+
body = JSON.dump({
|
110
|
+
devicetype: 'Ruby'
|
111
|
+
})
|
112
|
+
|
113
|
+
uri = URI.parse("http://#{bridge.ip}/api")
|
114
|
+
http = Net::HTTP.new(uri.host)
|
115
|
+
response = JSON(http.request_post(uri.path, body).body).first
|
116
|
+
|
117
|
+
if error = response['error']
|
118
|
+
raise get_error(error)
|
119
|
+
end
|
120
|
+
|
121
|
+
if @username = response['success']['username']
|
122
|
+
File.write(File.expand_path('~/.hue'), JSON.dump({username: @username}))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_bridge_id
|
127
|
+
return ENV['HUE_BRIDGE_ID'] if ENV['HUE_BRIDGE_ID']
|
128
|
+
|
129
|
+
json = JSON(File.read(File.expand_path('~/.hue')))
|
130
|
+
json['bridge_id']
|
131
|
+
rescue
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_error(error)
|
136
|
+
# Find error class and return instance
|
137
|
+
klass = Hue::ERROR_MAP[error['type']] || UnknownError unless klass
|
138
|
+
klass.new(error['description'])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hue
|
2
|
+
module EditableState
|
3
|
+
def on?
|
4
|
+
@state['on']
|
5
|
+
end
|
6
|
+
|
7
|
+
def on!
|
8
|
+
self.on = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def off!
|
12
|
+
self.on = false
|
13
|
+
end
|
14
|
+
|
15
|
+
%w{on hue saturation brightness color_temperature alert effect}.each do |key|
|
16
|
+
define_method "#{key}=".to_sym do |value|
|
17
|
+
set_state({key.to_sym => value})
|
18
|
+
instance_variable_set("@#{key}".to_sym, value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_xy(x, y)
|
23
|
+
set_state({:xy => [x, y]})
|
24
|
+
@x, @y = x, y
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hue/errors.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Hue
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class UnauthorizedUser < Error; end
|
5
|
+
class InvalidJSON < Error; end
|
6
|
+
class ResourceNotAvailable < Error; end
|
7
|
+
class MethodNotAvailable < Error; end
|
8
|
+
class MissingBody < Error; end
|
9
|
+
class ParameterNotAvailable < Error; end
|
10
|
+
class InvalidValueForParameter < Error; end
|
11
|
+
class ParameterNotModifiable < Error; end
|
12
|
+
class InternalError < Error; end
|
13
|
+
class LinkButtonNotPressed < Error; end
|
14
|
+
class ParameterNotModifiableWhileOff < ParameterNotModifiable; end
|
15
|
+
class TooManyGroups < Error; end
|
16
|
+
class GroupTooFull < Error; end
|
17
|
+
|
18
|
+
class InvalidUsername < Error; end
|
19
|
+
class UnknownError < Error; end
|
20
|
+
class NoBridgeFound < Error; end
|
21
|
+
|
22
|
+
# Status code to exception map
|
23
|
+
ERROR_MAP = {
|
24
|
+
1 => Hue::UnauthorizedUser,
|
25
|
+
2 => Hue::InvalidJSON,
|
26
|
+
3 => Hue::ResourceNotAvailable,
|
27
|
+
4 => Hue::MethodNotAvailable,
|
28
|
+
5 => Hue::MissingBody,
|
29
|
+
6 => Hue::ParameterNotAvailable,
|
30
|
+
7 => Hue::InvalidValueForParameter,
|
31
|
+
8 => Hue::ParameterNotModifiable,
|
32
|
+
901 => Hue::InternalError,
|
33
|
+
101 => Hue::LinkButtonNotPressed,
|
34
|
+
201 => Hue::ParameterNotModifiableWhileOff,
|
35
|
+
301 => Hue::TooManyGroups,
|
36
|
+
302 => Hue::GroupTooFull
|
37
|
+
}
|
38
|
+
end
|
data/lib/hue/group.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
module Hue
|
2
|
+
class Group
|
3
|
+
include Enumerable
|
4
|
+
include TranslateKeys
|
5
|
+
include EditableState
|
6
|
+
|
7
|
+
# Unique identification number.
|
8
|
+
attr_reader :id
|
9
|
+
|
10
|
+
# Bridge the group is associated with
|
11
|
+
attr_reader :bridge
|
12
|
+
|
13
|
+
# A unique, editable name given to the group.
|
14
|
+
attr_accessor :name
|
15
|
+
|
16
|
+
# Hue of the group. This is a wrapping value between 0 and 65535.
|
17
|
+
# Both 0 and 65535 are red, 25500 is green and 46920 is blue.
|
18
|
+
attr_accessor :hue
|
19
|
+
|
20
|
+
# Saturation of the group. 255 is the most saturated (colored)
|
21
|
+
# and 0 is the least saturated (white).
|
22
|
+
attr_accessor :saturation
|
23
|
+
|
24
|
+
# Brightness of the group. This is a scale from the minimum
|
25
|
+
# brightness the group is capable of, 0, to the maximum capable
|
26
|
+
# brightness, 255. Note a brightness of 0 is not off.
|
27
|
+
attr_accessor :brightness
|
28
|
+
|
29
|
+
# The x coordinate of a color in CIE color space. Between 0 and 1.
|
30
|
+
#
|
31
|
+
# @see http://developers.meethue.com/coreconcepts.html#color_gets_more_complicated
|
32
|
+
attr_reader :x
|
33
|
+
|
34
|
+
# The y coordinate of a color in CIE color space. Between 0 and 1.
|
35
|
+
#
|
36
|
+
# @see http://developers.meethue.com/coreconcepts.html#color_gets_more_complicated
|
37
|
+
attr_reader :y
|
38
|
+
|
39
|
+
# The Mired Color temperature of the light. 2012 connected lights
|
40
|
+
# are capable of 153 (6500K) to 500 (2000K).
|
41
|
+
#
|
42
|
+
# @see http://en.wikipedia.org/wiki/Mired
|
43
|
+
attr_accessor :color_temperature
|
44
|
+
|
45
|
+
# A fixed name describing the type of group.
|
46
|
+
attr_reader :type
|
47
|
+
|
48
|
+
def initialize(client, bridge, id = nil, data = {})
|
49
|
+
@client = client
|
50
|
+
@bridge = bridge
|
51
|
+
@id = id
|
52
|
+
|
53
|
+
unpack(data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def each(&block)
|
57
|
+
lights.each(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def lights
|
61
|
+
@lights ||= begin
|
62
|
+
@light_ids.map do |light_id|
|
63
|
+
@client.light(light_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def name=(name)
|
69
|
+
resp = set_group_state({:name => name})
|
70
|
+
@name = new? ? name : resp[0]['success']["/groups/#{id}/name"]
|
71
|
+
end
|
72
|
+
|
73
|
+
def lights=(light_ids)
|
74
|
+
light_ids.map! do |light_id|
|
75
|
+
light_id.is_a?(Light) ? light_id.id : light_id.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
@light_ids = light_ids.uniq
|
79
|
+
@lights = nil # resets the memoization
|
80
|
+
|
81
|
+
set_group_state({:lights => @light_ids})
|
82
|
+
end
|
83
|
+
|
84
|
+
def scene=(scene)
|
85
|
+
scene_id = scene.is_a?(Scene) ? scene.id : scene
|
86
|
+
set_group_state({:scene => scene_id})
|
87
|
+
end
|
88
|
+
|
89
|
+
def <<(light_id)
|
90
|
+
@light_ids << light_id
|
91
|
+
set_group_state({:lights => @light_ids})
|
92
|
+
end
|
93
|
+
alias_method :add_light, :<<
|
94
|
+
|
95
|
+
def set_group_state(attributes)
|
96
|
+
return if new?
|
97
|
+
body = translate_keys(attributes, GROUP_KEYS_MAP)
|
98
|
+
|
99
|
+
uri = URI.parse(base_url)
|
100
|
+
http = Net::HTTP.new(uri.host)
|
101
|
+
response = http.request_put(uri.path, JSON.dump(body))
|
102
|
+
JSON(response.body)
|
103
|
+
end
|
104
|
+
|
105
|
+
def set_state(attributes)
|
106
|
+
return if new?
|
107
|
+
body = translate_keys(attributes, STATE_KEYS_MAP)
|
108
|
+
|
109
|
+
uri = URI.parse("#{base_url}/action")
|
110
|
+
http = Net::HTTP.new(uri.host)
|
111
|
+
response = http.request_put(uri.path, JSON.dump(body))
|
112
|
+
JSON(response.body)
|
113
|
+
end
|
114
|
+
|
115
|
+
def refresh
|
116
|
+
json = JSON(Net::HTTP.get(URI.parse(base_url)))
|
117
|
+
unpack(json)
|
118
|
+
@lights = nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def create!
|
122
|
+
body = {
|
123
|
+
:name => @name,
|
124
|
+
:lights => @light_ids,
|
125
|
+
}
|
126
|
+
|
127
|
+
uri = URI.parse("http://#{@bridge.ip}/api/#{@client.username}/groups")
|
128
|
+
http = Net::HTTP.new(uri.host)
|
129
|
+
response = http.request_post(uri.path, JSON.dump(body))
|
130
|
+
json = JSON(response.body)
|
131
|
+
|
132
|
+
@id = json[0]['success']['id']
|
133
|
+
end
|
134
|
+
|
135
|
+
def destroy!
|
136
|
+
uri = URI.parse(base_url)
|
137
|
+
http = Net::HTTP.new(uri.host)
|
138
|
+
response = http.delete(uri.path)
|
139
|
+
json = JSON(response.body)
|
140
|
+
@id = nil if json[0]['success']
|
141
|
+
end
|
142
|
+
|
143
|
+
def new?
|
144
|
+
@id.nil?
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
GROUP_KEYS_MAP = {
|
150
|
+
:name => :name,
|
151
|
+
:light_ids => :lights,
|
152
|
+
:type => :type,
|
153
|
+
:state => :action
|
154
|
+
}
|
155
|
+
|
156
|
+
STATE_KEYS_MAP = {
|
157
|
+
:on => :on,
|
158
|
+
:brightness => :bri,
|
159
|
+
:hue => :hue,
|
160
|
+
:saturation => :sat,
|
161
|
+
:xy => :xy,
|
162
|
+
:color_temperature => :ct,
|
163
|
+
:alert => :alert,
|
164
|
+
:effect => :effect,
|
165
|
+
:color_mode => :colormode,
|
166
|
+
}
|
167
|
+
|
168
|
+
def unpack(data)
|
169
|
+
unpack_hash(data, GROUP_KEYS_MAP)
|
170
|
+
|
171
|
+
unless new?
|
172
|
+
unpack_hash(@state, STATE_KEYS_MAP)
|
173
|
+
@x, @y = @state['xy']
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def base_url
|
178
|
+
"http://#{@bridge.ip}/api/#{@client.username}/groups/#{id}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/lib/hue/light.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
module Hue
|
2
|
+
class Light
|
3
|
+
include TranslateKeys
|
4
|
+
include EditableState
|
5
|
+
|
6
|
+
HUE_RANGE = 0..65535
|
7
|
+
SATURATION_RANGE = 0..255
|
8
|
+
BRIGHTNESS_RANGE = 0..255
|
9
|
+
COLOR_TEMPERATURE_RANGE = 153..500
|
10
|
+
|
11
|
+
# Unique identification number.
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
# Bridge the light is associated with
|
15
|
+
attr_reader :bridge
|
16
|
+
|
17
|
+
# A unique, editable name given to the light.
|
18
|
+
attr_accessor :name
|
19
|
+
|
20
|
+
# Hue of the light. This is a wrapping value between 0 and 65535.
|
21
|
+
# Both 0 and 65535 are red, 25500 is green and 46920 is blue.
|
22
|
+
attr_reader :hue
|
23
|
+
|
24
|
+
# Saturation of the light. 255 is the most saturated (colored)
|
25
|
+
# and 0 is the least saturated (white).
|
26
|
+
attr_reader :saturation
|
27
|
+
|
28
|
+
# Brightness of the light. This is a scale from the minimum
|
29
|
+
# brightness the light is capable of, 0, to the maximum capable
|
30
|
+
# brightness, 255. Note a brightness of 0 is not off.
|
31
|
+
attr_reader :brightness
|
32
|
+
|
33
|
+
# The x coordinate of a color in CIE color space. Between 0 and 1.
|
34
|
+
#
|
35
|
+
# @see http://developers.meethue.com/coreconcepts.html#color_gets_more_complicated
|
36
|
+
attr_reader :x
|
37
|
+
|
38
|
+
# The y coordinate of a color in CIE color space. Between 0 and 1.
|
39
|
+
#
|
40
|
+
# @see http://developers.meethue.com/coreconcepts.html#color_gets_more_complicated
|
41
|
+
attr_reader :y
|
42
|
+
|
43
|
+
# The Mired Color temperature of the light. 2012 connected lights
|
44
|
+
# are capable of 153 (6500K) to 500 (2000K).
|
45
|
+
#
|
46
|
+
# @see http://en.wikipedia.org/wiki/Mired
|
47
|
+
attr_reader :color_temperature
|
48
|
+
|
49
|
+
# The alert effect, which is a temporary change to the bulb’s state.
|
50
|
+
# This can take one of the following values:
|
51
|
+
# * `none` – The light is not performing an alert effect.
|
52
|
+
# * `select` – The light is performing one breathe cycle.
|
53
|
+
# * `lselect` – The light is performing breathe cycles for 30 seconds
|
54
|
+
# or until an "alert": "none" command is received.
|
55
|
+
#
|
56
|
+
# Note that in version 1.0 this contains the last alert sent to the
|
57
|
+
# light and not its current state. This will be changed to contain the
|
58
|
+
# current state in an upcoming patch.
|
59
|
+
#
|
60
|
+
# @see http://developers.meethue.com/coreconcepts.html#some_extra_fun_stuff
|
61
|
+
attr_reader :alert
|
62
|
+
|
63
|
+
# The dynamic effect of the light, can either be `none` or
|
64
|
+
# `colorloop`. If set to colorloop, the light will cycle through
|
65
|
+
# all hues using the current brightness and saturation settings.
|
66
|
+
attr_reader :effect
|
67
|
+
|
68
|
+
# Indicates the color mode in which the light is working, this is
|
69
|
+
# the last command type it received. Values are `hs` for Hue and
|
70
|
+
# Saturation, `xy` for XY and `ct` for Color Temperature. This
|
71
|
+
# parameter is only present when the light supports at least one
|
72
|
+
# of the values.
|
73
|
+
attr_reader :color_mode
|
74
|
+
|
75
|
+
# A fixed name describing the type of light.
|
76
|
+
attr_reader :type
|
77
|
+
|
78
|
+
# The hardware model of the light.
|
79
|
+
attr_reader :model
|
80
|
+
|
81
|
+
# An identifier for the software version running on the light.
|
82
|
+
attr_reader :software_version
|
83
|
+
|
84
|
+
# Reserved for future functionality.
|
85
|
+
attr_reader :point_symbol
|
86
|
+
|
87
|
+
def initialize(client, bridge, id, hash)
|
88
|
+
@client = client
|
89
|
+
@bridge = bridge
|
90
|
+
@id = id
|
91
|
+
unpack(hash)
|
92
|
+
end
|
93
|
+
|
94
|
+
def name=(new_name)
|
95
|
+
unless (1..32).include?(new_name.length)
|
96
|
+
raise InvalidValueForParameter, 'name must be between 1 and 32 characters.'
|
97
|
+
end
|
98
|
+
|
99
|
+
body = {
|
100
|
+
:name => new_name
|
101
|
+
}
|
102
|
+
|
103
|
+
uri = URI.parse(base_url)
|
104
|
+
http = Net::HTTP.new(uri.host)
|
105
|
+
response = http.request_put(uri.path, JSON.dump(body))
|
106
|
+
response = JSON(response.body).first
|
107
|
+
if response['success']
|
108
|
+
@name = new_name
|
109
|
+
# else
|
110
|
+
# TODO: Error
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Indicates if a light can be reached by the bridge. Currently
|
115
|
+
# always returns true, functionality will be added in a future
|
116
|
+
# patch.
|
117
|
+
def reachable?
|
118
|
+
@state['reachable']
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param transition The duration of the transition from the light’s current
|
122
|
+
# state to the new state. This is given as a multiple of 100ms and
|
123
|
+
# defaults to 4 (400ms). For example, setting transistiontime:10 will
|
124
|
+
# make the transition last 1 second.
|
125
|
+
def set_state(attributes, transition = nil)
|
126
|
+
body = translate_keys(attributes, STATE_KEYS_MAP)
|
127
|
+
|
128
|
+
# Add transition
|
129
|
+
body.merge!({:transitiontime => transition}) if transition
|
130
|
+
|
131
|
+
uri = URI.parse("#{base_url}/state")
|
132
|
+
http = Net::HTTP.new(uri.host)
|
133
|
+
response = http.request_put(uri.path, JSON.dump(body))
|
134
|
+
JSON(response.body)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Refresh the state of the lamp
|
138
|
+
def refresh
|
139
|
+
json = JSON(Net::HTTP.get(URI.parse(base_url)))
|
140
|
+
unpack(json)
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
KEYS_MAP = {
|
146
|
+
:state => :state,
|
147
|
+
:type => :type,
|
148
|
+
:name => :name,
|
149
|
+
:model => :modelid,
|
150
|
+
:software_version => :swversion,
|
151
|
+
:point_symbol => :pointsymbol
|
152
|
+
}
|
153
|
+
|
154
|
+
STATE_KEYS_MAP = {
|
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,
|
164
|
+
:reachable => :reachable,
|
165
|
+
}
|
166
|
+
|
167
|
+
def unpack(hash)
|
168
|
+
unpack_hash(hash, KEYS_MAP)
|
169
|
+
unpack_hash(@state, STATE_KEYS_MAP)
|
170
|
+
@x, @y = @state['xy']
|
171
|
+
end
|
172
|
+
|
173
|
+
def base_url
|
174
|
+
"http://#{@bridge.ip}/api/#{@client.username}/lights/#{id}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/hue/scene.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Hue
|
2
|
+
class Scene
|
3
|
+
include Enumerable
|
4
|
+
include TranslateKeys
|
5
|
+
|
6
|
+
# Unique identification number.
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
# Bridge the scene is associated with
|
10
|
+
attr_reader :bridge
|
11
|
+
|
12
|
+
# A unique, editable name given to the scene.
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
# Whether or not the scene is active on a group.
|
16
|
+
attr_reader :active
|
17
|
+
|
18
|
+
def initialize(client, bridge, id, data)
|
19
|
+
@client = client
|
20
|
+
@bridge = bridge
|
21
|
+
@id = id
|
22
|
+
|
23
|
+
unpack(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def lights
|
27
|
+
@lights ||= begin
|
28
|
+
@light_ids.map do |light_id|
|
29
|
+
@client.light(light_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
SCENE_KEYS_MAP = {
|
37
|
+
:name => :name,
|
38
|
+
:light_ids => :lights,
|
39
|
+
:active => :active,
|
40
|
+
}
|
41
|
+
|
42
|
+
def unpack(data)
|
43
|
+
unpack_hash(data, SCENE_KEYS_MAP)
|
44
|
+
end
|
45
|
+
|
46
|
+
def base_url
|
47
|
+
"http://#{@bridge.ip}/api/#{@client.username}/scenes/#{id}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Hue
|
2
|
+
module TranslateKeys
|
3
|
+
def translate_keys(hash, map)
|
4
|
+
new_hash = {}
|
5
|
+
hash.each do |key, value|
|
6
|
+
new_key = map[key.to_sym]
|
7
|
+
key = new_key if new_key
|
8
|
+
new_hash[key] = value
|
9
|
+
end
|
10
|
+
new_hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def unpack_hash(hash, map)
|
14
|
+
map.each do |local_key, remote_key|
|
15
|
+
value = hash[remote_key.to_s]
|
16
|
+
next unless value
|
17
|
+
instance_variable_set("@#{local_key}", value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/hue/version.rb
ADDED
data/lib/huebot/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: huebot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: hue
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.2.0
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 0.2.0
|
11
|
+
date: 2019-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
27
13
|
description: Declare and run YAML programs for Philips Hue devices
|
28
14
|
email: jordan.hollinger@gmail.com
|
29
15
|
executables:
|
@@ -33,6 +19,17 @@ extra_rdoc_files: []
|
|
33
19
|
files:
|
34
20
|
- README.md
|
35
21
|
- bin/huebot
|
22
|
+
- lib/hue.rb
|
23
|
+
- lib/hue/LICENSE
|
24
|
+
- lib/hue/bridge.rb
|
25
|
+
- lib/hue/client.rb
|
26
|
+
- lib/hue/editable_state.rb
|
27
|
+
- lib/hue/errors.rb
|
28
|
+
- lib/hue/group.rb
|
29
|
+
- lib/hue/light.rb
|
30
|
+
- lib/hue/scene.rb
|
31
|
+
- lib/hue/translate_keys.rb
|
32
|
+
- lib/hue/version.rb
|
36
33
|
- lib/huebot.rb
|
37
34
|
- lib/huebot/bot.rb
|
38
35
|
- lib/huebot/cli.rb
|