lifx_api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 055c814b51eff3591097009aed7385b3022fd6e2
4
+ data.tar.gz: d66f4c78f22718034ae0de373b357e344730ac80
5
+ SHA512:
6
+ metadata.gz: 3be95ba0e47d8d6e1eb53d2bdd7c557e1f755eab02b9ceea431dc7df6d8bde85a40be32e9afcbebcfde145d6b32a4156005f87113087028ca56b2a9e6cdf5a84
7
+ data.tar.gz: bbf46d92d4e5362098886b8677778ef24d2bba265f59f11c41ef5b9d2e0a3776ff736f20e05eb53b126944e5d8aad18d60367ff7dfb785dcfa0f34c484a0d7d1
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # LifxApi
2
+
3
+ A Ruby client for the LIFX API.
4
+
5
+ This provides access to the [LIFX HTTP API](https://api.developer.lifx.com/), so it can control your lights from anywhere in the world. It does not implement the [LAN API](https://lan.developer.lifx.com/).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'lifx_api'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install lifx_api
22
+
23
+ ## Usage
24
+
25
+ You'll need to have some LIFX bulbs already setup and configured. Then you will need to obtain an `access_token` from the [LIFX website](https://cloud.lifx.com/sign_in).
26
+
27
+ ```ruby
28
+ require 'lifx_api'
29
+
30
+ access_token = "a1b7349df2a...e213"
31
+ client = LifxApi.new access_token
32
+ lights = client.list_lights
33
+ exit() if lights.count == 0
34
+ light_id = lights.first[:id]
35
+
36
+ client.toggle_power selector: 'all'
37
+
38
+ client.toggle_power selector: "id:#{light_id}"
39
+ ```
40
+
41
+ ## Endpoints and parameters
42
+
43
+ ### TODO. See [LIFX HTTP API](https://api.developer.lifx.com/) in the mean time
44
+
45
+ ## Deviation from the API spec
46
+
47
+ Some API endpoints require a mandatory [`selector`](https://api.developer.lifx.com/docs/selectors) parameter, which defines which bulbs to apply your action to. This client will default the `selector` parameter to `'all`', if no selector is provided.
48
+
49
+ ```ruby
50
+ # which means that you can call
51
+ client.list_bulbs
52
+ # ...and receive a hash of all your bulbs back
53
+
54
+ # instead of having to explicitly specify you want all bulbs:
55
+ client.list_bulbs selector: 'all'
56
+ ```
57
+
58
+ ## Exceptions
59
+
60
+ If there is an error, LifxApi will raise an exception. The exception message will usually give a good indication of what went wrong, but you can also rescue the exception and access the request, response and decoded JSON objects, via the `request`, `response` and `data` methods.
61
+
62
+ ## Development
63
+
64
+ Run `rake test` to run the tests and `rake console` to start an interactive pry console.
65
+
66
+ ## Contributing
67
+
68
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cyclotron3k/lifx_api.
69
+
70
+ ## License
71
+
72
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ desc "Launch an interactive Pry console"
13
+ task :console do
14
+ exec "pry -r lifx_api -I ./lib"
15
+ end
@@ -0,0 +1,121 @@
1
+ class LifxApi
2
+ ENDPOINTS = [{
3
+ method_name: :list_lights,
4
+ http_method: :get,
5
+ path: '/v1/lights/%{selector}',
6
+ path_params: {
7
+ selector: {required: true, type: :selector, default: 'all'},
8
+ },
9
+ }, {
10
+ method_name: :set_state,
11
+ http_method: :put,
12
+ path: '/v1/lights/%{selector}/state',
13
+ path_params: {
14
+ selector: {required: true, type: :selector, default: 'all'},
15
+ },
16
+ body_params: {
17
+ power: {type: :string, description: 'The power state you want to set on the selector. on or off'},
18
+ color: {type: :string, description: 'The color to set the light to.'},
19
+ brightness: {type: :numeric, description: 'The brightness level from 0.0 to 1.0. Overrides any brightness set in color (if any).'},
20
+ duration: {type: :numeric, default_decription: '1.0', description: 'How long in seconds you want the power action to take. Range: 0.0 - 3155760000.0 (100 years)'},
21
+ infrared: {type: :numeric, description: 'The maximum brightness of the infrared channel.'},
22
+ },
23
+ }, {
24
+ method_name: :set_states,
25
+ http_method: :put,
26
+ path: '/v1/lights/states',
27
+ body_params: {
28
+ states: {required: true},
29
+ defaults: {type: :hash},
30
+ },
31
+ }, {
32
+ method_name: :stage_delta,
33
+ http_method: :post,
34
+ path: '/v1/lights/%{selector}/state/delta',
35
+ path_params: {
36
+ selector: {required: true, type: :selector, default: 'all'}
37
+ },
38
+ body_params: {
39
+ power: {type: :on_off, description: 'The power state you want to set on the selector. on or off'},
40
+ duration: {type: :numeric, default_decription: '1.0', description: 'How long in seconds you want the power action to take. Range: 0.0 - 3155760000.0 (100 years)'},
41
+ infrared: {type: :numeric, description: 'The maximum brightness of the infrared channel.'},
42
+ hue: {type: :numeric, description: 'Rotate the hue by this angle in degrees.'},
43
+ saturation: {type: :numeric, description: 'Change the saturation by this additive amount; the resulting saturation is clipped to [0, 1].'},
44
+ brightness: {type: :numeric, description: 'Change the brightness by this additive amount; the resulting brightness is clipped to [0, 1].'},
45
+ kelvin: {type: :numeric, description: 'Change the kelvin by this additive amount; the resulting kelvin is clipped to [2500, 9000].'},
46
+ },
47
+ }, {
48
+ method_name: :toggle_power,
49
+ http_method: :post,
50
+ path: '/v1/lights/%{selector}/toggle',
51
+ path_params: {
52
+ selector: {required: true, type: :selector, default: 'all'},
53
+ },
54
+ body_params: {
55
+ duration: {type: :numeric, default_decription: '1.0', description: 'The time is seconds to spend perfoming the power toggle.'},
56
+ },
57
+ }, {
58
+ method_name: :breathe_effect,
59
+ http_method: :post,
60
+ path: '/v1/lights/%{selector}/effects/breathe',
61
+ path_params: {
62
+ selector: {required: true, type: :selector, default: 'all'},
63
+ },
64
+ body_params: {
65
+ color: {required: :true, type: :string, description: 'The color to use for the breathe effect.'},
66
+ from_color: {type: :string, default_decription: 'current bulb color', description: 'The color to start the effect from. If this parameter is omitted then the color the bulb is currently set to is used instead.'},
67
+ period: {type: :numeric, default_decription: '1.0', description: 'The time in seconds for one cyles of the effect.'},
68
+ cycles: {type: :numeric, default_decription: '1.0', description: 'The number of times to repeat the effect.'},
69
+ persist: {type: :boolean, default_decription: 'false', description: 'If false set the light back to its previous value when effect ends, if true leave the last effect color.'},
70
+ power_on: {type: :boolean, default_decription: 'true', description: 'If true, turn the bulb on if it is not already on.'},
71
+ peak: {type: :numeric, default_decription: '0.5', description: 'Defines where in a period the target color is at its maximum. Minimum 0.0, maximum 1.0.'},
72
+ }
73
+ }, {
74
+ method_name: :pulse_effect,
75
+ http_method: :post,
76
+ path: '/v1/lights/%{selector}/effects/pulse',
77
+ path_params: {
78
+ selector: {required: true, type: :selector, default: 'all'},
79
+ },
80
+ body_params: {
81
+ color: {required: true, type: :string, description: 'The color to use for the pulse effect.'},
82
+ from_color: {type: :string, default_decription: 'current bulb color', description: 'The color to start the effect from. If this parameter is omitted then the color the bulb is currently set to is used instead.'},
83
+ period: {type: :numeric, default_decription: '1.0', description: 'The time in seconds for one cyles of the effect.'},
84
+ cycles: {type: :numeric, default_decription: '1.0', description: 'The number of times to repeat the effect.'},
85
+ persist: {type: :boolean, default_decription: 'false', description: 'If false set the light back to its previous value when effect ends, if true leave the last effect color.'},
86
+ power_on: {type: :boolean, default_decription: 'true', description: 'If true, turn the bulb on if it is not already on.'},
87
+ }
88
+ }, {
89
+ method_name: :cycle,
90
+ http_method: :post,
91
+ path: '/v1/lights/%{selector}/cycle',
92
+ body_params: {
93
+ states: {required: true, type: 'array of mixed', description: 'Array of state hashes as per Set State. Must have 2 to 5 entries.'},
94
+ defaults: {type: 'object', description: 'Default values to use when not specified in each states[] object.'},
95
+ direction: {type: 'stringforward', description: 'Direction in which to cycle through the list. Can be forward or backward'},
96
+ }
97
+ }, {
98
+ method_name: :list_scenes,
99
+ http_method: :get,
100
+ path: '/v1/scenes',
101
+ }, {
102
+ method_name: :activate_scene,
103
+ http_method: :put,
104
+ path: '/v1/scenes/scene_id:%{scene_uuid}/activate',
105
+ path_params: {
106
+ scene_uuid: {required: true, type: :uuid, description: 'The UUID for the scene you wish to activate'},
107
+ },
108
+ body_params: {
109
+ duration: {type: :numeric, default_decription: '1.0', description: 'The time in seconds to spend performing the scene transition.'},
110
+ ignore: {type: 'array of strings', description: 'Any of "power", "infrared", "duration", "intensity", "hue", "saturation", "brightness" or "kelvin", specifying that these properties should not be changed on devices when applying the scene.'},
111
+ overrides: {type: 'object', description: 'A state object as per Set State specifying properties to apply to all devices in the scene, overriding those configured in the scene.'},
112
+ }
113
+ }, {
114
+ method_name: :validate_color,
115
+ http_method: :put,
116
+ path: '/v1/color',
117
+ query_params: {
118
+ color: {type: :string, required: true},
119
+ },
120
+ }]
121
+ end
@@ -0,0 +1,17 @@
1
+ class LifxApi
2
+ class Error < StandardError
3
+ attr_reader :request, :response, :data
4
+
5
+ def initialize(request, response, data=nil)
6
+ @request = request
7
+ @response = response
8
+ @data = data
9
+ message = if data.is_a?(Hash) and data.key? :error
10
+ "#{response.code} - #{data[:error]}"
11
+ else
12
+ "#{response.code}"
13
+ end
14
+ super message
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ class LifxApi
2
+ VERSION = '0.1.0'
3
+ end
data/lib/lifx_api.rb ADDED
@@ -0,0 +1,125 @@
1
+ require 'net/http'
2
+ require 'openssl'
3
+ require 'cgi'
4
+ require 'json'
5
+ require 'lifx_api/version'
6
+ require 'lifx_api/endpoints'
7
+ require 'lifx_api/error'
8
+
9
+ class LifxApi
10
+
11
+ PROTOCOL = 'https'
12
+ HOST = 'api.lifx.com'
13
+ DEBUG = false
14
+
15
+ def initialize(access_token)
16
+ @access_token = access_token
17
+ @base_uri = URI "#{PROTOCOL}://#{HOST}"
18
+
19
+ @agent = Net::HTTP.new @base_uri.host, @base_uri.port
20
+ @agent.use_ssl = PROTOCOL == 'https',
21
+ @agent.keep_alive_timeout = 10
22
+ @agent.set_debug_output $stdout if DEBUG
23
+ end
24
+
25
+ ENDPOINTS.each do |endpoint_spec|
26
+ define_method endpoint_spec[:method_name], Proc.new { |params={}|
27
+ parsed_params = parse_params endpoint_spec, params
28
+ request = create_request endpoint_spec, parsed_params
29
+ process_request request
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def parse_params(endpoint_spec, params)
36
+ [:path_params, :body_params, :query_params].each_with_object({}) do |field, parsed|
37
+ next unless endpoint_spec[field]
38
+
39
+ parsed[field] = endpoint_spec[field].each_with_object({}) do |(key, field_spec), clean|
40
+ value = params[key] || field_spec[:default]
41
+ raise "Missing #{key}" if field_spec[:required] and value.nil?
42
+ next if value.nil?
43
+ raise ArgumentError, "#{key} (#{value}) failed validation" unless valid? value, field_spec[:type]
44
+ clean[key] = value
45
+ end
46
+ end
47
+ end
48
+
49
+ def valid?(value, value_format)
50
+ case value_format
51
+ when :selector
52
+ value =~ /^((label|id|(location|group)(_id)?|scene_id):.*|all)$/
53
+ when :numeric
54
+ value.is_a?(Numeric) or value =~ /^[\d\.]+$/
55
+ when :boolean
56
+ ['true', 'false', true, false].include? value
57
+ when :on_off
58
+ ['on', 'off'].include? value
59
+ when :hash
60
+ value.is_a? Hash
61
+ when :string
62
+ value.is_a? String
63
+ when :uuid
64
+ value.is_a?(String) and value =~ /^[\da-f]{4}([\da-f]{4}-){4}[\da-f]{12}$/
65
+ else
66
+ puts "Don't know how to validate #{value_format}" if DEBUG
67
+ true
68
+ end
69
+ end
70
+
71
+ def create_request(endpoint_spec, params)
72
+ uri = @base_uri.clone
73
+
74
+ uri.path = if params.key? :path_params
75
+ endpoint_spec[:path] % Hash[params[:path_params].map { |k, v| [k, CGI.escape(v).gsub('+', '%20')]}]
76
+ else
77
+ endpoint_spec[:path]
78
+ end
79
+
80
+ if params.key? :query_params
81
+ uri.query = URI.encode_www_form params[:query_params]
82
+ end
83
+
84
+ request = case endpoint_spec[:http_method]
85
+ when :get
86
+ Net::HTTP::Get.new uri
87
+ when :put
88
+ Net::HTTP::Put.new uri
89
+ when :post
90
+ Net::HTTP::Post.new uri
91
+ else
92
+ raise NotImplementedError, "Invalid HTTP method: #{endpoint_spec[:http_method]}"
93
+ end
94
+
95
+ if params.key? :body_params
96
+ request.set_form_data params[:body_params]
97
+ end
98
+
99
+ request
100
+ end
101
+
102
+ def process_request(request)
103
+ request['Authorization'] = "Bearer #{@access_token}"
104
+
105
+ puts "\n\n\e[1;32mDespatching request to #{request.path}\e[0m" if DEBUG
106
+
107
+ @session = @agent.start unless @agent.active?
108
+ response = @session.request request
109
+
110
+ puts "\e[1;32mResponse received\e[0m" if DEBUG
111
+
112
+ data = case response['content-type']
113
+ when /application\/json/
114
+ JSON.parse response.read_body, symbolize_names: true
115
+ else
116
+ raise "Don't know how to parse #{response['content-type']}"
117
+ end
118
+
119
+ unless (200..299).include? response.code.to_i
120
+ raise LifxApi::Error.new request, response, data
121
+ end
122
+
123
+ data
124
+ end
125
+ end
data/lifx_api.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "lifx_api/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "lifx_api"
8
+ spec.version = LifxApi::VERSION
9
+ spec.authors = "cyclotron3k"
10
+
11
+ spec.summary = "A client for the LIFX HTTP API"
12
+ spec.homepage = "https://github.com/cyclotron3k/lifx_api"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.15"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.0"
24
+ spec.add_development_dependency "pry", "~> 0.10"
25
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lifx_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - cyclotron3k
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ description:
70
+ email:
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - ".gitignore"
76
+ - Gemfile
77
+ - LICENSE.txt
78
+ - README.md
79
+ - Rakefile
80
+ - lib/lifx_api.rb
81
+ - lib/lifx_api/endpoints.rb
82
+ - lib/lifx_api/error.rb
83
+ - lib/lifx_api/version.rb
84
+ - lifx_api.gemspec
85
+ homepage: https://github.com/cyclotron3k/lifx_api
86
+ licenses:
87
+ - MIT
88
+ metadata: {}
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.5.1
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: A client for the LIFX HTTP API
109
+ test_files: []