├── 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 | 
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 |
--------------------------------------------------------------------------------