├── Rakefile ├── lib ├── ruby_spark │ ├── version.rb │ ├── tinker.rb │ └── device.rb ├── ruby_spark.rb └── helpers │ └── configuration.rb ├── Gemfile ├── .gitignore ├── examples ├── device_example.rb └── tinker_example.rb ├── spec ├── spec_helper.rb ├── fixtures │ └── vcr_cassettes │ │ ├── spark_timeout.yml │ │ ├── bad_device_id.yml │ │ ├── bad_token.yml │ │ ├── function.yml │ │ ├── info.yml │ │ ├── analog_read.yml │ │ ├── digital_read.yml │ │ ├── analog_write.yml │ │ ├── digital_write.yml │ │ └── variable.yml └── spark │ ├── tinker_spec.rb │ └── device_spec.rb ├── LICENSE.txt ├── ruby_spark.gemspec └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/ruby_spark/version.rb: -------------------------------------------------------------------------------- 1 | module RubySpark 2 | VERSION = "2.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ruby_spark.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /examples/device_example.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "ruby_spark" 3 | 4 | device = RubySpark::Device.new(ARGV[0], ARGV[1]) 5 | 6 | p device.info 7 | p device.function("set", "1") 8 | 9 | while true do 10 | p device.variable("light") 11 | end 12 | -------------------------------------------------------------------------------- /examples/tinker_example.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "ruby_spark" 3 | 4 | device = RubySpark::Tinker.new(ARGV[0], ARGV[1]) 5 | 6 | p device.info 7 | 8 | while true do 9 | p device.digital_write("6", ["HIGH", "LOW"].sample) 10 | sleep(3) 11 | end 12 | -------------------------------------------------------------------------------- /lib/ruby_spark.rb: -------------------------------------------------------------------------------- 1 | require 'httparty' 2 | 3 | require 'ruby_spark/version' 4 | require 'ruby_spark/device' 5 | require 'ruby_spark/tinker' 6 | require 'helpers/configuration' 7 | 8 | module RubySpark 9 | class ConfigurationError < StandardError; end 10 | 11 | extend Configuration 12 | 13 | define_setting :access_token 14 | define_setting :timeout, 30 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'vcr' 4 | 5 | require 'ruby_spark' # and any other gems you need 6 | 7 | VCR.configure do |c| 8 | c.cassette_library_dir = 'spec/fixtures/vcr_cassettes' 9 | c.hook_into :webmock 10 | c.default_cassette_options = { 11 | :record => :once, 12 | :allow_playback_repeats => true, 13 | } 14 | c.allow_http_connections_when_no_cassette = false 15 | end 16 | 17 | RSpec.configure do |config| 18 | # some (optional) config here 19 | end 20 | -------------------------------------------------------------------------------- /lib/helpers/configuration.rb: -------------------------------------------------------------------------------- 1 | module Configuration 2 | 3 | def configuration 4 | yield self 5 | end 6 | 7 | def define_setting(name, default = nil) 8 | class_variable_set("@@#{name}", default) 9 | 10 | define_class_method "#{name}=" do |value| 11 | class_variable_set("@@#{name}", value) 12 | end 13 | 14 | define_class_method name do 15 | class_variable_get("@@#{name}") 16 | end 17 | end 18 | 19 | private 20 | 21 | def define_class_method(name, &block) 22 | (class << self; self; end).instance_eval do 23 | define_method name, &block 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/ruby_spark/tinker.rb: -------------------------------------------------------------------------------- 1 | module RubySpark 2 | 3 | class Tinker < Device 4 | def digital_write(pin, message) 5 | response = post('digitalwrite', :params => "D#{pin},#{message}") 6 | handle(response) do 7 | response["return_value"] == 1 8 | end 9 | end 10 | 11 | def digital_read(pin) 12 | response = post('digitalread', :params => "D#{pin}") 13 | handle(response) do 14 | response["return_value"] == 1 ? "HIGH" : "LOW" 15 | end 16 | end 17 | 18 | def analog_write(pin, value) 19 | response = post('analogwrite', :params => "A#{pin},#{value}") 20 | handle(response) do 21 | response["return_value"] == 1 22 | end 23 | end 24 | 25 | def analog_read(pin) 26 | response = post('analogread', :params => "A#{pin}") 27 | handle(response) do 28 | response["return_value"] 29 | end 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/spark_timeout.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.particle.io/v1/devices/good_device_id/?access_token=good_access_token 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: {} 10 | response: 11 | status: 12 | code: 403 13 | message: Forbidden 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:50:42 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '34' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"error\": \"Timed out.\"\n}" 32 | http_version: 33 | recorded_at: Thu, 20 Feb 2014 21:50:42 GMT 34 | recorded_with: VCR 2.5.0 35 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/bad_device_id.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.particle.io/v1/devices/bad_device_id/?access_token=good_access_token 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: {} 10 | response: 11 | status: 12 | code: 403 13 | message: Forbidden 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:50:42 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '34' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"error\": \"Permission Denied\"\n}" 32 | http_version: 33 | recorded_at: Thu, 20 Feb 2014 21:50:42 GMT 34 | recorded_with: VCR 2.5.0 35 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/bad_token.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.particle.io/v1/devices/good_device_id/?access_token=bad_token 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: {} 10 | response: 11 | status: 12 | code: 400 13 | message: Bad Request 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:47:57 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '109' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"code\": 400,\n \"error\": \"invalid_grant\",\n \"error_description\": 32 | \"The access token provided is invalid.\"\n}" 33 | http_version: 34 | recorded_at: Thu, 20 Feb 2014 21:47:57 GMT 35 | recorded_with: VCR 2.5.0 36 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/function.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.particle.io/v1/devices/good_device_id/readTemp 6 | body: 7 | encoding: US-ASCII 8 | string: access_token=good_access_token¶ms=outside 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:45:09 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '121' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"id\": \"good_device_id\",\n \"name\": \"chippy\",\n 32 | \ \"last_app\": null,\n \"connected\": true,\n \"return_value\": 72\n}" 33 | http_version: 34 | recorded_at: Thu, 20 Feb 2014 21:45:09 GMT 35 | recorded_with: VCR 2.5.0 36 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/info.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.particle.io/v1/devices/good_device_id/?access_token=good_access_token 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:43:08 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '144' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"id\": \"good_device_id\",\n \"name\": \"chippy\",\n 32 | \ \"variables\": {\n \"temperature\": \"int32\"\n },\n \"functions\": 33 | [\n \"readTemp\"\n ]\n}" 34 | http_version: 35 | recorded_at: Thu, 20 Feb 2014 21:43:07 GMT 36 | recorded_with: VCR 2.5.0 37 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/analog_read.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.particle.io/v1/devices/good_device_id/analogread 6 | body: 7 | encoding: UTF-8 8 | string: access_token=good_access_token¶ms=A6 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Wed, 25 Dec 2013 06:13:11 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '127' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: UTF-8 31 | string: |- 32 | { 33 | "id": "good_device_id", 34 | "name": "First Device", 35 | "last_app": null, 36 | "connected": true, 37 | "return_value": 2399 38 | } 39 | http_version: 40 | recorded_at: Wed, 25 Dec 2013 06:13:11 GMT 41 | recorded_with: VCR 2.5.0 42 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/digital_read.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.particle.io/v1/devices/good_device_id/digitalread 6 | body: 7 | encoding: UTF-8 8 | string: access_token=good_access_token¶ms=D6 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Wed, 25 Dec 2013 06:13:03 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '124' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: UTF-8 31 | string: |- 32 | { 33 | "id": "good_device_id", 34 | "name": "First Device", 35 | "last_app": null, 36 | "connected": true, 37 | "return_value": 1 38 | } 39 | http_version: 40 | recorded_at: Wed, 25 Dec 2013 06:13:03 GMT 41 | recorded_with: VCR 2.5.0 42 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/analog_write.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.particle.io/v1/devices/good_device_id/analogwrite 6 | body: 7 | encoding: UTF-8 8 | string: access_token=good_access_token¶ms=A7%2C130 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Wed, 25 Dec 2013 06:12:18 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '124' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: UTF-8 31 | string: |- 32 | { 33 | "id": "good_device_id", 34 | "name": "First Device", 35 | "last_app": null, 36 | "connected": true, 37 | "return_value": 1 38 | } 39 | http_version: 40 | recorded_at: Wed, 25 Dec 2013 06:12:18 GMT 41 | recorded_with: VCR 2.5.0 42 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/digital_write.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.particle.io/v1/devices/good_device_id/digitalwrite 6 | body: 7 | encoding: UTF-8 8 | string: access_token=good_access_token¶ms=D7%2CHIGH 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Wed, 25 Dec 2013 06:11:50 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '124' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: UTF-8 31 | string: |- 32 | { 33 | "id": "good_device_id", 34 | "name": "First Device", 35 | "last_app": null, 36 | "connected": true, 37 | "return_value": 1 38 | } 39 | http_version: 40 | recorded_at: Wed, 25 Dec 2013 06:11:51 GMT 41 | recorded_with: VCR 2.5.0 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Eli Fatsi 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. 23 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/variable.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.particle.io/v1/devices/good_device_id/temperature?access_token=good_access_token 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: {} 10 | response: 11 | status: 12 | code: 200 13 | message: OK 14 | headers: 15 | Access-Control-Allow-Origin: 16 | - ! '*' 17 | Content-Type: 18 | - application/json; charset=utf-8 19 | Date: 20 | - Thu, 20 Feb 2014 21:44:49 GMT 21 | Server: 22 | - nginx/1.4.2 23 | X-Powered-By: 24 | - Express 25 | Content-Length: 26 | - '218' 27 | Connection: 28 | - keep-alive 29 | body: 30 | encoding: US-ASCII 31 | string: ! "{\n \"cmd\": \"VarReturn\",\n \"name\": \"temperature\",\n \"result\": 32 | 70,\n \"deviceInfo\": {\n \"last_app\": \"\",\n \"last_heard\": \"2014-02-20T21:44:49.566Z\",\n 33 | \ \"connected\": true,\n \"deviceID\": \"good_device_id\"\n 34 | \ }\n}" 35 | http_version: 36 | recorded_at: Thu, 20 Feb 2014 21:44:49 GMT 37 | recorded_with: VCR 2.5.0 38 | -------------------------------------------------------------------------------- /ruby_spark.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ruby_spark/version' 5 | require 'ruby_spark/device' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "ruby_spark" 9 | spec.version = RubySpark::VERSION 10 | spec.authors = ["Eli Fatsi"] 11 | spec.email = ["eli.fatsi@viget.com"] 12 | spec.description = "Ruby Gem to make API calls to the Particle Cloud" 13 | spec.summary = "Ruby Gem to make API calls to the Particle Cloud" 14 | spec.homepage = "http://github.com/efatsi/ruby_spark" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files`.split($/) 18 | spec.test_files = spec.files.grep(%r{^spec/}) 19 | 20 | spec.add_development_dependency "bundler", "~> 1.3" 21 | spec.add_development_dependency "rake", "~> 10.1" 22 | spec.add_development_dependency "rspec", "~> 2.14" 23 | spec.add_development_dependency "vcr", "~> 2.5" 24 | spec.add_development_dependency "webmock", "~> 1.11" 25 | spec.add_development_dependency "pry", "~> 0.9" 26 | 27 | spec.add_dependency "httparty", "~> 0.13" 28 | end 29 | -------------------------------------------------------------------------------- /spec/spark/tinker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'pry' 3 | 4 | describe RubySpark::Tinker do 5 | 6 | context "with Access Token set in config variable" do 7 | before { RubySpark.access_token = "good_access_token" } 8 | subject { described_class.new("good_device_id") } 9 | 10 | describe "#digital_write" do 11 | it "succeeds when Access Token and Device ID are correct" do 12 | VCR.use_cassette("digital_write") do 13 | subject.digital_write(7, "HIGH").should == true 14 | end 15 | end 16 | end 17 | 18 | describe "#digital_read" do 19 | it "succeeds when Access Token and Device ID are correct" do 20 | VCR.use_cassette("digital_read") do 21 | subject.digital_read(6).should == "HIGH" 22 | end 23 | end 24 | end 25 | 26 | describe "#analog_write" do 27 | it "succeeds when Access Token and Device ID are correct" do 28 | VCR.use_cassette("analog_write") do 29 | subject.analog_write(7, 130).should == true 30 | end 31 | end 32 | end 33 | 34 | describe "#analog_read" do 35 | it "succeeds when Access Token and Device ID are correct" do 36 | VCR.use_cassette("analog_read") do 37 | subject.analog_read(6).should == 2399 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/ruby_spark/device.rb: -------------------------------------------------------------------------------- 1 | module RubySpark 2 | 3 | class Device 4 | class ApiError < StandardError; end 5 | 6 | def initialize(device_id, access_token = RubySpark.access_token) 7 | raise RubySpark::ConfigurationError.new("Access Token not defined") if access_token.nil? 8 | 9 | @access_token = access_token 10 | @device_id = device_id 11 | end 12 | 13 | def info 14 | response = get("") 15 | handle(response) do 16 | response 17 | end 18 | end 19 | 20 | def variable(variable_name) 21 | response = get(variable_name) 22 | handle(response) do 23 | response["result"] 24 | end 25 | end 26 | 27 | def function(function_name, arguments) 28 | response = post(function_name, :params => arguments) 29 | handle(response) do 30 | response["return_value"] 31 | end 32 | end 33 | 34 | private 35 | 36 | def post(action, params = {}) 37 | url = base_url + action 38 | body = access_params.merge(params) 39 | 40 | HTTParty.post(url, :body => body, :timeout => timeout).parsed_response 41 | end 42 | 43 | def get(action, params = {}) 44 | url = base_url + action 45 | query = access_params.merge(params) 46 | 47 | HTTParty.get(url, :query => query, :timeout => timeout).parsed_response 48 | end 49 | 50 | def handle(response, &block) 51 | if error = error_from(response) 52 | raise ApiError.new(error) if error.length > 0 53 | else 54 | yield block 55 | end 56 | end 57 | 58 | def error_from(response) 59 | response["error"].tap do |error| 60 | description = response["error_description"] 61 | error.concat(": #{description}") if description 62 | error.concat(": Invalid Device ID") if error == "Permission Denied" 63 | end 64 | end 65 | 66 | def base_url 67 | "https://api.particle.io/v1/devices/#{@device_id}/" 68 | end 69 | 70 | def access_params 71 | {:access_token => @access_token} 72 | end 73 | 74 | def timeout 75 | RubySpark.timeout 76 | end 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RubySpark 2 | 3 | Easily control you [Particle](http://particle.io) (formerly Spark) Device with Ruby. 4 | 5 | ## Obtaining a Particle Access Token and Device ID 6 | 7 | Assuming at this point you've followed Particle's [Getting Started](http://docs.particle.io/#/start) guides and connected your Device with the Particle Cloud. 8 | 9 | Head over to the [Particle Build IDE](https://www.particle.io/build). In the Settings tab you can get your Access Token, and you can fetch your Device ID from the Devices tab. You'll need these both to authenticate your API calls, and the Device ID to direct them. 10 | 11 | ## Installation 12 | 13 | To use this gem, install it with `gem install ruby_spark` or add this line to your Gemfile: 14 | 15 | ```ruby 16 | gem 'ruby_spark' 17 | ``` 18 | 19 | and install it with `bundle install` 20 | 21 | ## Usage 22 | 23 | Load: 24 | 25 | ```ruby 26 | require 'ruby_spark' 27 | ``` 28 | 29 | Configure: 30 | 31 | ```ruby 32 | # config/initializers/ruby_spark.rb 33 | 34 | RubySpark.configuration do |config| 35 | config.access_token = "spark_api_access_token" 36 | config.timeout = 10.seconds # defaults to 30 seconds 37 | end 38 | ``` 39 | 40 | ### Device API 41 | 42 | To instantiate a Device, you need to pass it's `device_id`. If you have your `access_token` setup ahead of time using the `config.access_token` then the second argument is optional. 43 | 44 | ```ruby 45 | device = RubySpark::Device.new("device_device_id") 46 | # or 47 | device = RubySpark::Device.new("device_device_id", "spark_api_access_token") 48 | ``` 49 | 50 | Fire away: 51 | 52 | ```ruby 53 | device.info #=> { info hash } 54 | 55 | device.variable("something") #=> number (for now) 56 | device.function("foo", "argument") #=> number 57 | ``` 58 | 59 | ### Tinker API 60 | 61 | The tinker class provides `digital_read`, `digital_write`, `analog_read`, and `analog_write` for the default spark device code. This is the same interface as the tinker app. 62 | 63 | ```ruby 64 | device = RubySpark::Tinker.new("device_device_id") 65 | # or 66 | device = RubySpark::Tinker.new("device_device_id", "spark_api_access_token") 67 | ``` 68 | 69 | Fire away: 70 | 71 | ```ruby 72 | device.info #=> { info hash } 73 | 74 | device.digital_write(3, "HIGH") #=> true or false 75 | device.digital_read(5) #=> "HIGH" or "LOW" 76 | ``` 77 | 78 | ## Contributing 79 | 80 | Happily accepting contributions. To contribute, fork, develop, add some specs, and pull request. 81 | 82 | Note about the specs. All API requests make use of the [VCR](https://github.com/vcr/vcr) gem. To contribute without exposing your Access Token and Device ID, run the specs with your real authentication, and then find-and-replace your Access Token and Device ID with fake values in the spec and any VCR cassettes. 83 | 84 | *** 85 | 86 | 87 | Code At Viget 88 | 89 | 90 | Visit [code.viget.com](http://code.viget.com) to see more projects from [Viget.](https://viget.com) 91 | -------------------------------------------------------------------------------- /spec/spark/device_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'pry' 3 | 4 | describe RubySpark::Device do 5 | 6 | context "when things go right" do 7 | before { RubySpark.access_token = "good_access_token" } 8 | subject { described_class.new("good_device_id") } 9 | 10 | describe "#info" do 11 | it "succeeds when Access Token and Device ID are correct" do 12 | VCR.use_cassette("info") do 13 | info = subject.info 14 | 15 | info.should be_a Hash 16 | info.keys.should =~ ["id", "name", "variables", "functions"] 17 | end 18 | end 19 | end 20 | 21 | describe "#variable" do 22 | it "succeeds when Access Token and Device ID are correct" do 23 | VCR.use_cassette("variable") do 24 | subject.variable("temperature").should == 70 25 | end 26 | end 27 | end 28 | 29 | describe "#function" do 30 | it "succeeds when Access Token and Device ID are correct" do 31 | VCR.use_cassette("function") do 32 | subject.function("readTemp", "outside").should == 72 33 | end 34 | end 35 | end 36 | end 37 | 38 | context "when things go wrong" do 39 | it "returns the appropriate error when Access Token is bad" do 40 | RubySpark.access_token = "bad_token" 41 | subject = described_class.new("good_device_id") 42 | 43 | VCR.use_cassette("bad_token") do 44 | expect { 45 | subject.info 46 | }.to raise_error(RubySpark::Device::ApiError) 47 | end 48 | 49 | VCR.use_cassette("bad_token") do 50 | begin 51 | subject.info 52 | rescue => e 53 | e.message.should == "invalid_grant: The access token provided is invalid." 54 | end 55 | end 56 | end 57 | 58 | it "returns proper error if Access Token is not defined" do 59 | RubySpark.access_token = nil 60 | 61 | expect { 62 | subject = described_class.new("good_device_id") 63 | }.to raise_error(RubySpark::ConfigurationError) 64 | end 65 | 66 | it "returns the appropriate error when Device ID is bad" do 67 | RubySpark.access_token = "good_access_token" 68 | subject = described_class.new("bad_device_id") 69 | 70 | VCR.use_cassette("bad_device_id") do 71 | expect { 72 | subject.info 73 | }.to raise_error(RubySpark::Device::ApiError) 74 | end 75 | 76 | VCR.use_cassette("bad_device_id") do 77 | begin 78 | subject.info 79 | rescue => e 80 | e.message.should == "Permission Denied: Invalid Device ID" 81 | end 82 | end 83 | end 84 | 85 | it "returns the appropriate error when the Particle API times out" do 86 | RubySpark.access_token = "good_access_token" 87 | subject = described_class.new("good_device_id") 88 | 89 | VCR.use_cassette("spark_timeout") do 90 | expect { 91 | subject.info 92 | }.to raise_error(RubySpark::Device::ApiError) 93 | end 94 | 95 | VCR.use_cassette("spark_timeout") do 96 | begin 97 | subject.info 98 | rescue => e 99 | e.message.should == "Timed out." 100 | end 101 | end 102 | end 103 | end 104 | 105 | end 106 | --------------------------------------------------------------------------------