lifx_api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []