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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 565062504fb9277b5047788566d50d5ea36223567f9ad5d8466a7ffa7886b661
4
- data.tar.gz: 31d364292a69ffa6193ead6699d4b0642249c686a4917e040acf6e42408e9d02
3
+ metadata.gz: ec0452c2e2f31fbbf2fac6b5bfe817f19f9f312ed85eca34c5dcbf47d4280b58
4
+ data.tar.gz: a5412c05d81ba53ca81a2461d19d408e025c78298237459746c6ede4f376614b
5
5
  SHA512:
6
- metadata.gz: cbaaa7f8beb87c9443a697957766338d961ae08602e8472298d80234945b152e101099fa8ad5edae3226b86fe7c6a7528273034a29680a02f9d40c5ab99f4b34
7
- data.tar.gz: e31b3ed57ccd4f47c64841c2707d067776d89f5f7c7b45c26393de0928f321f0a18d3c5fc7a89c7e432abea401f8262891d0b6b770dcfcc2b21ec9e0d99aa14f
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # TODO remove
4
- $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ # Used for local testing
4
+ # $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
5
 
6
6
  require 'huebot'
7
7
  require 'huebot/cli'
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Hue
2
+ VERSION = '0.1.5'
3
+ end
@@ -1,4 +1,4 @@
1
1
  module Huebot
2
2
  # Gem version
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
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.1.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: 2018-12-24 00:00:00.000000000 Z
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