particlerb 0.0.1

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.
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
+