├── error.html ├── .tool-versions ├── .travis.yml ├── spec ├── fixtures │ ├── not_found_error.json │ ├── create_dashboard.json │ ├── permission.json │ ├── create_trigger.json │ ├── user.json │ ├── group.json │ ├── token.json │ ├── trigger.json │ ├── feed.json │ ├── layouts.json │ ├── data.json │ ├── block.json │ ├── feed_details.json │ ├── activities.json │ └── dashboard.json ├── io_spec.rb ├── adafruit │ └── io │ │ ├── user_spec.rb │ │ ├── client_spec.rb │ │ ├── tokens_spec.rb │ │ ├── activities_spec.rb │ │ ├── data_spec.rb │ │ ├── triggers_spec.rb │ │ ├── permissions_spec.rb │ │ ├── dashboard_spec.rb │ │ ├── block_spec.rb │ │ ├── group_spec.rb │ │ └── feed_spec.rb ├── README.md ├── helper.rb └── support │ └── shared_contexts.rb ├── lib └── adafruit │ ├── io │ ├── version.rb │ ├── client │ │ ├── user.rb │ │ ├── activities.rb │ │ ├── tokens.rb │ │ ├── triggers.rb │ │ ├── dashboards.rb │ │ ├── groups.rb │ │ ├── feeds.rb │ │ ├── permissions.rb │ │ ├── blocks.rb │ │ └── data.rb │ ├── configurable.rb │ ├── arguments.rb │ ├── client.rb │ ├── request_handler.rb │ └── mqtt.rb │ └── io.rb ├── Rakefile ├── .gitignore ├── CONTRIBUTORS.md ├── examples ├── v2 │ ├── dashboards.rb │ ├── tokens.rb │ ├── activities.rb │ ├── mqtt.rb │ ├── feeds.rb │ ├── groups.rb │ ├── triggers.rb │ ├── permissions.rb │ ├── data.rb │ └── complete_app.rb └── mqtt.rb ├── test.rb ├── Gemfile ├── CHANGELOG.md ├── LICENSE.md ├── .github └── workflows │ └── ci.yml ├── adafruit-io.gemspec └── README.md /error.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.1 4 | 5 | -------------------------------------------------------------------------------- /spec/fixtures/not_found_error.json: -------------------------------------------------------------------------------- 1 | {"error": "not found"} 2 | -------------------------------------------------------------------------------- /spec/io_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Adafruit::IO do 4 | 5 | end -------------------------------------------------------------------------------- /lib/adafruit/io/version.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | VERSION = "3.0.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/create_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test dashboard", 3 | "description": "this is a test dashboard" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :test => :spec 7 | task :default => :spec -------------------------------------------------------------------------------- /lib/adafruit/io.rb: -------------------------------------------------------------------------------- 1 | require "adafruit/io/client" 2 | require "adafruit/io/mqtt" 3 | require "adafruit/io/version" 4 | 5 | module Adafruit 6 | module IO 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/permission.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 10, 3 | "scope_value": null, 4 | "mode": "r", 5 | "user_id": 1, 6 | "created_at": "2017-09-18T17:27:07Z", 7 | "updated_at": "2018-02-22T17:46:39Z", 8 | "scope": "public" 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/create_trigger.json: -------------------------------------------------------------------------------- 1 | { 2 | "feed_id": 89, 3 | "operator": "gt", 4 | "value": "50", 5 | "action": "feed", 6 | "action_feed_id": 90, 7 | "action_value": "Trigger Target 6425e758 over 50!", 8 | "trigger_type": "reactive" 9 | } 10 | -------------------------------------------------------------------------------- /.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 | .env 19 | profile 20 | profile-dev 21 | .ruby-version 22 | release.md 23 | .rake_tasks~ 24 | -------------------------------------------------------------------------------- /spec/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Test User", 4 | "color": null, 5 | "username": "test_username", 6 | "role": "tester", 7 | "default_group_id": 1, 8 | "default_dashboard_id": 1, 9 | "time_zone": null, 10 | "created_at": "2017-03-29T18:03:59Z", 11 | "updated_at": "2017-03-29T18:04:00Z" 12 | } 13 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/user.rb: -------------------------------------------------------------------------------- 1 | 2 | module Adafruit 3 | module IO 4 | class Client 5 | module User 6 | 7 | # Get user associated with the current key. If this method returns nil 8 | # it means you have a bad key. 9 | def user(*args) 10 | get '/api/v2/user' 11 | end 12 | 13 | end 14 | end 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /spec/fixtures/group.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "test_username", 3 | "owner": { 4 | "id": 1, 5 | "username": "test_username" 6 | }, 7 | "id": 7, 8 | "key": "test-group-17d3c880", 9 | "name": "Test Group 17d3c880", 10 | "description": null, 11 | "visibility": "private", 12 | "created_at": "2017-04-12T20:30:48Z", 13 | "updated_at": "2017-04-12T20:30:48Z", 14 | "feeds": [ 15 | 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /spec/fixtures/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "key": "244f9689be334a94b2cd99b38631d1b0", 4 | "master": true, 5 | "createable": true, 6 | "readable": true, 7 | "updateable": true, 8 | "deleteable": true, 9 | "expiration": null, 10 | "user_id": 1, 11 | "feed_id": null, 12 | "created_at": "2017-04-13T18:15:20Z", 13 | "updated_at": "2017-04-13T18:15:20Z", 14 | "qr_code": "data:image/png;base64,i...", 15 | "status": "active" 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors (sorted alphabetically) 2 | ==================================== 3 | 4 | taken from the [github graph](https://github.com/adafruit/io-client-ruby/graphs/contributors) 5 | 6 | * [Adam Bachman](https://github.com/abachman) 7 | * [Justin Cooper](https://github.com/jwcooper) 8 | * [Nikita Avvakumov](https://github.com/NikitaAvvakumov) 9 | * [Petr Havelka](https://github.com/CiTroNaK) 10 | * [Ric Kamicar](https://github.com/rkam) 11 | * [Todd Treece](https://github.com/toddtreece) 12 | -------------------------------------------------------------------------------- /lib/adafruit/io/configurable.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | module Configurable 4 | 5 | # Client configuration variables 6 | attr_accessor :key, :username 7 | 8 | # Client configuration variables with defaults 9 | attr_writer :api_endpoint 10 | 11 | def api_endpoint 12 | if @api_endpoint.nil? 13 | 'https://io.adafruit.com' 14 | else 15 | @api_endpoint 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /examples/v2/dashboards.rb: -------------------------------------------------------------------------------- 1 | # Dashboards let you see your data on Adafruit IO. Dashboards are made of 2 | # blocks, blocks subscribe to feeds through block_feeds. 3 | 4 | require 'adafruit/io' 5 | 6 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 7 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 8 | # you run this script 9 | # 10 | # to show all HTTP request activity add `debug: true` 11 | api = Adafruit::IO::Client.new key: ENV['IO_KEY'], username: ENV['IO_USERNAME'] 12 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/activities.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Activities 5 | 6 | # Get all dashboards. 7 | def activities(*args) 8 | username, _ = extract_username(args) 9 | get api_url(username, 'activities') 10 | end 11 | 12 | def delete_activities(*args) 13 | username, _ = extract_username(args) 14 | delete api_url(username, 'activities') 15 | end 16 | end 17 | end 18 | end 19 | end 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/v2/tokens.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'adafruit/io' 3 | 4 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 5 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 6 | # you run this script 7 | # 8 | # to show all HTTP request activity add `debug: true` 9 | api_key = ENV['IO_KEY'] 10 | username = ENV['IO_USERNAME'] 11 | api = Adafruit::IO::Client.new key: api_key, username: username 12 | 13 | token = api.tokens[0] 14 | 15 | puts "CURRENT TOKEN" 16 | puts JSON.pretty_generate(token) 17 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require 'adafruit/io' 2 | 3 | aio = Adafruit::IO::Client.new :key => 'API_KEY_HERE' 4 | aio.send_data("Test Send", 22) 5 | puts aio.receive("Test Send") 6 | #client.create_feed({:name => "Weather Station", :mode => "output"}) 7 | #puts client.feeds(3) 8 | #aio.send_data("Test Send", 0) 9 | 10 | #10.times { 11 | # aio.send_data("Test Send", Random.rand(101)) 12 | # sleep(1) 13 | #} 14 | 15 | #puts aio.send_group("First Group", {"temperature" => 45, "humidity" => 32}) 16 | 17 | #hash = aio.receive_group("First Group") 18 | 19 | puts hash -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'coveralls_reborn', '~> 0.29', :require => false 5 | gem 'json', '~> 2.3', :platforms => [:ruby_18, :jruby] 6 | gem 'mime-types', '~> 3.7' 7 | gem 'netrc', '~> 0.11' 8 | gem 'rb-fsevent', '~> 0.11' 9 | gem 'rspec', '~> 3.13' 10 | gem 'simplecov', '~> 0.22', :require => false 11 | gem 'test-queue', '~> 0.11' 12 | gem 'vcr', '~> 6.3' 13 | gem 'webmock', '~> 3.26' 14 | gem 'dotenv', '~> 3.1' 15 | end 16 | 17 | # Specify your gem's dependencies in adafruit-io.gemspec 18 | gemspec 19 | -------------------------------------------------------------------------------- /spec/adafruit/io/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'returns user' do 12 | mock_response( 13 | path: api_path('user', username: nil), 14 | method: :get, 15 | status: 200, 16 | body: "[#{mock_user_json}]", 17 | ) 18 | 19 | user = @aio.user 20 | expect(user).not_to be_empty 21 | end 22 | 23 | end 24 | 25 | -------------------------------------------------------------------------------- /spec/fixtures/trigger.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "test_username", 3 | "owner": { 4 | "id": 1, 5 | "username": "test_username" 6 | }, 7 | "id": 6, 8 | "feed_id": 89, 9 | "operator": "gt", 10 | "created_at": "2017-12-19T18:44:32Z", 11 | "updated_at": "2017-12-19T18:44:32Z", 12 | "value": "50", 13 | "action": "feed", 14 | "to_feed_id": null, 15 | "action_feed_id": 90, 16 | "action_value": "Trigger Target 6425e758 over 50!", 17 | "trigger_type_id": 2, 18 | "enabled": true, 19 | "trigger_type": "reactive", 20 | "description": "If Trigger Target 6425e758 is greater than \"50\" then set Notifications 5fcf9820 to \"Trigger Target 6425e758 over 50!\"." 21 | } 22 | -------------------------------------------------------------------------------- /spec/adafruit/io/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Adafruit::IO::Client do 4 | before do 5 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 6 | @aio.api_endpoint = TEST_URL 7 | end 8 | 9 | describe "configuration" do 10 | it "sets a valid key" do 11 | expect(@aio.key).to eq MY_KEY 12 | end 13 | 14 | it "sends the proper user agent" do 15 | stub_request(:get, URI::join(TEST_URL, "/api/test")). 16 | with(headers: { 17 | 'User-Agent'=>"AdafruitIO-Ruby/#{ Adafruit::IO::VERSION } (#{ RUBY_PLATFORM })" 18 | }). 19 | to_return(:status => 200, :body => "", :headers => {}) 20 | 21 | expect { 22 | @aio.get('/api/test') 23 | }.not_to raise_error 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "test_username", 3 | "owner": { 4 | "id": 1, 5 | "username": "test_username" 6 | }, 7 | "id": 14, 8 | "name": "Garbage f40afe36", 9 | "description": null, 10 | "history": true, 11 | "unit_type": null, 12 | "unit_symbol": null, 13 | "last_value": null, 14 | "visibility": "private", 15 | "license": null, 16 | "created_at": "2017-04-11T20:09:47Z", 17 | "updated_at": "2017-04-11T20:09:47Z", 18 | "status_notify": false, 19 | "status_timeout": 60, 20 | "key": "garbage-f40afe36", 21 | "group": { 22 | "id": 1, 23 | "key": "default", 24 | "name": "Default", 25 | "user_id": 1 26 | }, 27 | "groups": [ 28 | { 29 | "id": 1, 30 | "key": "default", 31 | "name": "Default", 32 | "user_id": 1 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /spec/adafruit/io/tokens_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'returns tokens' do 12 | mock_response( 13 | path: api_path('tokens'), 14 | method: :get, 15 | status: 200, 16 | body: "[#{ fixture_json('token') }]", 17 | ) 18 | 19 | tokens = @aio.tokens 20 | expect(tokens).not_to be_empty 21 | end 22 | 23 | it 'returns a single token' do 24 | mock_response( 25 | path: api_path('tokens', 1), 26 | method: :get, 27 | status: 200, 28 | body: fixture_json('token') 29 | ) 30 | 31 | tokens = @aio.token(1) 32 | expect(tokens).not_to be_empty 33 | end 34 | 35 | end 36 | 37 | -------------------------------------------------------------------------------- /spec/adafruit/io/activities_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'returns the list of activities' do 12 | mock_response( 13 | path: api_path('activities'), 14 | method: :get, 15 | status: 200, 16 | body: mock_activities_json, 17 | ) 18 | 19 | activities = @aio.activities 20 | expect(activities).not_to be_empty 21 | end 22 | 23 | it 'deletes all activities' do 24 | mock_response( 25 | path: api_path('activities'), 26 | method: :delete, 27 | status: 200, 28 | body: '' 29 | ) 30 | 31 | body = @aio.delete_activities 32 | expect(body).to be_empty 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.0.0 2 | ---- 3 | - update dependencies for ruby 3.x, potentially breaking older ruby versions 4 | 5 | 2.0.0 6 | ---- 7 | - initial API v2 client release 8 | - basic API support for: activities, blocks, dashboards, data, feeds, groups, permissions, tokens, triggers, and user records 9 | - basic MQTT client 10 | 11 | 12 | 13 | 2.0.0.beta.3 14 | ---- 15 | - MQTT Client 16 | 17 | 2.0.0.beta.1 18 | ---- 19 | - Feeds, Data, and Groups 20 | 21 | 1.1.0 22 | ---- 23 | - updated test suite 24 | - consolidated shared functionality into IOObject (thanks @rkam) 25 | - deprecate Group#groups 26 | 27 | 1.0.3 28 | ---- 29 | Update outgoing user agent 30 | 31 | 1.0.2 32 | ---- 33 | Remove unnecessary put statements 34 | 35 | 1.0.1 36 | ---- 37 | Fix host for connecting to io. 38 | Remove hashie completely. 39 | 40 | 1.0.0 41 | ---- 42 | Initial release 43 | 44 | 0.0.1 45 | ---- 46 | Initial Changelog 47 | -------------------------------------------------------------------------------- /examples/v2/activities.rb: -------------------------------------------------------------------------------- 1 | # Activities are the history of your activity on Adafruit IO. 2 | 3 | require 'adafruit/io' 4 | 5 | def actify(action) 6 | action.end_with?('e') ? 7 | (action + 'd').upcase : 8 | (action + 'ed').upcase 9 | end 10 | 11 | def nice_time(tstring) 12 | time = Time.parse(tstring) 13 | time.strftime('%Y-%m-%d %r') 14 | end 15 | 16 | 17 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 18 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 19 | # you run this script 20 | # 21 | # to show all HTTP request activity add `debug: true` 22 | api = Adafruit::IO::Client.new key: ENV['IO_KEY'], username: ENV['IO_USERNAME'] 23 | 24 | activities = api.activities 25 | 26 | activities.each do |act| 27 | puts "%s I %s A %s NAMED \"%s\"" % [ 28 | nice_time(act['created_at']), 29 | actify(act['action']), 30 | act['model'].upcase, 31 | act['data']['name'] 32 | ] 33 | end 34 | 35 | # permanently clear activity history 36 | # api.delete_activities 37 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/tokens.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Tokens 5 | 6 | # Get all tokens. 7 | def tokens(*args) 8 | username, _ = extract_username(args) 9 | 10 | get api_url(username, 'tokens') 11 | end 12 | 13 | # Get a token specified by key 14 | def token(*args) 15 | username, arguments = extract_username(args) 16 | token_id = get_id_from_arguments(arguments) 17 | 18 | get api_url(username, 'tokens', token_id) 19 | end 20 | 21 | # Create a token. No attributes need to be passed in. 22 | def create_token(*args) 23 | username, arguments = extract_username(args) 24 | 25 | post api_url(username, 'tokens') 26 | end 27 | 28 | def delete_token(*args) 29 | username, arguments = extract_username(args) 30 | token_id = get_id_from_arguments(arguments) 31 | 32 | delete api_url(username, 'tokens', token_id) 33 | end 34 | 35 | end 36 | end 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /spec/fixtures/layouts.json: -------------------------------------------------------------------------------- 1 | { 2 | "lg": [ 3 | { 4 | "i": "10", 5 | "x": 0, 6 | "y": 0, 7 | "w": 2, 8 | "h": 4 9 | }, 10 | { 11 | "i": "2", 12 | "x": 2, 13 | "y": 0, 14 | "w": 4, 15 | "h": 4 16 | }, 17 | { 18 | "i": "1", 19 | "x": 0, 20 | "y": 4, 21 | "w": 6, 22 | "h": 4 23 | }, 24 | { 25 | "i": "4", 26 | "x": 6, 27 | "y": 4, 28 | "w": 6, 29 | "h": 4 30 | }, 31 | { 32 | "i": "3", 33 | "x": 6, 34 | "y": 0, 35 | "w": 6, 36 | "h": 4 37 | }, 38 | { 39 | "i": "105", 40 | "x": 12, 41 | "y": 0, 42 | "w": 4, 43 | "h": 8 44 | } 45 | ], 46 | "md": [ 47 | { 48 | "i": "26", 49 | "x": 0, 50 | "y": 0, 51 | "w": 6, 52 | "h": 4 53 | }, 54 | { 55 | "i": "27", 56 | "x": 0, 57 | "y": 4, 58 | "w": 6, 59 | "h": 4 60 | }, 61 | { 62 | "i": "105", 63 | "x": 0, 64 | "y": 0, 65 | "w": 4, 66 | "h": 4 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /spec/fixtures/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "value": "80", 5 | "position": null, 6 | "feed_id": 1, 7 | "group_id": null, 8 | "expiration": null, 9 | "lat": 0, 10 | "lon": 0, 11 | "ele": 0, 12 | "completed_at": null, 13 | "created_at": "2016-08-15T20:29:00Z", 14 | "updated_at": "2016-08-15T20:29:00Z", 15 | "created_epoch": 1471292956.85221 16 | }, 17 | { 18 | "id": 2, 19 | "value": "81", 20 | "position": null, 21 | "feed_id": 1, 22 | "group_id": null, 23 | "expiration": null, 24 | "lat": 0, 25 | "lon": 0, 26 | "ele": 0, 27 | "completed_at": null, 28 | "created_at": "2016-08-15T20:30:00Z", 29 | "updated_at": "2016-08-15T20:30:00Z", 30 | "created_epoch": 1471292956.85221 31 | }, 32 | { 33 | "id": 3, 34 | "value": "82", 35 | "position": null, 36 | "feed_id": 1, 37 | "group_id": null, 38 | "expiration": null, 39 | "lat": 0, 40 | "lon": 0, 41 | "ele": 0, 42 | "completed_at": null, 43 | "created_at": "2016-08-15T20:31:00Z", 44 | "updated_at": "2016-08-15T20:31:00Z", 45 | "created_epoch": 1471292956.85221 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Adafruit 2 | Author: Justin Cooper, Adam Bachman 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /spec/adafruit/io/data_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | context 'with a feed having data' do 12 | context 'data' do 13 | it 'returns feed data' do 14 | # get Data 15 | mock_response( 16 | path: "api/v2/test_username/feeds/#{ mock_feed['key'] }/data", 17 | method: :get, 18 | status: 200, 19 | body: mock_data_json, 20 | ) 21 | data = @aio.data mock_feed 22 | 23 | expect(data.size).to eq(3) 24 | end 25 | 26 | it 'accepts params' do 27 | # get DATA 28 | mock_response( 29 | path: "api/v2/test_username/feeds/#{ mock_feed['key'] }/data?end_time=2001-01-01&start_time=2000-01-01", 30 | method: :get, 31 | status: 200, 32 | body: mock_data_json, 33 | ) 34 | data = @aio.data(mock_feed, start_time: '2000-01-01', end_time: '2001-01-01') 35 | 36 | expect(data.size).to eq(3) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: 17 | - '3.2' 18 | - '3.3' 19 | - '3.4' 20 | 21 | name: Ruby ${{ matrix.ruby }} Tests 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler: latest 32 | bundler-cache: true 33 | 34 | - name: Display Ruby version 35 | run: ruby -v 36 | 37 | - name: Display Bundler version 38 | run: bundle -v 39 | 40 | - name: Install dependencies 41 | run: bundle install --jobs 4 --retry 3 42 | 43 | - name: Run tests 44 | run: bundle exec rspec 45 | 46 | - name: Check code coverage (Ruby 3.4 only) 47 | if: matrix.ruby == '3.4' 48 | run: | 49 | if [ -f coverage/.last_run.json ]; then 50 | echo "Code coverage report generated" 51 | cat coverage/.last_run.json 52 | fi 53 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # Adafruit IO Ruby Client Test README 2 | 3 | To run the tests you can use rake from the top level directory. 4 | 5 | However, most tests require a valid Adafruit IO account to run, and so an 6 | account key needs to be provided. The key is passed to the tests via an 7 | environment variable called ADAFRUIT_IO_KEY. 8 | 9 | So, the tests can be run via the following command: 10 | 11 | ADAFRUIT_IO_KEY=my_key rake test 12 | 13 | Alternatively, you can place the key into a file in the top level directory 14 | called _.env_. The format is the same as above, but as a single line and 15 | by itself. e.g. 16 | 17 | # cat .env 18 | ADAFRUIT_IO_KEY=my_key 19 | 20 | # NOTES 21 | 22 | - Take care running the tests that you do not spam the server. The server has 23 | throttle checks in place and if you exceed the limit, you will be blocked for 24 | a period of time. See 25 | [HTTP 503: Unavailable](https://learn.adafruit.com/adafruit-io/http-status-codes). 26 | See also [Data Policies](https://learn.adafruit.com/adafruit-io/data-policies). 27 | 28 | - Additionally, you must have one feed, one group and one dashboard available 29 | for creating. (As of this writing, you are allowed only 10, 10, and 5, 30 | respectively). Again see 31 | [Data Policies](https://learn.adafruit.com/adafruit-io/data-policies) 32 | -------------------------------------------------------------------------------- /adafruit-io.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'adafruit/io/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "adafruit-io" 8 | spec.version = Adafruit::IO::VERSION 9 | spec.authors = ["Justin Cooper", "Adam Bachman"] 10 | spec.email = ["justin@adafruit.com"] 11 | spec.summary = %q{Adafruit IO API Client Library} 12 | spec.description = %q{API Client Library for the Adafruit IO product} 13 | spec.homepage = "https://github.com/adafruit/io-client-ruby" 14 | spec.license = "MIT" 15 | 16 | spec.files = %w(LICENSE.md README.md Rakefile adafruit-io.gemspec) 17 | spec.files += Dir.glob("lib/**/*.rb") 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ["lib"] 21 | spec.licenses = ['MIT'] 22 | 23 | spec.add_dependency "faraday", "~> 2.14" 24 | spec.add_dependency "faraday-multipart", "~> 1.0" 25 | spec.add_dependency "activesupport", "~> 8.0" 26 | spec.add_dependency "mqtt", "~> 0.7" 27 | 28 | spec.add_development_dependency "bundler", "~> 2.7" 29 | spec.add_development_dependency "rake", "~> 13.2" 30 | end 31 | -------------------------------------------------------------------------------- /examples/v2/mqtt.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'adafruit/io' 3 | 4 | api_key = ENV['IO_KEY'] 5 | username = ENV['IO_USER'] 6 | 7 | # Optionally set uri (hostname of Adafruit IO compatible MQTT service), 8 | # protocol, and port. 9 | mqtt = Adafruit::IO::MQTT.new(username, api_key, 10 | uri: 'io.adafruit.vm', 11 | protocol: 'mqtt', 12 | port: 1883 13 | ) 14 | 15 | # background listener 16 | Thread.new do 17 | # subscriptions accumulate, so calling .subscribe multiple times will cause 18 | # each feed to be subscribed to 19 | mqtt.subscribe('feed-a') 20 | mqtt.subscribe('feed-b') 21 | 22 | # subscribing to groups gets a Group JSON message 23 | mqtt.subscribe_group('default') 24 | 25 | # get messages as they are received 26 | loop do 27 | topic, message = mqtt.get 28 | puts "[receiving plain %s %s] %s" % [topic, Time.now, message] 29 | end 30 | 31 | # OR with a block 32 | # mqtt.get do |topic, message| 33 | # puts "[receiving block %s %s] %s" % [topic, Time.now, message] 34 | # end 35 | end 36 | 37 | value = 1 38 | loop do 39 | puts "[publishing] #{value}" 40 | 41 | ## publish to a single feed 42 | # mqtt.publish('sample-data', value) 43 | 44 | ## publish to multiple feeds in a group 45 | mqtt.publish_group('default', { 46 | 'feed-a' => value * rand(), 47 | 'feed-b' => value * rand(), 48 | }) 49 | 50 | value = (value + 1) % 100 51 | sleep 10 52 | end 53 | -------------------------------------------------------------------------------- /spec/fixtures/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string", 3 | "description": "string", 4 | "key": "1", 5 | "visual_type": "string", 6 | "column": 0, 7 | "row": 0, 8 | "size_x": 0, 9 | "size_y": 0, 10 | "block_feeds": [ 11 | { 12 | "id": "string", 13 | "feed": { 14 | "id": 0, 15 | "name": "string", 16 | "key": "string", 17 | "group": {}, 18 | "groups": [ 19 | { 20 | "id": 0, 21 | "name": "string", 22 | "description": "string", 23 | "feeds": [ 24 | {} 25 | ], 26 | "created_at": "string", 27 | "updated_at": "string" 28 | } 29 | ], 30 | "description": "string", 31 | "details": { 32 | "shared_with": [ 33 | {} 34 | ], 35 | "data": { 36 | "first": {}, 37 | "last": {}, 38 | "count": 0 39 | } 40 | }, 41 | "unit_type": "string", 42 | "unit_symbol": "string", 43 | "history": true, 44 | "visibility": "private", 45 | "license": "string", 46 | "enabled": true, 47 | "last_value": "string", 48 | "status": "string", 49 | "status_notify": true, 50 | "status_timeout": 0, 51 | "created_at": "string", 52 | "updated_at": "string" 53 | }, 54 | "group": null 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /examples/v2/feeds.rb: -------------------------------------------------------------------------------- 1 | # Feeds are where data is stored. Feeds can belong to one or more groups, all 2 | # feeds are in the default group unless otherwise specified. 3 | 4 | require 'adafruit/io' 5 | require 'securerandom' 6 | 7 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 8 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 9 | # you run this script 10 | # 11 | # to show all HTTP request activity add `debug: true` 12 | api_key = ENV['IO_KEY'] 13 | username = ENV['IO_USERNAME'] 14 | api = Adafruit::IO::Client.new key: api_key, username: username 15 | 16 | # create a feed 17 | puts "create" 18 | garbage = api.create_feed(name: "Garbage #{SecureRandom.hex(4)}") 19 | 20 | # add data 21 | puts "add data" 22 | api.send_data garbage, 'something' 23 | api.send_data garbage, 'goes here' 24 | 25 | # load data 26 | puts "load data" 27 | data = api.data(garbage) 28 | puts "#{data.size} points: #{ data.map {|d| d['value']}.join(', ') }" 29 | 30 | # get details 31 | puts "read" 32 | puts JSON.pretty_generate(api.feed_details(garbage)) 33 | 34 | # delete feed 35 | puts "delete" 36 | api.delete_feed(garbage) 37 | 38 | # try reading 39 | puts "read?" 40 | # ... get 404 41 | begin 42 | api.feed(garbage['key']) 43 | rescue => ex 44 | if ex.response.status === 404 45 | puts "expected error #{ex.response.status}: #{ex.message}" 46 | else 47 | puts "unexpected error! #{ex.message}" 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /examples/v2/groups.rb: -------------------------------------------------------------------------------- 1 | # Feeds are where data is stored. Feeds can belong to one or more groups, all 2 | # feeds are in the default group unless otherwise specified. 3 | 4 | require 'adafruit/io' 5 | require 'securerandom' 6 | 7 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 8 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 9 | # you run this script 10 | # 11 | # to show all HTTP request activity add `debug: true` 12 | api_key = ENV['IO_KEY'] 13 | username = ENV['IO_USERNAME'] 14 | api = Adafruit::IO::Client.new key: api_key, username: username 15 | 16 | # create a group 17 | puts "create" 18 | group = api.create_group(name: "Test Group #{SecureRandom.hex(4)}") 19 | puts JSON.pretty_generate(group) 20 | puts "-" * 80 21 | 22 | # create a feed 23 | feed = api.create_feed(name: "Test Feed #{SecureRandom.hex(4)}") 24 | 25 | # add feed to group 26 | puts "add feed" 27 | added = api.group_add_feed(group, feed) 28 | puts JSON.pretty_generate(added) 29 | puts "-" * 80 30 | 31 | # reload the group 32 | puts "reload group" 33 | group = api.group(group) 34 | puts JSON.pretty_generate(group) 35 | puts "-" * 80 36 | 37 | # remove the feed from the group 38 | puts "remove feed" 39 | removed = api.group_remove_feed(group, feed) 40 | puts JSON.pretty_generate(removed) 41 | puts "-" * 80 42 | 43 | # delete feed 44 | puts "delete feed" 45 | api.delete_feed(feed) 46 | 47 | # delete group 48 | puts "delete group" 49 | api.delete_group(group) 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/v2/triggers.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'adafruit/io' 3 | require 'securerandom' 4 | require 'json' 5 | 6 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 7 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 8 | # you run this script 9 | # 10 | # to show all HTTP request activity add `debug: true` 11 | api_key = ENV['IO_KEY'] 12 | username = ENV['IO_USERNAME'] 13 | api = Adafruit::IO::Client.new key: api_key, username: username 14 | 15 | trigs = api.triggers 16 | 17 | puts "all triggers:" 18 | if trigs.empty? 19 | puts " [empty]" 20 | else 21 | puts JSON.pretty_generate(trigs) 22 | 23 | trigs.each do |trig| 24 | puts "- #{trig}" 25 | end 26 | end 27 | 28 | puts "-------------------------" 29 | 30 | # 1) create a feed to watch 31 | watch = api.create_feed(name: "Trigger Target #{SecureRandom.hex(4)}") 32 | 33 | # 2) create a target feed for notifications 34 | notify = api.create_feed(name: "Notifications #{SecureRandom.hex(4)}") 35 | 36 | # 3) create a trigger that pings `notify` when `watch` goes over 50. 37 | trigger = api.create_trigger( 38 | # react 39 | trigger_type: 'reactive', 40 | # when feed 41 | feed_id: watch['id'], 42 | # is greater than 43 | operator: 'gt', 44 | # 50 45 | value: '50', 46 | 47 | # by sending a message to feed 48 | action: 'feed', 49 | action_feed_id: notify['id'], 50 | 51 | # with value 52 | action_value: "#{watch['name']} over 50!", 53 | ) 54 | 55 | puts "made a trigger: #{JSON.pretty_generate(trigger)}" 56 | 57 | -------------------------------------------------------------------------------- /examples/mqtt.rb: -------------------------------------------------------------------------------- 1 | # Example script using Adafruit::IO::MQTT to access adafruit.io 2 | # - Nicholas Humfrey has an excellent MQTT library, which is very easy to use. 3 | 4 | require 'adafruit/io' 5 | 6 | username = ENV['IO_USERNAME'] 7 | key = ENV['IO_KEY'] 8 | feed = 'welcome' 9 | 10 | connection_opts = { 11 | port: 8883, 12 | uri: 'io.adafruit.com', 13 | protocol: 'mqtts' 14 | } 15 | 16 | mqtt = Adafruit::IO::MQTT.new username, key, connection_opts 17 | 18 | Thread.new do 19 | puts "subscribe to #{feed}" 20 | mqtt.subscribe(feed, last_value: true) 21 | 22 | mqtt.get do |topic, message| 23 | puts "<- received #{feed} #{message} at #{Time.now.to_f}" 24 | end 25 | end 26 | 27 | # pause while subscription sets up in other thread 28 | sleep 2 29 | 30 | 5.times do |n| 31 | mqtt.publish feed, n 32 | puts "-> published #{n} at #{Time.now.to_f}" 33 | sleep 5 34 | end 35 | 36 | # output should look something like: 37 | # 38 | # $ ruby -Ilib/ examples/mqtt.rb 39 | # subscribe to welcome 40 | # <- received welcome 1 at 1525728547.548456 41 | # -> published 0 at 1525728549.474504 42 | # <- received welcome 0 at 1525728549.524008 43 | # -> published 1 at 1525728554.477849 44 | # <- received welcome 1 at 1525728554.524014 45 | # -> published 2 at 1525728559.4833012 46 | # <- received welcome 2 at 1525728559.5317278 47 | # -> published 3 at 1525728564.487375 48 | # <- received welcome 3 at 1525728564.5310678 49 | # -> published 4 at 1525728569.490098 50 | # <- received welcome 4 at 1525728569.54076 51 | # 52 | -------------------------------------------------------------------------------- /examples/v2/permissions.rb: -------------------------------------------------------------------------------- 1 | # Permissions control who can see what feeds. 2 | 3 | require 'adafruit/io' 4 | 5 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 6 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 7 | # you run this script 8 | # 9 | # to show all HTTP request activity add `debug: true` 10 | api_key = ENV['IO_KEY'] 11 | username = ENV['IO_USERNAME'] 12 | api = Adafruit::IO::Client.new key: api_key, username: username 13 | 14 | # If permissions test feed doesn't exist, create it 15 | feed = api.feed('permissions-example') rescue nil 16 | if feed.nil? 17 | puts 'creating feed "Permissions Test"' 18 | feed = api.create_feed(name: 'Permissions Test', key: 'permissions-example') 19 | end 20 | 21 | # get all permissions for a feed 22 | permissions = api.permissions('feed', feed['key']) 23 | puts "#{feed['name']} has permissions #{permissions}" 24 | 25 | # create (or update) public permission record for the feed 26 | perm = api.create_permission 'feed', feed['key'], scope: 'public', mode: 'r' 27 | puts "add public read permission to #{feed['name']} -> #{perm}" 28 | 29 | permissions = api.permissions('feed', feed['key']) 30 | puts "#{feed['name']} has permissions #{permissions}" 31 | 32 | # make the feed private again 33 | del = api.delete_permission 'feed', feed['key'], perm['id'] 34 | puts "deleted #{perm['id']} from #{feed['name']}" 35 | 36 | permissions = api.permissions('feed', feed['key']) 37 | puts "#{feed['name']} has permissions #{permissions}" 38 | 39 | api.delete_feed('permissions-example') 40 | puts "deleted example feed" 41 | 42 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/triggers.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Triggers 5 | 6 | # Get all triggers. 7 | def triggers(*args) 8 | username, _ = extract_username(args) 9 | 10 | get api_url(username, 'triggers') 11 | end 12 | 13 | # Get a trigger specified by key 14 | def trigger(*args) 15 | username, arguments = extract_username(args) 16 | trigger_id = get_id_from_arguments(arguments) 17 | 18 | get api_url(username, 'triggers', trigger_id) 19 | end 20 | 21 | # Create a trigger. No attributes need to be passed in. 22 | def create_trigger(*args) 23 | username, arguments = extract_username(args) 24 | attrs = valid_trigger_attrs(arguments) 25 | 26 | post api_url(username, 'triggers'), attrs 27 | end 28 | 29 | def delete_trigger(*args) 30 | username, arguments = extract_username(args) 31 | trigger_id = get_id_from_arguments(arguments) 32 | 33 | delete api_url(username, 'triggers', trigger_id) 34 | end 35 | 36 | def update_trigger(*args) 37 | username, arguments = extract_username(args) 38 | trigger_id = get_id_from_arguments(arguments) 39 | attrs = valid_trigger_attrs(arguments) 40 | 41 | put api_url(username, 'triggers', trigger_id), attrs 42 | end 43 | 44 | private 45 | 46 | def valid_trigger_attrs(arguments) 47 | get_query_from_arguments( 48 | arguments, 49 | %w(feed_id operator value action to_feed_id action_feed_id 50 | action_value enabled trigger_type) 51 | ) 52 | end 53 | 54 | end 55 | end 56 | end 57 | end 58 | 59 | -------------------------------------------------------------------------------- /examples/v2/data.rb: -------------------------------------------------------------------------------- 1 | # Data is the heart of Adafruit IO and priority for the API Library. All data 2 | # is stored and retreived in the context of a Feed. 3 | 4 | require 'adafruit/io' 5 | 6 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 7 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 8 | # you run this script 9 | # 10 | # to show all HTTP request activity add `debug: true` 11 | api_key = ENV['IO_KEY'] 12 | username = ENV['IO_USERNAME'] 13 | api = Adafruit::IO::Client.new key: api_key, username: username 14 | 15 | feed_key = "temperature-#{rand(1000000)}" 16 | 17 | temperature = api.feed(feed_key) rescue nil 18 | if temperature.nil? 19 | temperature = api.create_feed name: feed_key 20 | end 21 | 22 | # 23 | # Adding data 24 | # 25 | 26 | # generate some garbage data 27 | yesterday = (Time.now - 60 * 60 * 24) 28 | points = [] 29 | 20.times do |n| 30 | points << {value: rand() * 50, created_at: (yesterday + n * 10).utc.iso8601} 31 | end 32 | 33 | # and send it as a batch 34 | api.send_batch_data(temperature, points) 35 | 36 | # 37 | # Reading data with pagination 38 | # 39 | # With /data API, get all data points with pagination 40 | all_temperatures = [] 41 | end_time = Time.now 42 | 43 | loop do 44 | # use a limit that's lower than the amount of data published to force pagination 45 | results = api.data(temperature['key'], limit: 15, end_time: end_time) 46 | print '.' 47 | 48 | all_temperatures = all_temperatures.concat(results) 49 | 50 | break if api.last_page? 51 | end_time = api.pagination['start'] 52 | 53 | # pause to avoid overwhelming the rate limiter 54 | sleep 2 55 | end 56 | puts 57 | 58 | all_temperatures.each do |temp| 59 | puts "%-32s%-16.3f%-30s" % [temp['id'], temp['value'].to_f, temp['created_at']] 60 | end 61 | -------------------------------------------------------------------------------- /spec/adafruit/io/triggers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'gets the list of triggers' do 12 | mock_response( 13 | path: api_path('triggers'), 14 | method: :get, 15 | status: 200, 16 | body: "[#{mock_trigger_json}]", 17 | ) 18 | 19 | response = @aio.triggers 20 | expect(response).not_to be_empty 21 | end 22 | 23 | it 'creates a trigger' do 24 | mock_response( 25 | path: api_path('triggers'), 26 | method: :post, 27 | status: 200, 28 | with_request_body: true, 29 | body: mock_trigger_json, 30 | ) 31 | 32 | response = @aio.create_trigger(JSON.parse(mock_create_trigger_json)) 33 | expect(response).not_to be_empty 34 | end 35 | 36 | it 'gets a trigger' do 37 | mock_response( 38 | path: api_path('triggers', '1'), 39 | method: :get, 40 | status: 200, 41 | body: mock_trigger_json, 42 | ) 43 | 44 | response = @aio.trigger(1) 45 | expect(response).not_to be_empty 46 | end 47 | 48 | it 'updates a trigger' do 49 | mock_response( 50 | path: api_path('triggers', '1'), 51 | method: :put, 52 | status: 200, 53 | body: mock_trigger_json, 54 | ) 55 | 56 | response = @aio.update_trigger(1, {value: 100}) 57 | expect(response).not_to be_empty 58 | end 59 | 60 | it 'deletes a trigger' do 61 | mock_response( 62 | path: api_path('triggers', '1'), 63 | method: :delete, 64 | status: 200, 65 | body: mock_trigger_json, 66 | ) 67 | 68 | response = @aio.delete_trigger(1) 69 | expect(response).not_to be_empty 70 | end 71 | end 72 | 73 | 74 | -------------------------------------------------------------------------------- /spec/fixtures/feed_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "test_username", 3 | "owner": { 4 | "id": 1, 5 | "username": "test_username" 6 | }, 7 | "id": 13, 8 | "name": "Example 3a5d79b9", 9 | "description": null, 10 | "history": true, 11 | "unit_type": null, 12 | "unit_symbol": null, 13 | "last_value": "goes here", 14 | "visibility": "private", 15 | "license": null, 16 | "created_at": "2017-04-11T20:08:40Z", 17 | "updated_at": "2017-04-11T20:08:40Z", 18 | "status_notify": false, 19 | "status_timeout": 60, 20 | "key": "garbage-3a5d79b9", 21 | "group": { 22 | "id": 1, 23 | "key": "default", 24 | "name": "Default", 25 | "user_id": 1 26 | }, 27 | "details": { 28 | "shared_with": [ 29 | 30 | ], 31 | "data": { 32 | "first": { 33 | "id": 44320, 34 | "value": "something", 35 | "feed_id": 13, 36 | "created_at": "2017-04-11T20:08:40Z", 37 | "created_epoch": 1491941320.409515, 38 | "expiration": null, 39 | "group_id": null, 40 | "position": null, 41 | "completed_at": null, 42 | "lat": null, 43 | "lon": null, 44 | "ele": null, 45 | "location": null 46 | }, 47 | "last": { 48 | "id": 44321, 49 | "value": "goes here", 50 | "feed_id": 13, 51 | "created_at": "2017-04-11T20:08:40Z", 52 | "created_epoch": 1491941320.682605, 53 | "expiration": null, 54 | "group_id": null, 55 | "position": null, 56 | "completed_at": null, 57 | "lat": null, 58 | "lon": null, 59 | "ele": null, 60 | "location": null 61 | }, 62 | "count": 2 63 | } 64 | }, 65 | "groups": [ 66 | { 67 | "id": 1, 68 | "key": "default", 69 | "name": "Default", 70 | "user_id": 1 71 | } 72 | ] 73 | } 74 | 75 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/dashboards.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Dashboards 5 | 6 | # Get all dashboards. 7 | def dashboards(*args) 8 | username, _ = extract_username(args) 9 | 10 | get api_url(username, 'dashboards') 11 | end 12 | 13 | # Get a dashboard specified by key 14 | def dashboard(*args) 15 | username, arguments = extract_username(args) 16 | dashboard_key = get_key_from_arguments(arguments) 17 | 18 | get api_url(username, 'dashboards', dashboard_key) 19 | end 20 | 21 | # Create a dashboard. No attributes need to be passed in. 22 | def create_dashboard(*args) 23 | username, arguments = extract_username(args) 24 | attrs = arguments.shift 25 | 26 | post api_url(username, 'dashboards'), attrs 27 | end 28 | 29 | def delete_dashboard(*args) 30 | username, arguments = extract_username(args) 31 | dashboard_key = get_key_from_arguments(arguments) 32 | 33 | delete api_url(username, 'dashboards', dashboard_key) 34 | end 35 | 36 | def update_dashboard(*args) 37 | username, arguments = extract_username(args) 38 | dashboard_key = get_key_from_arguments(arguments) 39 | query = get_query_from_arguments(arguments, %w(name key)) 40 | 41 | put api_url(username, 'dashboards', dashboard_key), query 42 | end 43 | 44 | def update_dashboard_layouts(*args) 45 | username, arguments = extract_username(args) 46 | dashboard_key = get_key_from_arguments(arguments) 47 | query = get_query_from_arguments(arguments, %w(layouts)) 48 | 49 | post api_url(username, 'dashboards', dashboard_key, 'update_layouts'), query 50 | end 51 | end 52 | end 53 | end 54 | end 55 | 56 | 57 | -------------------------------------------------------------------------------- /spec/adafruit/io/permissions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | %w(feed group dashboard).each do |model_type| 12 | it "lists permissions for a #{model_type}" do 13 | mock_response( 14 | path: api_path("#{model_type}s", 'key', 'acl'), 15 | method: :get, 16 | status: 200, 17 | body: "[#{mock_permission_json}]", 18 | ) 19 | 20 | response = @aio.permissions(model_type, 'key') 21 | expect(response).not_to be_empty 22 | end 23 | 24 | it "deletes a permission for a #{model_type}" do 25 | mock_response( 26 | path: api_path("#{model_type}s", 'key', 'acl', '1'), 27 | method: :delete, 28 | status: 200, 29 | body: mock_permission_json, 30 | ) 31 | 32 | response = @aio.delete_permission(model_type, 'key', '1') 33 | expect(response).not_to be_empty 34 | end 35 | 36 | it "creates a public permision for a #{model_type}" do 37 | mock_response( 38 | path: api_path("#{model_type}s", 'key', 'acl'), 39 | method: :post, 40 | status: 200, 41 | with_request_body: true, 42 | body: mock_permission_json, 43 | ) 44 | 45 | response = @aio.create_permission(model_type, 'key', {scope: 'public', mode: 'r'}) 46 | expect(response).not_to be_empty 47 | end 48 | 49 | it "gets a permision for a #{model_type}" do 50 | mock_response( 51 | path: api_path("#{model_type}s", 'key', 'acl', '1'), 52 | method: :get, 53 | status: 200, 54 | body: mock_permission_json, 55 | ) 56 | 57 | response = @aio.permission(model_type, 'key', '1') 58 | expect(response).not_to be_empty 59 | end 60 | 61 | end 62 | end 63 | 64 | 65 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'json' 4 | require 'rspec' 5 | require 'webmock/rspec' 6 | 7 | begin 8 | require 'simplecov' 9 | SimpleCov.start 10 | rescue LoadError 11 | warn 'warning: simplecov not found, skipping coverage' 12 | end 13 | 14 | begin 15 | require 'dotenv' 16 | Dotenv.load 17 | rescue LoadError 18 | warn 'warning: dotenv not found, please make sure env contains proper variables' 19 | end 20 | 21 | require 'adafruit/io' 22 | 23 | # include all support files 24 | Dir["./spec/support/**/*.rb"].each {|f| require f} 25 | 26 | # isolate "normal" test suite from making calls to io.adafruit.com 27 | WebMock.disable_net_connect!(allow_localhost: true) 28 | 29 | def fixture_path 30 | File.expand_path("../fixtures", __FILE__) 31 | end 32 | 33 | def fixture(file) 34 | File.new(fixture_path + '/' + file) 35 | end 36 | 37 | $_fixture_json_files = {} 38 | def fixture_json(file) 39 | if $_fixture_json_files[file].nil? 40 | $_fixture_json_files[file] = fixture(file + '.json').read 41 | end 42 | $_fixture_json_files[file].dup 43 | end 44 | 45 | MY_KEY = 'blah'.freeze 46 | 47 | FEED_KEY1 = 'test_feed_1'.freeze 48 | FEED_DESC = 'My Test Feed Description'.freeze 49 | 50 | DATA_NAME1 = 'test_data_1'.freeze 51 | DATA_NAME2 = 'test_data_2'.freeze 52 | 53 | GROUP_NAME1 = 'test_group_1'.freeze 54 | GROUP_NAME2 = 'test_group_2'.freeze 55 | 56 | DASHBOARD_NAME1 = 'test_dashboard_1'.freeze 57 | DASHBOARD_NAME2 = 'test_dashboard_2'.freeze 58 | 59 | RSpec.describe 'initialization' do 60 | before do 61 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 62 | @aio.api_endpoint = TEST_URL 63 | end 64 | 65 | context 'starting with no feeds' do 66 | it 'found an IO key in the environment' do 67 | expect(MY_KEY).to be_a String 68 | end 69 | 70 | it 'can create a client using that key' do 71 | # use test-only key 72 | expect(@aio).to_not be_nil 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/groups.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Groups 5 | 6 | # Get all groups. 7 | def groups(*args) 8 | username, _ = extract_username(args) 9 | 10 | get api_url(username, 'groups') 11 | end 12 | 13 | # Get a group specified by key 14 | def group(*args) 15 | username, arguments = extract_username(args) 16 | group_key = get_key_from_arguments(arguments) 17 | 18 | get api_url(username, 'groups', group_key) 19 | end 20 | 21 | def group_add_feed(*args) 22 | username, arguments = extract_username(args) 23 | group_key = get_key_from_arguments(arguments) 24 | feed_key = get_key_from_arguments(arguments) 25 | 26 | post api_url(username, 'groups', group_key, 'add'), feed_key: feed_key 27 | end 28 | 29 | def group_remove_feed(*args) 30 | username, arguments = extract_username(args) 31 | group_key = get_key_from_arguments(arguments) 32 | feed_key = get_key_from_arguments(arguments) 33 | 34 | post api_url(username, 'groups', group_key, 'remove'), feed_key: feed_key 35 | end 36 | 37 | def create_group(*args) 38 | username, arguments = extract_username(args) 39 | group_attrs = arguments.shift 40 | 41 | post api_url(username, 'groups'), group_attrs 42 | end 43 | 44 | def delete_group(*args) 45 | username, arguments = extract_username(args) 46 | group_key = get_key_from_arguments(arguments) 47 | 48 | delete api_url(username, 'groups', group_key) 49 | end 50 | 51 | def update_group(*args) 52 | username, arguments = extract_username(args) 53 | group_key = get_key_from_arguments(arguments) 54 | query = get_query_from_arguments(arguments, %w(name key)) 55 | 56 | put api_url(username, 'groups', group_key), query 57 | end 58 | 59 | end 60 | end 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/feeds.rb: -------------------------------------------------------------------------------- 1 | 2 | module Adafruit 3 | module IO 4 | class Client 5 | module Feeds 6 | 7 | # Get all feeds. 8 | def feeds(*args) 9 | username, _ = extract_username(args) 10 | 11 | get api_url(username, 'feeds') 12 | end 13 | 14 | # Get a feed specified by key 15 | def feed(*args) 16 | username, arguments = extract_username(args) 17 | feed_key = get_key_from_arguments(arguments) 18 | 19 | get api_url(username, 'feeds', feed_key) 20 | end 21 | 22 | # Get a feed along with additional details about the feed. This method 23 | # has more to lookup and so is slower than `feed` 24 | def feed_details(*args) 25 | username, arguments = extract_username(args) 26 | feed_key = get_key_from_arguments(arguments) 27 | 28 | get api_url(username, 'feeds', feed_key, 'details') 29 | end 30 | 31 | def create_feed(*args) 32 | username, arguments = extract_username(args) 33 | feed_attrs = valid_feed_attrs(arguments) 34 | 35 | post api_url(username, 'feeds'), feed_attrs 36 | end 37 | 38 | def delete_feed(*args) 39 | username, arguments = extract_username(args) 40 | feed_key = get_key_from_arguments(arguments) 41 | 42 | delete api_url(username, 'feeds', feed_key) 43 | end 44 | 45 | def update_feed(*args) 46 | username, arguments = extract_username(args) 47 | feed_key = get_key_from_arguments(arguments) 48 | feed_attrs = valid_feed_attrs(arguments) 49 | 50 | put api_url(username, 'feeds', feed_key), feed_attrs 51 | end 52 | 53 | private 54 | 55 | def valid_feed_attrs(arguments) 56 | query = get_query_from_arguments( 57 | arguments, 58 | %w(name key description unit_type unit_symbol history visibility 59 | license status_notify status_timeout) 60 | ) 61 | end 62 | 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/fixtures/activities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "test_username", 4 | "owner": { 5 | "id": 1, 6 | "username": "test_username" 7 | }, 8 | "id": 413, 9 | "action": "create", 10 | "model": "Block", 11 | "data": "{\"id\":108,\"dashboard_id\":11,\"feed_id\":null,\"visual_type\":\"momentary_button\",\"created_at\":\"2018-05-03T16:26:39Z\",\"updated_at\":\"2018-05-03T16:26:39Z\",\"name\":\"Sample Data\",\"description\":null,\"position\":null,\"properties\":{\"text\":\"Reset\",\"value\":\"1\",\"release\":\"0\",\"backgroundColor\":\"#1B9AF7\"},\"column\":null,\"row\":null,\"size_x\":2,\"size_y\":2,\"source_key\":null,\"source\":null}", 12 | "user_id": 1, 13 | "created_at": "2018-05-03T16:26:39Z", 14 | "updated_at": "2018-05-03T16:26:39Z" 15 | }, 16 | { 17 | "username": "test_username", 18 | "owner": { 19 | "id": 1, 20 | "username": "test_username" 21 | }, 22 | "id": 410, 23 | "action": "create", 24 | "model": "Block", 25 | "data": "{\"id\":106,\"dashboard_id\":6,\"feed_id\":null,\"visual_type\":\"line_chart\",\"created_at\":\"2018-05-01T16:26:35Z\",\"updated_at\":\"2018-05-01T16:26:35Z\",\"name\":\"Counter One\",\"description\":null,\"position\":null,\"properties\":{\"xAxisLabel\":\"X\",\"yAxisLabel\":\"Y\",\"yAxisMin\":\"\",\"yAxisMax\":\"\",\"historyHours\":\"0\"},\"column\":null,\"row\":null,\"size_x\":6,\"size_y\":4,\"source_key\":null,\"source\":null}", 26 | "user_id": 1, 27 | "created_at": "2018-05-01T16:26:35Z", 28 | "updated_at": "2018-05-01T16:26:35Z" 29 | }, 30 | { 31 | "username": "test_username", 32 | "owner": { 33 | "id": 1, 34 | "username": "test_username" 35 | }, 36 | "id": 409, 37 | "action": "create", 38 | "model": "Block", 39 | "data": "{\"id\":105,\"dashboard_id\":1,\"feed_id\":null,\"visual_type\":\"stream\",\"created_at\":\"2018-04-26T20:41:19Z\",\"updated_at\":\"2018-04-26T20:41:19Z\",\"name\":\"feed-a\",\"description\":null,\"position\":null,\"properties\":{\"fontSize\":\"12\",\"fontColor\":\"#63de00\",\"showName\":\"yes\",\"showTimestamp\":\"yes\",\"errors\":\"yes\",\"showLocation\":\"no\"},\"column\":null,\"row\":null,\"size_x\":4,\"size_y\":4,\"source_key\":null,\"source\":null}", 40 | "user_id": 1, 41 | "created_at": "2018-04-26T20:41:19Z", 42 | "updated_at": "2018-04-26T20:41:19Z" 43 | } 44 | ] 45 | 46 | -------------------------------------------------------------------------------- /spec/adafruit/io/dashboard_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'returns dashboards' do 12 | mock_response( 13 | path: api_path('dashboards'), 14 | method: :get, 15 | status: 200, 16 | body: "[#{mock_dashboard_json}]", 17 | ) 18 | 19 | dashboards = @aio.dashboards 20 | expect(dashboards).not_to be_empty 21 | end 22 | 23 | it 'returns a dashboard' do 24 | mock_response( 25 | path: api_path('dashboards', 'dashboard-key'), 26 | method: :get, 27 | status: 200, 28 | body: mock_dashboard_json, 29 | ) 30 | 31 | dashboards = @aio.dashboard('dashboard-key') 32 | expect(dashboards).not_to be_empty 33 | end 34 | 35 | it 'creates a dashboard' do 36 | mock_response( 37 | path: api_path('dashboards'), 38 | method: :post, 39 | status: 200, 40 | body: mock_create_dashboard_json, 41 | ) 42 | 43 | dashboard = @aio.create_dashboard(JSON.parse(mock_create_dashboard_json)) 44 | expect(dashboard).not_to be_empty 45 | end 46 | 47 | it 'updates a dashboard' do 48 | mock_response( 49 | path: api_path('dashboards', 'dashboard-key'), 50 | method: :put, 51 | status: 200, 52 | body: mock_dashboard_json, 53 | ) 54 | 55 | dashboard = @aio.update_dashboard(key: 'dashboard-key', name: 'New Name') 56 | expect(dashboard).not_to be_empty 57 | end 58 | 59 | it 'updates dashboard layouts' do 60 | mock_response( 61 | path: api_path('dashboards', 'dashboard-key', 'update_layouts'), 62 | method: :post, 63 | status: 200, 64 | body: mock_dashboard_json, 65 | ) 66 | 67 | dashboard = @aio.update_dashboard_layouts(key: 'dashboard-key', layouts: JSON.parse(mock_layouts_json)) 68 | expect(dashboard).not_to be_empty 69 | end 70 | 71 | it 'deletes a dashboard' do 72 | mock_response( 73 | path: api_path('dashboards', 'dashboard-key'), 74 | method: :delete, 75 | status: 200, 76 | body: mock_dashboard_json, 77 | ) 78 | 79 | dashboard = @aio.delete_dashboard(key: 'dashboard-key') 80 | expect(dashboard).not_to be_empty 81 | end 82 | 83 | end 84 | 85 | -------------------------------------------------------------------------------- /spec/adafruit/io/block_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | # valid block properties 4 | 5 | RSpec.describe Adafruit::IO::Client do 6 | include_context "AdafruitIOv2" 7 | 8 | before do 9 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 10 | @aio.api_endpoint = TEST_URL 11 | end 12 | 13 | it 'returns blocks' do 14 | mock_response( 15 | path: api_path('dashboards', 'db', 'blocks'), 16 | method: :get, 17 | status: 200, 18 | body: "[#{mock_block_json}]", 19 | ) 20 | 21 | blocks = @aio.blocks(dashboard_key: 'db') 22 | expect(blocks).not_to be_empty 23 | end 24 | 25 | it 'returns a block' do 26 | mock_response( 27 | path: api_path('dashboards', 'db', 'blocks', 1), 28 | method: :get, 29 | status: 200, 30 | body: mock_block_json, 31 | ) 32 | 33 | block = @aio.block(dashboard_key: 'db', key: 1) 34 | expect(block).not_to be_empty 35 | end 36 | 37 | it 'creates a new block' do 38 | mock_response( 39 | path: api_path('dashboards', 'db', 'blocks'), 40 | method: :post, 41 | status: 200, 42 | body: mock_block_json, 43 | ) 44 | 45 | block = @aio.create_block({ 46 | dashboard_key: 'db', 47 | block: { 48 | name: "block", 49 | properties: { }, 50 | visual_type: "color", 51 | column: 0, 52 | row: 0, 53 | size_x: 0, 54 | size_y: 0, 55 | block_feeds: [{ 56 | feed_id: "1", 57 | group_id: "1" 58 | }] 59 | } 60 | }) 61 | expect(block).not_to be_empty 62 | end 63 | 64 | it 'deletes a block' do 65 | mock_response( 66 | path: api_path('dashboards', 'db', 'blocks', 1), 67 | method: :delete, 68 | status: 200, 69 | body: mock_block_json 70 | ) 71 | 72 | result = @aio.delete_block(key: 1, dashboard_key: 'db') 73 | expect(result['key']).to eq mock_block['key'] 74 | end 75 | 76 | it 'updates a block' do 77 | mock_record = mock_block 78 | updated_record = mock_record.dup 79 | updated_record['name'] = "new name" 80 | 81 | 82 | mock_response( 83 | path: api_path('dashboards', 'db', 'blocks', 1), 84 | method: :put, 85 | status: 200, 86 | with_request_body: true, 87 | body: JSON.generate(updated_record) 88 | ) 89 | 90 | result = @aio.update_block(key: 1, dashboard_key: 'db') 91 | expect(result['key']).to eq updated_record['key'] 92 | expect(result['name']).to eq updated_record['name'] 93 | end 94 | 95 | end 96 | 97 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/permissions.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Permissions 5 | VALID_TYPES = %(feed group dashboard) 6 | 7 | # Get all permissions for a resource. 8 | # 9 | # client.permissions(TYPE, KEY) 10 | def permissions(*args) 11 | username, arguments = extract_username(args) 12 | 13 | assert_argument_size(arguments, 2) 14 | assert_resource_type(arguments[0]) 15 | 16 | get api_url(username, pluralize_type(arguments[0]), arguments[1], 'acl') 17 | end 18 | 19 | def permission(*args) 20 | username, arguments = extract_username(args) 21 | 22 | assert_argument_size(arguments, 3) 23 | assert_resource_type(arguments[0]) 24 | 25 | get api_url(username, pluralize_type(arguments[0]), arguments[1], 'acl', arguments[2]) 26 | end 27 | 28 | def create_permission(*args) 29 | username, arguments = extract_username(args) 30 | 31 | assert_argument_size(arguments, 2) 32 | assert_resource_type(arguments[0]) 33 | 34 | permission_attrs = arguments.pop 35 | post api_url(username, pluralize_type(arguments[0]), arguments[1], 'acl'), permission_attrs 36 | end 37 | 38 | def delete_permission(*args) 39 | username, arguments = extract_username(args) 40 | 41 | assert_argument_size(arguments, 3) 42 | assert_resource_type(arguments[0]) 43 | 44 | delete api_url(username, pluralize_type(arguments[0]), arguments[1], 'acl', arguments[2]) 45 | end 46 | 47 | private 48 | 49 | def assert_resource_type(resource_type) 50 | if !VALID_TYPES.include?(resource_type) 51 | raise Adafruit::IO::Arguments::ArgumentError.new('permission resource type must be one of: feed, group, or dashboard') 52 | end 53 | end 54 | 55 | def assert_argument_size(arguments, size) 56 | if size === 3 57 | if arguments.size < 3 58 | raise Adafruit::IO::Arguments::ArgumentError.new('permission requires resource type (string), key (string), and permission id (integer) values. valid types are feeds, groups, or dashboards') 59 | end 60 | elsif size == 2 61 | if arguments.size < size 62 | raise Adafruit::IO::Arguments::ArgumentError.new('permissions requires resource type (string) and resource key (string) values. valid types are feed, group, or dashboard') 63 | end 64 | end 65 | end 66 | 67 | def pluralize_type(resource_type) 68 | "#{resource_type}s" 69 | end 70 | end 71 | end 72 | end 73 | end 74 | 75 | 76 | -------------------------------------------------------------------------------- /spec/support/shared_contexts.rb: -------------------------------------------------------------------------------- 1 | 2 | TEST_URL = ENV['ADAFRUIT_IO_URL'] || 'http://io.adafruit.test' 3 | 4 | shared_context "AdafruitIOv1" do 5 | before { @api_version = 'v1'} 6 | 7 | def mock_response(options={}) 8 | request = stub_request(options[:method], URI::join(TEST_URL, "/#{ options[:path] }")) 9 | 10 | headers = { 11 | 'Accept'=>'application/json', 12 | 'Accept-Encoding'=>/.*/, 13 | 'User-Agent'=> %r[AdafruitIO-Ruby/#{ Adafruit::IO::VERSION } \(.+\)], 14 | 'X-Aio-Key'=>'blah' 15 | } 16 | 17 | if options[:with_request_body] 18 | headers['Content-Type'] = 'application/json' 19 | request.with(:body => /.*?/) 20 | end 21 | 22 | request.with(:headers => headers) 23 | 24 | request.to_return(:status => options[:status], 25 | :body => options[:body], 26 | :headers => {}) 27 | end 28 | 29 | def mock_feed_json 30 | fixture_json('feed') 31 | end 32 | 33 | def mock_data_json 34 | fixture_json('data') 35 | end 36 | 37 | def mock_feed_record 38 | JSON.parse(mock_feed_json) 39 | end 40 | end 41 | 42 | shared_context "AdafruitIOv2" do 43 | before { @api_version = 'v2'} 44 | 45 | def mock_response(options={}) 46 | request = stub_request(options[:method], URI::join(TEST_URL, "/#{ options[:path] }")) 47 | 48 | headers = { 49 | 'Accept'=>'application/json', 50 | 'Accept-Encoding'=>/.*/, 51 | 'User-Agent'=> %r[AdafruitIO-Ruby/#{ Adafruit::IO::VERSION } \(.+\)], 52 | 'X-Aio-Key'=>'blah' 53 | } 54 | 55 | if options[:with_request_body] 56 | headers['Content-Type'] = 'application/json' 57 | request.with(:body => /.*?/) 58 | end 59 | 60 | request.with(:headers => headers) 61 | 62 | # pagination 63 | if options[:method] =~ /get/i 64 | response_headers = { 65 | 'X-Pagination-Count' => 1, 66 | 'X-Pagination-Limit' => 1000, 67 | 'X-Pagination-Start' => '2017-04-10T00:00:00Z', 68 | 'X-Pagination-End' => '2017-04-12T00:00:00Z' 69 | } 70 | else 71 | response_headers = {} 72 | end 73 | 74 | request.to_return(:status => options[:status], 75 | :body => options[:body], 76 | :headers => response_headers) 77 | end 78 | 79 | %w(feed data feed_details token user group activities permission 80 | create_trigger trigger layouts create_dashboard dashboard block).each do |k| 81 | define_method(:"mock_#{k}_json") do 82 | fixture_json(k) 83 | end 84 | 85 | define_method(:"mock_#{k}") do 86 | JSON.parse self.send(:"mock_#{k}_json") 87 | end 88 | end 89 | 90 | def api_path(*args) 91 | options = nil 92 | if args.last.is_a?(Hash) 93 | options = args.pop 94 | end 95 | 96 | username = @aio.username 97 | if options && options.has_key?(:username) 98 | username = options[:username] 99 | end 100 | 101 | File.join(*["api", "v2", username].concat(args.map(&:to_s)).compact) 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/adafruit/io/arguments.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | module Arguments 4 | class ArgumentError < StandardError; end 5 | 6 | # Allows us to give a username during client initialization or with a specific method. 7 | def extract_username(args) 8 | username = @username 9 | 10 | if args.last.is_a?(Hash) && args.last.has_key?(:username) 11 | username = args.last.delete(:username) 12 | end 13 | 14 | if username.nil? 15 | raise "cannot make request when username is nil" 16 | end 17 | 18 | [ username, args ] 19 | end 20 | 21 | # Allow users to pass a hash with key named 'key' or :key, or just a 22 | # plain string to use as a key. 23 | # 24 | # client.feed({ key: 'myfeed' }) 25 | # client.feed({ 'key' => 'myfeed' }) 26 | # client.feed('myfeed') 27 | # 28 | # Are all equivalent. 29 | def get_key_from_arguments(arguments) 30 | record_or_key = arguments.shift 31 | return nil if record_or_key.nil? 32 | 33 | if record_or_key.is_a?(String) 34 | record_or_key 35 | elsif record_or_key.is_a?(Hash) && (record_or_key.has_key?('key') || record_or_key.has_key?(:key)) 36 | record_or_key['key'] || record_or_key[:key] 37 | elsif record_or_key.respond_to?(:key) 38 | record_or_key.key 39 | else 40 | raise 'unrecognized object or key value in arguments' 41 | end 42 | end 43 | 44 | # Same as get_key_from_arguments but looking for id 45 | def get_id_from_arguments(arguments) 46 | record_or_id = arguments.shift 47 | return nil if record_or_id.nil? 48 | 49 | if record_or_id.is_a?(String) || record_or_id.is_a?(Integer) 50 | record_or_id 51 | elsif record_or_id.is_a?(Hash) && (record_or_id.has_key?('id') || record_or_id.has_key?(:id)) 52 | record_or_id['id'] || record_or_id[:id] 53 | elsif record_or_id.respond_to?(:id) 54 | record_or_id.id 55 | else 56 | raise 'unrecognized object or id value in arguments' 57 | end 58 | end 59 | 60 | # Get a filtered list of parameters from the next argument 61 | def get_query_from_arguments(arguments, params) 62 | query = {} 63 | options = arguments.shift 64 | return query if options.nil? 65 | 66 | params.each do |param| 67 | query[param] = options[param.to_sym] if options.has_key?(param.to_sym) 68 | end 69 | query 70 | end 71 | 72 | def require_argument(arguments, argument_name) 73 | arg = arguments.first 74 | if !arg.is_a?(Hash) 75 | raise ArgumentError.new("This request requires arguments to be a Hash containing the key: :#{argument_name}") 76 | end 77 | 78 | if !(arg.has_key?(argument_name) || arg.has_key?(argument_name.to_s)) 79 | raise ArgumentError.new("Missing required parameter, make sure to include #{argument_name}") 80 | end 81 | 82 | arg[argument_name] || arg[argument_name.to_s] 83 | end 84 | 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/adafruit/io/group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RSpec.describe Adafruit::IO::Client do 4 | include_context "AdafruitIOv2" 5 | 6 | before do 7 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 8 | @aio.api_endpoint = TEST_URL 9 | end 10 | 11 | it 'returns groups' do 12 | mock_response( 13 | path: api_path('groups'), 14 | method: :get, 15 | status: 200, 16 | body: "[#{mock_group_json}]", 17 | ) 18 | 19 | groups = @aio.groups 20 | expect(groups).not_to be_empty 21 | end 22 | 23 | it 'returns a group' do 24 | mock_response( 25 | path: api_path('groups', 'group-key'), 26 | method: :get, 27 | status: 200, 28 | body: mock_group_json, 29 | ) 30 | 31 | groups = @aio.group('group-key') 32 | expect(groups).not_to be_empty 33 | end 34 | 35 | it 'creates a group' do 36 | mock_response( 37 | path: api_path('groups'), 38 | method: :post, 39 | status: 200, 40 | with_request_body: true, 41 | body: mock_group_json, 42 | ) 43 | 44 | group = @aio.create_group(key: 'group-key', name: 'Group Name', description: "group description") 45 | expect(group).not_to be_empty 46 | end 47 | 48 | it 'updates a group' do 49 | mock_response( 50 | path: api_path('groups', 'group-key'), 51 | method: :put, 52 | status: 200, 53 | body: mock_group_json, 54 | ) 55 | 56 | group = @aio.update_group(key: 'group-key', name: 'Group Other Name') 57 | expect(group).not_to be_empty 58 | end 59 | 60 | it 'deletes a group' do 61 | mock_response( 62 | path: api_path('groups', 'group-key'), 63 | method: :delete, 64 | status: 200, 65 | body: mock_group_json 66 | ) 67 | 68 | result = @aio.delete_group(key: 'group-key') 69 | expect(result).to_not be_empty 70 | end 71 | 72 | it 'adds a feed to a group' do 73 | mock_response( 74 | path: api_path('groups', 'group-key', 'add'), 75 | method: :post, 76 | status: 200, 77 | with_request_body: true, 78 | body: mock_group_json, 79 | ) 80 | 81 | result = @aio.group_add_feed('group-key', 'other.feed-key') 82 | expect(result).to_not be_empty 83 | end 84 | 85 | it 'removes a feed from a group' do 86 | mock_response( 87 | path: api_path('groups', 'group-key', 'remove'), 88 | method: :post, 89 | status: 200, 90 | with_request_body: true, 91 | body: mock_group_json, 92 | ) 93 | 94 | result = @aio.group_remove_feed('group-key', 'feed-key') 95 | expect(result).to_not be_empty 96 | end 97 | 98 | it 'raises on failure to remove a feed from a group' do 99 | mock_response( 100 | path: api_path('groups', 'non-available-key', 'remove'), 101 | method: :post, 102 | status: 404, 103 | with_request_body: true, 104 | body: '{"error":"not found - API documentation can be found at https://io.adafruit.com/api/docs"}', 105 | ) 106 | 107 | expect { 108 | @aio.group_remove_feed('non-available-key', 'feed-key') 109 | }.to raise_error(Adafruit::IO::RequestError) 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /spec/fixtures/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string", 3 | "description": "string", 4 | "key": "string", 5 | "blocks": [ 6 | { 7 | "name": "string", 8 | "description": "string", 9 | "key": "string", 10 | "visual_type": "string", 11 | "column": 0, 12 | "row": 0, 13 | "size_x": 0, 14 | "size_y": 0, 15 | "block_feeds": [ 16 | { 17 | "id": "string", 18 | "feed": { 19 | "id": 0, 20 | "name": "string", 21 | "key": "string", 22 | "group": {}, 23 | "groups": [ 24 | { 25 | "id": 0, 26 | "name": "string", 27 | "description": "string", 28 | "feeds": [ 29 | {} 30 | ], 31 | "created_at": "string", 32 | "updated_at": "string" 33 | } 34 | ], 35 | "description": "string", 36 | "details": { 37 | "shared_with": [ 38 | {} 39 | ], 40 | "data": { 41 | "first": {}, 42 | "last": {}, 43 | "count": 0 44 | } 45 | }, 46 | "unit_type": "string", 47 | "unit_symbol": "string", 48 | "history": true, 49 | "visibility": "private", 50 | "license": "string", 51 | "enabled": true, 52 | "last_value": "string", 53 | "status": "string", 54 | "status_notify": true, 55 | "status_timeout": 0, 56 | "created_at": "string", 57 | "updated_at": "string" 58 | }, 59 | "group": { 60 | "id": 0, 61 | "name": "string", 62 | "description": "string", 63 | "feeds": [ 64 | { 65 | "id": 0, 66 | "name": "string", 67 | "key": "string", 68 | "group": {}, 69 | "groups": [ 70 | {} 71 | ], 72 | "description": "string", 73 | "details": { 74 | "shared_with": [ 75 | {} 76 | ], 77 | "data": { 78 | "first": {}, 79 | "last": {}, 80 | "count": 0 81 | } 82 | }, 83 | "unit_type": "string", 84 | "unit_symbol": "string", 85 | "history": true, 86 | "visibility": "private", 87 | "license": "string", 88 | "enabled": true, 89 | "last_value": "string", 90 | "status": "string", 91 | "status_notify": true, 92 | "status_timeout": 0, 93 | "created_at": "string", 94 | "updated_at": "string" 95 | } 96 | ], 97 | "created_at": "string", 98 | "updated_at": "string" 99 | } 100 | } 101 | ] 102 | } 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/blocks.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Blocks 5 | 6 | # Get all blocks for a dashboard. Requires dashboard_key. 7 | def blocks(*args) 8 | username, arguments = extract_username(args) 9 | dashboard_key = require_argument(arguments, :dashboard_key) 10 | 11 | get api_url(username, 'dashboards', dashboard_key, 'blocks') 12 | end 13 | 14 | # Get a block specified by ID 15 | def block(*args) 16 | username, arguments = extract_username(args) 17 | dashboard_key = require_argument(arguments, :dashboard_key) 18 | block_id = get_key_from_arguments(arguments) 19 | 20 | get api_url(username, 'dashboards', dashboard_key, 'blocks', block_id) 21 | end 22 | 23 | # Create a block 24 | def create_block(*args) 25 | username, arguments = extract_username(args) 26 | dashboard_key = require_argument(arguments, :dashboard_key) 27 | block_attrs = valid_block_attrs(arguments) 28 | 29 | post api_url(username, 'dashboards', dashboard_key, 'blocks'), block_attrs 30 | end 31 | 32 | # Delete a block 33 | def delete_block(*args) 34 | username, arguments = extract_username(args) 35 | dashboard_key = require_argument(arguments, :dashboard_key) 36 | block_id = get_key_from_arguments(arguments) 37 | 38 | delete api_url(username, 'dashboards', dashboard_key, 'blocks', block_id) 39 | end 40 | 41 | # Update a block 42 | def update_block(*args) 43 | username, arguments = extract_username(args) 44 | dashboard_key = require_argument(arguments, :dashboard_key) 45 | block_id = get_key_from_arguments(arguments) 46 | block_attrs = valid_block_attrs(arguments) 47 | 48 | put api_url(username, 'dashboards', dashboard_key, 'blocks', block_id), block_attrs 49 | end 50 | 51 | # The list of every valid property name that can be stored by Adafruit IO. 52 | # 53 | # Not every property applies to every block. Boolean values should be 54 | # stored as the string "yes" or "no". 55 | def valid_block_properties 56 | %w( 57 | text value release fontSize onText offText backgroundColor onColor 58 | offColor min max step label orientation handle minValue maxValue 59 | ringWidth label xAxisLabel yAxisLabel yAxisMin yAxisMax tile 60 | fontColor showName showTimestamp errors historyHours showNumbers 61 | showLocation 62 | ) 63 | end 64 | 65 | # The list of every valid block type that can be presented by Adafruit IO. 66 | def valid_block_visual_types 67 | %w( 68 | toggle_button slider momentary_button gauge line_chart text 69 | color_picker map stream image remote_control 70 | ) 71 | end 72 | 73 | private 74 | 75 | def valid_block_attrs(arguments) 76 | get_query_from_arguments( 77 | arguments, 78 | %w(name properties visual_type column row size_x size_y block_feeds) 79 | ) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | 86 | -------------------------------------------------------------------------------- /lib/adafruit/io/client.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | require 'faraday' 4 | 5 | require 'adafruit/io/arguments' 6 | require 'adafruit/io/configurable' 7 | require 'adafruit/io/request_handler' 8 | 9 | require 'adafruit/io/client/activities' 10 | require 'adafruit/io/client/blocks' 11 | require 'adafruit/io/client/dashboards' 12 | require 'adafruit/io/client/data' 13 | require 'adafruit/io/client/feeds' 14 | require 'adafruit/io/client/groups' 15 | require 'adafruit/io/client/permissions' 16 | require 'adafruit/io/client/tokens' 17 | require 'adafruit/io/client/triggers' 18 | require 'adafruit/io/client/user' 19 | 20 | module Adafruit 21 | module IO 22 | class Client 23 | 24 | include Adafruit::IO::Arguments 25 | include Adafruit::IO::Configurable 26 | include Adafruit::IO::RequestHandler 27 | 28 | def initialize(options) 29 | @key = options[:key] 30 | @username = options[:username] 31 | 32 | @debug = !!options[:debug] 33 | end 34 | 35 | # Text representation of the client, masking key 36 | # 37 | # @return [String] 38 | def inspect 39 | inspected = super 40 | inspected = inspected.gsub! @key, "#{@key[0..3]}#{'*' * (@key.size - 3)}" if @key 41 | inspected 42 | end 43 | 44 | def last_response 45 | @last_response 46 | end 47 | 48 | include Adafruit::IO::Client::Activities 49 | include Adafruit::IO::Client::Blocks 50 | include Adafruit::IO::Client::Dashboards 51 | include Adafruit::IO::Client::Data 52 | include Adafruit::IO::Client::Feeds 53 | include Adafruit::IO::Client::Groups 54 | include Adafruit::IO::Client::Permissions 55 | include Adafruit::IO::Client::Tokens 56 | include Adafruit::IO::Client::Triggers 57 | include Adafruit::IO::Client::User 58 | 59 | private 60 | 61 | def conn 62 | if api_endpoint 63 | url = api_endpoint 64 | else 65 | url = 'https://io.adafruit.com' 66 | end 67 | 68 | Faraday.new(:url => url) do |c| 69 | c.headers['X-AIO-Key'] = @key 70 | c.headers['Accept'] = 'application/json' 71 | c.headers['User-Agent'] = "AdafruitIO-Ruby/#{VERSION} (#{RUBY_PLATFORM})" 72 | c.request :json 73 | 74 | # if @debug is true, Faraday will get really noisy when making requests 75 | if @debug 76 | c.response :logger 77 | end 78 | 79 | c.response :json 80 | end 81 | end 82 | 83 | def api_url(username, *args) 84 | safe_path_join *['api', 'v2', username].concat(args) 85 | end 86 | 87 | # safely build URL paths from segments 88 | def safe_path_join(*paths) 89 | paths = paths.compact.map(&:to_s).reject(&:empty?) 90 | last = paths.length - 1 91 | paths.each_with_index.map { |path, index| 92 | safe_path_expand(path, index, last) 93 | }.join 94 | end 95 | 96 | def safe_path_expand(path, current, last) 97 | if path[0] === '/' && current != 0 98 | path = path[1..-1] 99 | end 100 | 101 | unless path[-1] === '/' || current == last 102 | path = [path, '/'] 103 | end 104 | 105 | path 106 | end 107 | 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /examples/v2/complete_app.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'adafruit/io' 3 | 4 | def section(label) 5 | puts "-" * 80 6 | puts "- " + label + " " + ("-" * (80 - label.size - 3)) 7 | puts "-" * 80 8 | 9 | yield 10 | 11 | puts 12 | end 13 | 14 | # replace ENV['IO_KEY'] and ENV['IO_USERNAME'] with your key and username, 15 | # respectively, or add IO_KEY and IO_USERNAME to your shell environment before 16 | # you run this script 17 | # 18 | # to show all HTTP request activity add `debug: true` 19 | api = Adafruit::IO::Client.new key: ENV['IO_KEY'], username: ENV['IO_USERNAME'] 20 | 21 | 22 | section 'Client Prepared' do 23 | puts api.inspect 24 | end 25 | 26 | temperature = nil 27 | humidity = nil 28 | 29 | section 'Getting or Creating Feeds' do 30 | temperature = api.feed_details('temperature') rescue nil 31 | if temperature.nil? 32 | puts "" 33 | temperature = api.create_feed(name: 'Temperature') 34 | end 35 | 36 | humidity = api.feed_details('humidity') rescue nil 37 | if humidity.nil? 38 | puts "" 39 | humidity = api.create_feed(name: 'Humidity') 40 | end 41 | 42 | puts "%-12s%-12s%s" % %w(Name Key Created_At) 43 | puts 44 | puts "%-12s%-12s%s" % [ temperature['name'], temperature['key'], temperature['created_at'] ] 45 | puts "%-12s%-12s%s" % [ humidity['name'], humidity['key'], humidity['created_at'] ] 46 | end 47 | 48 | # simple random value in a range 49 | def rand_in(min, max) 50 | (rand() * (max - min)) + min 51 | end 52 | 53 | # random location in valid format 54 | def rand_location 55 | {lat: rand_in(40, 50), lon: rand_in(40, 80) * -1, ele: rand_in(200, 400)} 56 | end 57 | 58 | new_temp = nil 59 | new_humid = nil 60 | 61 | section 'Adding Data' do 62 | # 63 | # Add Data 64 | # 65 | # api.send_data feed_key, value, [ location ] 66 | # 67 | loc = rand_location() 68 | new_temp = api.send_data(temperature['key'], rand_in(20, 40), loc) 69 | puts "new temperature value: " 70 | puts JSON.pretty_generate(new_temp) 71 | 72 | new_humid = api.send_data(humidity['key'], rand_in(40, 90), loc) 73 | puts "new humidity value: " 74 | puts JSON.pretty_generate(new_humid) 75 | end 76 | 77 | # 78 | # Reporting on Data 79 | # 80 | # With /data API, get all data points with pagination 81 | section 'Get All Temperatures' do 82 | all_temperatures = api.data(temperature['key']) 83 | puts "%-32s%-16s%-30s" % ['ID', 'Temperature', 'Timestamp'] 84 | all_temperatures.each do |temp| 85 | puts "%-32s%-16.3f%-30s" % [temp['id'], temp['value'].to_f, temp['created_at']] 86 | end 87 | end 88 | 89 | # With /chart API, get graphable data points with timestamps and (possible) aggregation 90 | # 91 | # Chart data is returned as a JSON record with these keys and nested objects: 92 | # 93 | # { 94 | # feed: { id, name, key }, 95 | # parameters: { start_time, end_time, hours, [ resolution ] }, 96 | # columns: [ date, value ], 97 | # data: [ 98 | # [$date, $value], 99 | # ... 100 | # ] 101 | # } 102 | # 103 | section 'Get Humidity Chart' do 104 | humidity_chart = api.data_chart(humidity['key'], hours: 4) 105 | puts "%-28s%8s" % ['Time', 'Humidity'] 106 | humidity_chart['data'].each do |point| 107 | puts "%-28s%8.2f %s" % [point[0], point[1], ('-' * point[1].to_i)] 108 | end 109 | end 110 | 111 | 112 | section 'Working with data as a stream' do 113 | point = nil 114 | 115 | 5.times do 116 | begin 117 | point = api.next_data(temperature['key']) 118 | rescue => ex 119 | if ex.response.status === 404 120 | break 121 | else 122 | raise ex 123 | end 124 | end 125 | 126 | puts "POINT #{point.inspect}" 127 | # puts "TEMPERATURE %s %0.3f" % [ point['id'], point['value'].to_f ] 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/adafruit/io/request_handler.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | 3 | module Adafruit 4 | module IO 5 | class RequestError < StandardError 6 | attr_reader :response 7 | 8 | def initialize(message, response) 9 | super(message) 10 | @response = response 11 | end 12 | end 13 | 14 | module RequestHandler 15 | 16 | attr_reader :last_response, :pagination 17 | 18 | def request(method, url, data = nil, options = nil) 19 | @last_response = send(method, url, data) 20 | end 21 | 22 | def get(url, options = {}) 23 | request(:handle_get, url, options) 24 | end 25 | 26 | def post(url, data, options = {}) 27 | request :handle_post, url, data, options 28 | end 29 | 30 | def put(url, data, options = {}) 31 | request :handle_put, url, data, options 32 | end 33 | 34 | def delete(url, options = {}) 35 | request :handle_delete, url 36 | end 37 | 38 | def last_page? 39 | pagination.nil? || (pagination['limit'] != pagination['count']) 40 | end 41 | 42 | private 43 | 44 | def update_pagination(response) 45 | @pagination = nil 46 | 47 | if pagination_keys = response.headers.keys.grep(/x-pagination-/i) 48 | @pagination = {} 49 | pagination_keys.each do |k| 50 | @pagination[k.gsub(/x-pagination-/i, '')] = response.headers[k] 51 | end 52 | end 53 | end 54 | 55 | def parsed_response(response) 56 | if response.respond_to?(:data) 57 | response.data 58 | else 59 | begin 60 | json = JSON.parse(response.body) 61 | rescue => ex 62 | if @debug 63 | puts "ERROR PARSING JSON: #{ex.message}" 64 | puts ex.backtrace[0..5].join("\n") 65 | end 66 | response.body 67 | end 68 | end 69 | end 70 | 71 | def handle_get(url, options = {}) 72 | response = conn.get do |req| 73 | req.url url 74 | options.each do |k,v| 75 | req.params[k] = v 76 | end 77 | end 78 | 79 | update_pagination(response) 80 | 81 | if response.status < 200 || response.status > 299 82 | raise Adafruit::IO::RequestError.new("GET error: #{ response.body }", response) 83 | else 84 | parsed_response response 85 | end 86 | end 87 | 88 | def handle_post(url, data, options = {}) 89 | response = conn.post do |req| 90 | req.url url 91 | req.body = data 92 | end 93 | 94 | if response.status < 200 || response.status > 299 95 | raise Adafruit::IO::RequestError.new("POST error: #{ response.body }", response) 96 | else 97 | parsed_response response 98 | end 99 | end 100 | 101 | def handle_put(url, data, options = {}) 102 | response = conn.put do |req| 103 | req.url url 104 | req.body = data 105 | end 106 | 107 | if response.status < 200 || response.status > 299 108 | raise Adafruit::IO::RequestError.new("PUT error: #{ response.body }", response) 109 | else 110 | parsed_response response 111 | end 112 | end 113 | 114 | def handle_delete(url, options = {}) 115 | response = conn.delete do |req| 116 | req.url url 117 | end 118 | 119 | if response.status < 200 || response.status > 299 120 | if response.status === 404 121 | nil 122 | else 123 | raise Adafruit::IO::RequestError.new("DELETE error: #{ response.body }", response) 124 | end 125 | else 126 | parsed_response response 127 | end 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/adafruit/io/client/data.rb: -------------------------------------------------------------------------------- 1 | module Adafruit 2 | module IO 3 | class Client 4 | module Data 5 | 6 | # Add data to a feed. 7 | # 8 | # Params: 9 | # - feed_key: string 10 | # - value: string or number 11 | # - location (optional): {lat: Number, lon: Number, ele: Number} 12 | # - created_at (optional): iso8601 date time string. 13 | # 14 | def send_data(*args) 15 | username, arguments = extract_username(args) 16 | feed_key = get_key_from_arguments(arguments) 17 | value = arguments.shift 18 | 19 | attrs = { 20 | value: value 21 | } 22 | 23 | if arguments.size > 0 && arguments.first.is_a?(Hash) 24 | location = arguments.shift 25 | if location[:lat] && location[:lon] 26 | attrs[:lat] = location[:lat] 27 | attrs[:lon] = location[:lon] 28 | attrs[:ele] = location[:ele] 29 | end 30 | end 31 | 32 | if arguments.size > 0 33 | if arguments.first.is_a?(String) 34 | created_at = arguments.shift 35 | attrs[:created_at] = created_at 36 | elsif arguments.first.is_a?(Time) 37 | created_at = arguments.shift 38 | attrs[:created_at] = created_at.utc.iso8601 39 | end 40 | end 41 | 42 | post api_url(username, 'feeds', feed_key, 'data'), attrs 43 | end 44 | 45 | # Send a batch of data points. 46 | # 47 | # Points can either be a list of strings, numbers, or hashes with the 48 | # keys [ value, created_at OPTIONAL, lat OPTIONAL, lon OPTIONAL, ele OPTIONAL ] 49 | # 50 | def send_batch_data(*args) 51 | username, arguments = extract_username(args) 52 | feed_key = get_key_from_arguments(arguments) 53 | values = arguments.shift 54 | 55 | if values.nil? || !values.is_a?(Array) 56 | raise "expected batch values to be an array" 57 | end 58 | 59 | if !values.first.is_a?(Hash) 60 | # convert values to hashes with single key: 'value' 61 | values = values.map {|val| {value: val}} 62 | end 63 | 64 | post api_url(username, 'feeds', feed_key, 'data', 'batch'), data: values 65 | end 66 | 67 | # Get all data for a feed, may paginate. 68 | def data(*args) 69 | username, arguments = extract_username(args) 70 | feed_key = get_key_from_arguments(arguments) 71 | query = get_query_from_arguments arguments, %w(start_time end_time limit) 72 | 73 | get api_url(username, 'feeds', feed_key, 'data'), query 74 | end 75 | 76 | # Get charting data for a feed. 77 | def data_chart(*args) 78 | username, arguments = extract_username(args) 79 | feed_key = get_key_from_arguments(arguments) 80 | query = get_query_from_arguments arguments, %w(start_time end_time limit hours resolution) 81 | 82 | get api_url(username, 'feeds', feed_key, 'data', 'chart'), query 83 | end 84 | 85 | # Get a single data point. 86 | def datum(*args) 87 | username, arguments = extract_username(args) 88 | feed_key = get_key_from_arguments(arguments) 89 | data_id = arguments.shift 90 | 91 | get api_url(username, 'feeds', feed_key, 'data', data_id) 92 | end 93 | 94 | # Retrieve the next unprocessed data point. 95 | def next_data(*args) 96 | username, arguments = extract_username(args) 97 | feed_key = get_key_from_arguments(arguments) 98 | 99 | get api_url(username, 'feeds', feed_key, 'data', 'next') 100 | end 101 | 102 | # Retrieve the previously processed data point. This method does not 103 | # move the stream pointer. 104 | def prev_data(*args) 105 | username, arguments = extract_username(args) 106 | feed_key = get_key_from_arguments(arguments) 107 | 108 | get api_url(username, 'feeds', feed_key, 'data', 'prev') 109 | end 110 | 111 | # Move the stream processing pointer to and retrieve the last (newest) 112 | # data point available. 113 | def last_data(*args) 114 | username, arguments = extract_username(args) 115 | feed_key = get_key_from_arguments(arguments) 116 | 117 | get api_url(username, 'feeds', feed_key, 'data', 'last') 118 | end 119 | 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/adafruit/io/feed_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | # List of current properties associated with a feed. 4 | FEED_PROPS_v2_0_0 = %w( 5 | created_at description group groups history 6 | id key last_value license name 7 | owner status_notify status_timeout unit_symbol 8 | unit_type updated_at username 9 | visibility 10 | ) 11 | 12 | RSpec.describe Adafruit::IO::Client do 13 | include_context "AdafruitIOv2" 14 | 15 | before do 16 | @aio = Adafruit::IO::Client.new key: MY_KEY, username: 'test_username' 17 | @aio.api_endpoint = TEST_URL 18 | end 19 | 20 | context 'starting with no feeds' do 21 | context '#feed with string key argument' do 22 | it 'raises request error' do 23 | mock_response( 24 | path: api_path('feeds', FEED_KEY1), 25 | method: :get, 26 | status: 404, 27 | body: fixture_json('not_found_error'), 28 | ) 29 | 30 | expect { @aio.feed FEED_KEY1 }.to raise_error(Adafruit::IO::RequestError) 31 | end 32 | end 33 | 34 | context '#create' do 35 | it 'returns a new feed with the expected properties' do 36 | mock_response( 37 | path: api_path('feeds'), 38 | method: :post, 39 | status: 200, 40 | body: mock_feed_json, 41 | ) 42 | 43 | feed = @aio.create_feed(name: mock_feed['name']) 44 | 45 | expect(feed.keys).not_to include('error') 46 | expect(feed['name']).to eq mock_feed['name'] 47 | expect(feed.keys.sort).to eq FEED_PROPS_v2_0_0.sort 48 | end 49 | end 50 | end 51 | 52 | context 'with a newly created feed,' do 53 | context '#feeds with no args' do 54 | it 'returns the newly created feed' do 55 | mock_response( 56 | path: api_path('feeds'), 57 | method: :get, 58 | status: 200, 59 | body: JSON.generate([ mock_feed ]), 60 | ) 61 | 62 | feeds = @aio.feeds 63 | 64 | feed = feeds.find { |f| f['name'] == mock_feed['name'] } 65 | 66 | expect(feed).not_to be_nil 67 | expect(feed['name']).to eq mock_feed['name'] 68 | end 69 | end 70 | 71 | context '#feed with one arg' do 72 | before do 73 | mock_response( 74 | path: api_path('feeds', mock_feed['key']), 75 | method: :get, 76 | status: 200, 77 | body: mock_feed_json, 78 | ) 79 | end 80 | 81 | it 'returns that feed when given string key' do 82 | feed = @aio.feed(mock_feed['key']) 83 | expect(feed['name']).to eq mock_feed['name'] 84 | end 85 | 86 | it 'returns that feed when given string-key hash' do 87 | feed = @aio.feed({'key' => mock_feed['key']}) 88 | expect(feed['name']).to eq mock_feed['name'] 89 | end 90 | 91 | it 'returns that feed when given symbol-key hash' do 92 | feed = @aio.feed(key: mock_feed['key']) 93 | expect(feed['name']).to eq mock_feed['name'] 94 | end 95 | end 96 | 97 | describe '#update_feed' do 98 | before(:example) do 99 | @mock_record = mock_feed 100 | 101 | @updated_record = @mock_record.dup 102 | @updated_record['description'] = FEED_DESC 103 | end 104 | 105 | context 'updating the description' do 106 | it 'returns the updated feed' do 107 | mock_response( 108 | path: api_path('feeds', @mock_record['key']), 109 | method: :put, 110 | status: 200, 111 | with_request_body: true, 112 | body: JSON.generate(@updated_record), 113 | ) 114 | 115 | feed = @aio.update_feed(@mock_record['key'], description: @updated_record['description']) 116 | 117 | expect(feed['id']).to eq @mock_record['id'] 118 | expect(feed['description']).to eq FEED_DESC 119 | end 120 | end 121 | end 122 | 123 | context '#delete' do 124 | 125 | it 'returns deleted feed' do 126 | mock_response( 127 | path: api_path('feeds', mock_feed['key']), 128 | method: :delete, 129 | status: 200, 130 | body: fixture_json('feed') 131 | ) 132 | 133 | result = @aio.delete_feed(mock_feed['key']) 134 | expect(result['id']).to eq mock_feed['id'] 135 | end 136 | 137 | it "raises not found when feed doesn't exist" do 138 | mock_response( 139 | path: api_path('feeds', mock_feed['key']), 140 | method: :delete, 141 | status: 404, 142 | body: fixture_json('not_found_error'), 143 | ) 144 | 145 | expect(@aio.delete_feed(mock_feed['key'])).to be_nil 146 | end 147 | 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/adafruit/io-client-ruby/workflows/CI/badge.svg) 2 | 3 | # adafruit-io 4 | 5 | A [Ruby][1] client for use with with [io.adafruit.com][2]. 6 | 7 | Note, this documentation covers the gem supporting V2 of the API, which is currently under active development and may be missing some features. It also breaks support for code that used version <= 1.0.4 of this library. 8 | 9 | Older releases are available at these links: 10 | 11 | * [1.0.4](https://github.com/adafruit/io-client-ruby/tree/v1.0.4) 12 | * [1.0.3](https://github.com/adafruit/io-client-ruby/tree/v1.0.3) 13 | * [1.0.0](https://github.com/adafruit/io-client-ruby/tree/v1.0.0) 14 | 15 | This is a near complete rewrite and strip-down of the library intended to support V2 of the Adafruit IO API with less code, maintenance, and stress. 16 | 17 | Why rewrite? This lets us the replace the existing, custom ActiveRecord-based interface with a flat, stateless API client returning plain hashes based on the JSON returned from API. Instead of writing a bunch of Ruby to make it feel like we're in a Rails app, we're just providing hooks into the API and a thin wrapper around Faraday. 18 | 19 | The API is not very complex, code that uses it shouldn't be either. 20 | 21 | 22 | ## Roadmap 23 | 24 | It is our goal to eventually support all API V2 methods, but that will happen in stages. 25 | 26 | - [x] Feeds `2.0.0.beta.1` 27 | - [x] Data `2.0.0.beta.1` 28 | - [x] Groups `2.0.0.beta.1` 29 | - [x] MQTT `2.0.0.beta.3` 30 | - [x] Tokens `2.0.0.beta.4` 31 | - [x] Blocks `2.0.0.beta.4` 32 | - [x] Dashboards `2.0.0.beta.4` 33 | - [x] Activities `2.0.0.beta.5` 34 | - [x] Permissions `2.0.0.beta.5` 35 | - [x] Triggers `2.0.0.beta.6` 36 | 37 | Still needing complete tests: 38 | 39 | - [ ] MQTT 40 | 41 | 42 | ## Installation 43 | 44 | Add this line to your application's Gemfile: 45 | 46 | gem 'adafruit-io' 47 | 48 | And then execute: 49 | 50 | $ bundle 51 | 52 | Or install it yourself as: 53 | 54 | $ gem install adafruit-io 55 | 56 | 57 | 58 | ## Basic Usage 59 | 60 | Each time you use the library, you'll have to pass your [Adafruit IO Key][4] to the client. 61 | 62 | 63 | ```ruby 64 | require 'adafruit/io' 65 | 66 | # create an instance 67 | aio = Adafruit::IO::Client.new key: 'KEY' 68 | ``` 69 | 70 | Since every API request requires a username, you can also pass a username to the client initializer to use it for every request. 71 | 72 | 73 | ```ruby 74 | require 'adafruit/io' 75 | 76 | # create an instance 77 | aio = Adafruit::IO::Client.new key: 'KEY', username: 'USERNAME' 78 | ``` 79 | 80 | 81 | 82 | ### Environment Variables 83 | 84 | Whenever possible, we recommend you keep your Adafruit IO API credentials out of your application code by using environment variables. All the examples 85 | 86 | [Others](http://blog.honeybadger.io/ruby-guide-environment-variables/) have written about using environment variables in Ruby, so we're not going to go into detail. We recommend the [dotenv](https://github.com/bkeepers/dotenv) gem if you're building a Ruby project. 87 | 88 | 89 | 90 | ### API Response Values 91 | 92 | All return values are **plain Ruby hashes** based on the JSON response returned by the API. Most basic requests should get back a Hash with a `key` field. The key can be used in subsequent requests. API requests that return a list of objects will return a simple array of hashes. Feeds, Groups, and Dashboards all rely on the `key` value, other endpoints (Blocks, Permissions, Tokens, Triggers) use `id`. 93 | 94 | You can find the current API documentation at [https://io.adafruit.com/api/docs/](https://io.adafruit.com/api/docs/). This library implements v2 of the Adafruit IO API. 95 | 96 | 97 | 98 | ### API Error Responses 99 | 100 | As of **v2.0.0**, this library raises an `Adafruit::IO::RequestError` on any non HTTP 200 status responses. Generally, this means your code should wrap API calls in `begin...rescue...end` blocks. 101 | 102 | ```ruby 103 | require 'adafruit/io' 104 | 105 | api_key = ENV['IO_KEY'] 106 | username = ENV['IO_USER'] 107 | 108 | api = Adafruit::IO::Client.new key: api_key, username: username 109 | 110 | 111 | 112 | ``` 113 | 114 | ## Example 115 | 116 | Here's an example of creating, adding data to, and deleting a feed. 117 | 118 | 119 | ```ruby 120 | require 'adafruit/io' 121 | 122 | api_key = ENV['IO_KEY'] 123 | username = ENV['IO_USER'] 124 | 125 | api = Adafruit::IO::Client.new key: api_key, username: username 126 | 127 | # create a feed 128 | puts "create" 129 | garbage = api.create_feed(name: "Garbage 123") 130 | 131 | # add data 132 | puts "add data" 133 | api.send_data garbage, 'something' 134 | api.send_data garbage, 'goes here' 135 | 136 | # load data 137 | puts "load data" 138 | data = api.data(garbage) 139 | puts "#{data.size} points: #{ data.map {|d| d['value']}.join(', ') }" 140 | 141 | # get details 142 | puts "read" 143 | puts JSON.pretty_generate(api.feed_details(garbage)) 144 | 145 | # delete feed 146 | puts "delete" 147 | api.delete_feed(garbage) 148 | 149 | # try reading 150 | puts "read?" 151 | # ... get nothing 152 | puts api.feed(garbage['key']).inspect 153 | ``` 154 | 155 | 156 | This code and more is available in [the examples/ directory](examples/). 157 | 158 | ## License 159 | 160 | Copyright (c) 2018 Adafruit Industries. Licensed under the MIT license. 161 | 162 | [Adafruit](https://adafruit.com) invests time and resources providing this open source code. Please support Adafruit and open-source hardware by purchasing products from [Adafruit](https://adafruit.com). 163 | 164 | ## Contributing 165 | 166 | 1. Fork it ( http://github.com/adafruit/io-client-ruby/fork ) 167 | 1. Create your feature branch (`git checkout -b my-new-feature`) 168 | 1. Write tests, write code, and run the tests (`bundle exec rspec`) 169 | 1. Commit your changes (`git commit -am 'Add some feature'`) 170 | 1. Push to the branch (`git push origin my-new-feature`) 171 | 1. Create a new Pull Request 172 | 173 | If you'd like to contribute and don't know where to start, [reach out on the Adafruit IO forum](https://forums.adafruit.com/viewforum.php?f=56) or in the [adafruit-io channel on our Discord server](https://discord.gg/adafruit). 174 | 175 | [1]: https://www.ruby-lang.org 176 | [2]: https://io.adafruit.com 177 | [3]: https://learn.adafruit.com/adafruit-io/feeds 178 | [4]: https://learn.adafruit.com/adafruit-io/api-key 179 | [5]: https://learn.adafruit.com/adafruit-io/groups 180 | -------------------------------------------------------------------------------- /lib/adafruit/io/mqtt.rb: -------------------------------------------------------------------------------- 1 | require 'mqtt' 2 | 3 | # 4 | # Adafruit::IO::MQTT provides a simple Adafruit IO aware wrapper around the 5 | # Ruby MQTT library at https://github.com/njh/ruby-mqtt. 6 | # 7 | # Our primary goal is to provide basic MQTT access to feeds. 8 | # 9 | # For example, publishing to a feed is as simple as: 10 | # 11 | # mqtt = Adafruit::IO::MQTT.new user, key 12 | # mqtt.publish('feed-key', 1) 13 | # 14 | # And subscribing to a feed is just as easy: 15 | # 16 | # mqtt = Adafruit::IO::MQTT.new user, key 17 | # mqtt.subscribe('feed-key') 18 | # mqtt.get do |topic, value| 19 | # puts "GOT VALUE FROM #{topic}: #{value}" 20 | # end 21 | # 22 | # If you need to access different MQTT endpoints or data formats (JSON, CSV) 23 | # you can use the MQTT library directly: 24 | # 25 | # mqtt = Adafruit::IO::MQTT.new user, key 26 | # mqtt.client.get("#{user}/groups/group-key/json") do |topic, message| 27 | # payload = JSON.parse(message) 28 | # # etc... 29 | # end 30 | # 31 | # Documentation for Ruby MQTT is available at http://www.rubydoc.info/gems/mqtt/MQTT/Client 32 | # 33 | module Adafruit 34 | module IO 35 | class MQTT 36 | 37 | # provide access to the raw MQTT library client 38 | attr_reader :client 39 | 40 | def initialize(username, key, opts={}) 41 | @options = { 42 | uri: 'io.adafruit.com', 43 | protocol: 'mqtts', 44 | port: 8883, 45 | username: username, 46 | key: key 47 | }.merge(opts) 48 | 49 | @connect_uri = "%{protocol}://%{username}:%{key}@%{uri}" % @options 50 | 51 | connect 52 | end 53 | 54 | def connect 55 | if @client.nil? || !@client.connected? 56 | @client = ::MQTT::Client.connect @connect_uri, @options[:port], ack_timeout: 10 57 | end 58 | end 59 | 60 | def disconnect 61 | if @client && @client.connected? 62 | @client.disconnect 63 | end 64 | end 65 | 66 | # Publish value to feed with given key 67 | def publish(key, value, location={}) 68 | raise 'client is not connected' unless @client.connected? 69 | 70 | topic = key_to_feed_topic(key) 71 | location = indifferent_keys(location) 72 | payload = payload_from_value_with_location(value, location) 73 | 74 | @client.publish(topic, payload) 75 | end 76 | 77 | # Publish to multiple feeds within a group. 78 | # 79 | # - `key` is a group key 80 | # - `values` is a hash where keys are feed keys and values are the 81 | # published value. 82 | # - `location` is the optional { :lat, :lon, :ele } hash specifying the 83 | # location data for this publish event. 84 | # 85 | def publish_group(key, values, location={}) 86 | raise 'client is not connected' unless @client.connected? 87 | raise 'values must be a hash' unless values.is_a?(Hash) 88 | 89 | topic = key_to_group_topic(key, false) 90 | location = indifferent_keys(location) 91 | payload = payload_from_values_with_location(values, location) 92 | 93 | @client.publish(topic, payload) 94 | end 95 | 96 | # Subscribe to the feed with the given key. Use .get to retrieve messages 97 | # from subscribed feeds. 98 | # 99 | # Include the { last_value: true } option if you'd like the feed to 100 | # receive the last value immediately. (like MQTT retain) 101 | def subscribe(key, options={}) 102 | raise 'client is not connected' unless @client.connected? 103 | 104 | topic = key_to_feed_topic(key) 105 | @client.subscribe(topic) 106 | 107 | if options[:last_value] 108 | @client.publish(topic + '/get', '') 109 | end 110 | end 111 | 112 | def unsubscribe(key) 113 | raise 'client is not connected' unless @client.connected? 114 | 115 | topic = key_to_feed_topic(key) 116 | @client.unsubscribe(topic) 117 | end 118 | 119 | # Subscribe to a group with the given key. 120 | # 121 | # NOTE: Unlike feed subscriptions, group subscriptions return a JSON 122 | # representation of the group record with a 'feeds' property containing a 123 | # JSON object whose keys are feed keys and whose values are the last 124 | # value received for that feed. 125 | def subscribe_group(key) 126 | raise 'client is not connected' unless @client.connected? 127 | 128 | topic = key_to_group_topic(key) 129 | @client.subscribe(topic) 130 | end 131 | 132 | # Retrieve the last value received from the MQTT connection for any 133 | # subscribed feeds or groups. This is a blocking method, which means it 134 | # won't return until a message is retrieved. 135 | # 136 | # Returns [topic, message] or yields it into the given block. 137 | # 138 | # With no block: 139 | # 140 | # mqtt_client.subscribe('feed-key') 141 | # loop do 142 | # topic, message = mqtt_client.get 143 | # # do something 144 | # end 145 | # 146 | # With a block: 147 | # 148 | # mqtt_client.subscribe('feed-key') 149 | # mqtt_client.get do |topic, message| 150 | # # do something 151 | # end 152 | # 153 | def get(&block) 154 | @client.get(&block) 155 | end 156 | 157 | private 158 | 159 | def encode_json(record) 160 | begin 161 | JSON.generate record 162 | rescue JSON::GeneratorError => ex 163 | puts "failed to generate JSON from record: #{record.inspect}" 164 | raise ex 165 | end 166 | end 167 | 168 | def key_to_feed_topic(key) 169 | "%s/f/%s" % [@options[:username], key] 170 | end 171 | 172 | def key_to_group_topic(key, json=true) 173 | "%s/g/%s%s" % [@options[:username], key, (json ? '/json' : '')] 174 | end 175 | 176 | def payload_from_value_with_location(value, location) 177 | payload = { value: value.to_s } 178 | 179 | if location.has_key?('lat') && location.has_key?['lon'] 180 | %w(lat lon ele).each do |f| 181 | payload[f] = location[f] 182 | end 183 | end 184 | 185 | encode_json payload 186 | end 187 | 188 | def payload_from_values_with_location(values, location) 189 | payload = { feeds: values } 190 | 191 | if location.has_key?('lat') && location.has_key?['lon'] 192 | payload[:location] = {} 193 | 194 | %w(lat lon ele).each do |f| 195 | payload[:location][f] = location[f] 196 | end 197 | end 198 | 199 | encode_json payload 200 | end 201 | 202 | def indifferent_keys(hash) 203 | hash.keys.inject({}) {|new_hash, key| 204 | new_hash[key.to_s] = hash[key] 205 | new_hash[key.to_sym] = hash[key] 206 | 207 | new_hash 208 | } 209 | end 210 | end 211 | end 212 | end 213 | --------------------------------------------------------------------------------