particlerb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # particlerb [![Build Status](https://travis-ci.org/monkbroc/particlerb.svg)](https://travis-ci.org/monkbroc/particlerb)
2
+
3
+ Ruby client for the [Particle.io] Cloud API
4
+
5
+ [Particle.io]: https://www.particle.io
6
+
7
+ *Note: this is not an official gem by Particle. It is maintained by Julien Vanier.*
8
+
9
+ ## Quick start
10
+
11
+ Install via Rubygems
12
+
13
+ gem install particlerb
14
+
15
+ ... or add to your Gemfile
16
+
17
+ gem "particlerb", "~> 0.0.1"
18
+
19
+
20
+ ### Making requests
21
+
22
+ API methods are available as module methods (consuming module-level
23
+ configuration) or as client instance methods.
24
+
25
+ ```ruby
26
+ # Provide authentication credentials
27
+ Particle.configure do |c|
28
+ c.access_token = "38bb7b318cc6898c80317decb34525844bc9db55"
29
+ end
30
+
31
+ # Fetch the list of devices
32
+ Particle.devices
33
+ ```
34
+ or
35
+
36
+ ```ruby
37
+ # Provide authentication credentials
38
+ client = Particle::Client.new(access_token: "38bb7b318cc6898c80317decb34525844bc9db55")
39
+ # Fetch the list of devices
40
+ client.devices
41
+ ```
42
+
43
+ ## Device commands
44
+
45
+ See the [Particle Cloud API documentation][API docs] for more details.
46
+
47
+ List all devices (returns an `Array` of `Device`)
48
+ ```ruby
49
+ devices = Particle.devices
50
+ ```
51
+
52
+ Get a `Device` by id or name
53
+ ```ruby
54
+ device = Particle.device('blue_fire')
55
+ device = Particle.device('f8bbe1e6e69e05c9c405ba1ca504d438061f1b0d')
56
+ ```
57
+
58
+ Get information about a device
59
+ ```ruby
60
+ device = Particle.device('blue_fire')
61
+ device.id
62
+ device.name
63
+ device.connected?
64
+ device.variables
65
+ device.functions
66
+ device.attributes # Hash of all attributes
67
+ device.get_attributes # forces refresh of all attributes from the cloud
68
+ ```
69
+
70
+ Claim a device and add it to your account (returns the `Device`)
71
+ ```ruby
72
+ Particle.device('blue_fire').claim
73
+ ```
74
+
75
+ Remove a device from your account
76
+ ```ruby
77
+ Particle.device('blue_fire').remove
78
+ Particle.devices.first.remove
79
+ ```
80
+
81
+ Rename a device
82
+ ```ruby
83
+ Particle.device('red').rename('green')
84
+ ```
85
+
86
+ Call a function on the firmware (returns the result of running the function)
87
+ ```ruby
88
+ Particle.device('coffeemaker').function('brew') # String argument optional
89
+ Particle.devices.first.function('digitalWrite', '1')
90
+ ```
91
+
92
+ Get the value of a firmware variable (returns the result as a String or Number)
93
+ ```ruby
94
+ Particle.device('mycar').variable('battery') # ==> 12.33
95
+ device = Particle.device('f8bbe1e6e69e05c9c405ba1ca504d438061f1b0d')
96
+ device.variable('version') # ==> "1.0.1"
97
+ ```
98
+
99
+
100
+ Signal a device to start blinking the RGB LED in rainbow patterns.
101
+ ```ruby
102
+ Particle.device('nyan_cat').signal(true)
103
+ ```
104
+
105
+
106
+ [API docs]: http://docs.particle.io/core/api
107
+
108
+ ### Accessing HTTP responses
109
+
110
+ While most methods return a domain object like `Device`, sometimes you may
111
+ need access to the raw HTTP response headers. You can access the last HTTP
112
+ response with `Client#last_response`:
113
+
114
+ ```ruby
115
+ device = Particle.device('123456').claim
116
+ response = Particle.last_response
117
+ headers = response.headers
118
+ ```
119
+
120
+ ## Thanks
121
+
122
+ This gem is heavily inspired by [Octokit][] by GitHub. I stand on the shoulder of giants. Thanks!
123
+
124
+ Octokit is copyright (c) 2009-2014 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober and licensed under the [MIT license][Octokit license].
125
+
126
+ [Octokit]: http://github.com/octokit/octokit.rb
127
+ [Octokit license]: https://github.com/octokit/octokit.rb/blob/master/LICENSE.md
128
+
129
+
130
+ ## License
131
+
132
+ Copyright (c) 2015 Julien Vanier
133
+
134
+ This gem is available under the [GNU General Public License version 3][GPL-v3]
135
+
136
+ [GPL-v3]: https://github.com/monkbroc/particlerb/blob/master/LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :test => :spec
8
+ task :default => :spec
@@ -0,0 +1,109 @@
1
+ module Particle
2
+ class Client
3
+
4
+ # Methods for the Particle device API
5
+ # @see http://docs.particle.io/core/api/#introduction-list-devices
6
+ module Devices
7
+
8
+ # Create a domain model for a Particle device
9
+ #
10
+ # @param target [String, Sawyer::Resource, Device] A device id, name or {Device} object
11
+ # @return [Device] A device object to interact with
12
+ def device(target)
13
+ if target.is_a? Device
14
+ target
15
+ elsif target.respond_to?(:to_attrs)
16
+ Device.new(self, target.to_attrs)
17
+ else
18
+ Device.new(self, target.to_s)
19
+ end
20
+ end
21
+
22
+ # List all Particle devices on the account
23
+ #
24
+ # @see http://docs.particle.io/core/api/#introduction-list-devices
25
+ #
26
+ # @return [Array<Device>] List of Particle devices to interact with
27
+ def devices
28
+ get(Device.list_path).map do |resource|
29
+ Device.new(self, resource)
30
+ end
31
+ end
32
+
33
+ # Get information about a Particle device
34
+ #
35
+ # @param target [String, Device] A device id, name or {Device} object
36
+ # @return [Hash] The device attributes
37
+ def device_attributes(target)
38
+ result = get(device(target).path)
39
+ result.to_attrs
40
+ end
41
+
42
+ # Add a Particle device to your account
43
+ #
44
+ # @param target [String, Device] A device id or {Device} object.
45
+ # You can't claim a device by name
46
+ # @return [Device] A device object to interact with
47
+ def claim_device(target)
48
+ result = post(Device.claim_path, id: device(target).id_or_name)
49
+ device(result.id)
50
+ end
51
+
52
+ # Remove a Particle device from your account
53
+ #
54
+ # @param target [String, Device] A device id, name or {Device} object
55
+ # @return [boolean] true for success
56
+ def remove_device(target)
57
+ result = delete(device(target).path)
58
+ result.ok
59
+ end
60
+
61
+ # Rename a Particle device in your account
62
+ #
63
+ # @param target [String, Device] A device id, name or {Device} object
64
+ # @param name [String] New name for the device
65
+ # @return [boolean] true for success
66
+ def rename_device(target, name)
67
+ result = put(device(target).path, name: name)
68
+ result.name == name
69
+ end
70
+
71
+ # Call a function in the firmware of a Particle device
72
+ #
73
+ # @param target [String, Device] A device id, name or {Device} object
74
+ # @param name [String] Function to run on firmware
75
+ # @param argument [String] Argument string to pass to the firmware function
76
+ # @return [Integer] Return value from the firmware function
77
+ def call_function(target, name, argument = "")
78
+ result = post(device(target).function_path(name), arg: argument)
79
+ result.return_value
80
+ end
81
+
82
+ # Get the value of a variable in the firmware of a Particle device
83
+ #
84
+ # @param target [String, Device] A device id, name or {Device} object
85
+ # @param name [String] Variable on firmware
86
+ # @return [String, Number] Value from the firmware variable
87
+ def get_variable(target, name)
88
+ result = get(device(target).variable_path(name))
89
+ result.result
90
+ end
91
+
92
+ # Signal the device to start blinking the RGB LED in a rainbow
93
+ # pattern. Useful to identify a particular device.
94
+ #
95
+ # @param target [String, Device] A device id, name or {Device} object
96
+ # @param enabled [String] Whether to enable or disable the rainbow signal
97
+ # @return [boolean] true when signaling, false when stopped
98
+ def signal_device(target, enabled = true)
99
+ result = put(device(target).path, signal: enabled ? '1' : '0')
100
+ # FIXME: API bug. Should return HTTP 408 so result.ok wouldn't be necessary
101
+ if result.ok == false
102
+ false
103
+ else
104
+ result.signaling
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,44 @@
1
+ require 'particle/connection'
2
+ require 'particle/device'
3
+ require 'particle/client/devices'
4
+
5
+ module Particle
6
+
7
+ # Client for the Particle API
8
+ #
9
+ # @see http://docs.particle.io/
10
+ class Client
11
+ include Particle::Configurable
12
+ include Particle::Connection
13
+ include Particle::Client::Devices
14
+
15
+ def initialize(options = {})
16
+ # Use options passed in, but fall back to module defaults
17
+ Particle::Configurable.keys.each do |key|
18
+ instance_variable_set(:"@#{key}", options[key] || Particle.instance_variable_get(:"@#{key}"))
19
+ end
20
+ end
21
+
22
+ # Text representation of the client, masking tokens
23
+ #
24
+ # @return [String]
25
+ def inspect
26
+ inspected = super
27
+
28
+ # Only show last 4 of token, secret
29
+ if @access_token
30
+ inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}"
31
+ end
32
+
33
+ inspected
34
+ end
35
+
36
+ # Set OAuth2 access token for authentication
37
+ #
38
+ # @param value [String] 40 character Particle OAuth2 access token
39
+ def access_token=(value)
40
+ reset_agent
41
+ @access_token = value
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ module Particle
2
+
3
+ # Configuration options for {Client}, defaulting to values in
4
+ # {Default}
5
+ module Configurable
6
+ # @!attribute [w] access_token
7
+ # @see http://docs.particle.io/core/api/#introduction-authentication
8
+ # @return [String] Particle access token for authentication
9
+ # @!attribute api_endpoint
10
+ # @return [String] Base URL for API requests. default: https://api.particle.io
11
+ # @!attribute connection_options
12
+ # @see https://github.com/lostisland/faraday
13
+ # @return [Hash] Configure connection options for Faraday
14
+ # @!attribute user_agent
15
+ # @return [String] Configure User-Agent header for requests.
16
+
17
+ attr_accessor :access_token, :connection_options,
18
+ :user_agent
19
+ attr_writer :api_endpoint
20
+
21
+ class << self
22
+ def keys
23
+ @keys ||= [
24
+ :access_token,
25
+ :api_endpoint,
26
+ :connection_options,
27
+ :user_agent
28
+ ]
29
+ end
30
+ end
31
+
32
+ # Yields an object to set up configuration options in an initializer
33
+ # file
34
+ def configure
35
+ yield self
36
+ end
37
+
38
+ # Reset configuration options to default values
39
+ def reset!
40
+ Particle::Configurable.keys.each do |key|
41
+ instance_variable_set :"@#{key}", Particle::Default.options[key]
42
+ end
43
+ end
44
+
45
+ # Compares client options to a Hash of requested options
46
+ #
47
+ # @param opts [Hash] Options to compare with current client options
48
+ # @return [Boolean]
49
+ def same_options?(opts)
50
+ opts.hash == options.hash
51
+ end
52
+
53
+ # Clever way to add / at the end of the api_endpoint
54
+ def api_endpoint
55
+ File.join(@api_endpoint, "")
56
+ end
57
+
58
+ private
59
+
60
+ def options
61
+ Hash[Particle::Configurable.keys.map{ |key| [key, instance_variable_get(:"@#{key}")] }]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,125 @@
1
+ require 'sawyer'
2
+ require 'particle/response/raise_error'
3
+
4
+ module Particle
5
+
6
+ # Network layer for API client
7
+ module Connection
8
+
9
+ # Faraday middleware stack
10
+ MIDDLEWARE = Faraday::RackBuilder.new do |builder|
11
+ builder.use Particle::Response::RaiseError
12
+ builder.adapter Faraday.default_adapter
13
+ end
14
+
15
+ # Make a HTTP GET request
16
+ #
17
+ # @param url [String] The path, relative to {#api_endpoint}
18
+ # @param options [Hash] Query and header params for request
19
+ # @return [Sawyer::Resource]
20
+ def get(url, options = {})
21
+ request :get, url, options
22
+ end
23
+
24
+ # Make a HTTP POST request
25
+ #
26
+ # @param url [String] The path, relative to {#api_endpoint}
27
+ # @param options [Hash] Body and header params for request
28
+ # @return [Sawyer::Resource]
29
+ def post(url, options = {})
30
+ request :post, url, options
31
+ end
32
+
33
+ # Make a HTTP PUT request
34
+ #
35
+ # @param url [String] The path, relative to {#api_endpoint}
36
+ # @param options [Hash] Body and header params for request
37
+ # @return [Sawyer::Resource]
38
+ def put(url, options = {})
39
+ request :put, url, options
40
+ end
41
+
42
+ # Make a HTTP PATCH request
43
+ #
44
+ # @param url [String] The path, relative to {#api_endpoint}
45
+ # @param options [Hash] Body and header params for request
46
+ # @return [Sawyer::Resource]
47
+ def patch(url, options = {})
48
+ request :patch, url, options
49
+ end
50
+
51
+ # Make a HTTP DELETE request
52
+ #
53
+ # @param url [String] The path, relative to {#api_endpoint}
54
+ # @param options [Hash] Query and header params for request
55
+ # @return [Sawyer::Resource]
56
+ def delete(url, options = {})
57
+ request :delete, url, options
58
+ end
59
+
60
+ # Make a HTTP HEAD request
61
+ #
62
+ # @param url [String] The path, relative to {#api_endpoint}
63
+ # @param options [Hash] Query and header params for request
64
+ # @return [Sawyer::Resource]
65
+ def head(url, options = {})
66
+ request :head, url, parse_query_and_convenience_headers(options)
67
+ end
68
+
69
+ # Hypermedia agent for the Particle API
70
+ #
71
+ # @return [Sawyer::Agent]
72
+ def agent
73
+ @agent ||= Sawyer::Agent.new(endpoint, sawyer_options) do |http|
74
+ http.headers[:content_type] = "application/json"
75
+ http.headers[:user_agent] = user_agent
76
+ if @access_token
77
+ http.authorization :Bearer, @access_token
78
+ end
79
+ end
80
+ end
81
+
82
+ # Response for last HTTP request
83
+ #
84
+ # @return [Sawyer::Response]
85
+ attr_reader :last_response
86
+
87
+ protected
88
+
89
+ def endpoint
90
+ api_endpoint
91
+ end
92
+
93
+ private
94
+
95
+ def reset_agent
96
+ @agent = nil
97
+ end
98
+
99
+ def request(method, path, data, options = {})
100
+ if data.is_a?(Hash)
101
+ options[:query] = data.delete(:query) || {}
102
+ options[:headers] = data.delete(:headers) || {}
103
+ if accept = data.delete(:accept)
104
+ options[:headers][:accept] = accept
105
+ end
106
+ end
107
+
108
+ @last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
109
+ response.data
110
+ end
111
+
112
+ def sawyer_options
113
+ opts = {
114
+ :links_parser => Sawyer::LinkParsers::Simple.new
115
+ }
116
+ conn_opts = @connection_options.dup
117
+ conn_opts[:builder] = MIDDLEWARE
118
+ conn_opts[:proxy] = @proxy if @proxy
119
+ opts[:faraday] = Faraday.new(conn_opts)
120
+
121
+ opts
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,44 @@
1
+ require 'particle/version'
2
+
3
+ module Particle
4
+
5
+ # Default configuration options for {Client}
6
+ module Default
7
+ API_ENDPOINT = "https://api.particle.io".freeze
8
+
9
+ USER_AGENT = "particlerb Ruby gem #{Particle::VERSION}".freeze
10
+
11
+ class << self
12
+
13
+ # Configuration options
14
+ # @return [Hash]
15
+ def options
16
+ Hash[Particle::Configurable.keys.map { |key| [key, send(key)] }]
17
+ end
18
+
19
+ def api_endpoint
20
+ ENV['PARTICLE_API_ENDPOINT'] || API_ENDPOINT
21
+ end
22
+
23
+ def access_token
24
+ ENV['PARTICLE_ACCESS_TOKEN']
25
+ end
26
+
27
+ # Default options for Faraday::Connection
28
+ # @return [Hash]
29
+ def connection_options
30
+ {
31
+ :headers => {
32
+ :user_agent => user_agent
33
+ }
34
+ }
35
+ end
36
+
37
+ # Default User-Agent header string from {USER_AGENT}
38
+ # @return [String]
39
+ def user_agent
40
+ USER_AGENT
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,129 @@
1
+ module Particle
2
+
3
+ # Domain model for one Particle device
4
+ class Device
5
+ ID_REGEX = /\h{24}/
6
+
7
+ def initialize(client, attributes)
8
+ @client = client
9
+ @attributes =
10
+ if attributes.is_a? String
11
+ if attributes =~ ID_REGEX
12
+ { id: attributes }
13
+ else
14
+ { name: attributes }
15
+ end
16
+ else
17
+ attributes
18
+ end
19
+ end
20
+
21
+ def id
22
+ get_attributes unless @attributes[:id]
23
+ @attributes[:id]
24
+ end
25
+
26
+ def name
27
+ get_attributes unless @attributes[:name]
28
+ @attributes[:name]
29
+ end
30
+
31
+ def id_or_name
32
+ @attributes[:id] || @attributes[:name]
33
+ end
34
+
35
+ %w(connected functions variables product_id last_heard).each do |key|
36
+ define_method key do
37
+ attributes[key.to_sym]
38
+ end
39
+ end
40
+ alias_method :connected?, :connected
41
+
42
+ def attributes
43
+ get_attributes unless @loaded
44
+ @attributes
45
+ end
46
+
47
+ def get_attributes
48
+ @loaded = true
49
+ @attributes = @client.device_attributes(self)
50
+ end
51
+
52
+ # Add a Particle device to your account
53
+ #
54
+ # @example Add a Photon by its id
55
+ # Particle.device('f8bbe1e6e69e05c9c405ba1ca504d438061f1b0d').claim
56
+ def claim
57
+ new_device = @client.claim_device(self)
58
+ self
59
+ end
60
+
61
+ # Remove a Particle device from your account
62
+ #
63
+ # @example Add a Photon by its id
64
+ # Particle.device('f8bbe1e6e69e05c9c405ba1ca504d438061f1b0d').claim
65
+ def remove
66
+ @client.remove_device(self)
67
+ end
68
+
69
+ # Rename a Particle device on your account
70
+ #
71
+ # @param name [String] New name for the device
72
+ # @example Change the name of a Photon
73
+ # Particle.device('blue').rename('red')
74
+ def rename(name)
75
+ @client.rename_device(self, name)
76
+ end
77
+
78
+ # Call a function in the firmware of a Particle device
79
+ #
80
+ # @param name [String] Function to run on firmware
81
+ # @param argument [String] Argument string to pass to the firmware function
82
+ # @example Call the thinker digitalWrite function
83
+ # Particle.device('white_whale').function('digitalWrite', '0')
84
+ def function(name, argument = "")
85
+ @client.call_function(self, name, argument)
86
+ end
87
+
88
+ # Get the value of a variable in the firmware of a Particle device
89
+ #
90
+ # @param target [String, Device] A device id, name or {Device} object
91
+ # @param name [String] Variable on firmware
92
+ # @return [String, Number] Value from the firmware variable
93
+ # @example Get the battery voltage
94
+ # Particle.device('mycar').variable('battery') == 12.5
95
+ def variable(name)
96
+ @client.get_variable(self, name)
97
+ end
98
+
99
+ # Signal the device to start blinking the RGB LED in a rainbow
100
+ # pattern. Useful to identify a particular device.
101
+ #
102
+ # @param enabled [String] Whether to enable or disable the rainbow signal
103
+ # @return [boolean] true when signaling, false when stopped
104
+ def signal(enabled = true)
105
+ @client.signal_device(self, enabled)
106
+ end
107
+
108
+ def self.list_path
109
+ "v1/devices"
110
+ end
111
+
112
+ def self.claim_path
113
+ "v1/devices"
114
+ end
115
+
116
+ def path
117
+ "/v1/devices/#{id_or_name}"
118
+ end
119
+
120
+ def function_path(name)
121
+ path + "/#{name}"
122
+ end
123
+
124
+ def variable_path(name)
125
+ path + "/#{name}"
126
+ end
127
+ end
128
+ end
129
+