Ruby Client for the MyJohnDeere API

This client allows you to connect the MyJohnDeere API
without having to code your own oAuth process, API requests, and pagination.
- Works with Rails, but does not require it
- Supports both sandbox and live mode
- Simplifies the oAuth negotiation process
- Provides an ActiveRecord-style interface to many endpoints
- Provides
get
, create
, put
, and delete
methods to make easy, authenticated, direct API calls - Uses ruby enumerables to handle pagination behind the scenes. Calls like
each
, map
, etc will fetch new pages of data as needed.
Table of Contents
How To Read This Documentation
We provide RDoc documentation, but here is a helpful guide for getting started. Because the gem name is long, all examples are going
to assume this shortcut:
JD = MyJohnDeereApi
So that when you see:
JD::Authorize
It really means:
MyJohnDeereApi::Authorize
Installation
This library is available as a gem. To use it, just install the gem:
gem install my_john_deere_api
If you're using Bundler (and why wouldn't you?) then add the gem to your gemfile:
gem 'my_john_deere_api'
and run:
bundle install
Authorizing with John Deere via oAuth 1.0
This is the simplest path to authorization, though your user has to jump through an extra hoop of giving you the verification code:
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
url = authorize.authorize_url
authorize.verify(code)
In reality, you will likely need to re-instantiate the authorize object when the user returns, and that works without issue:
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
url = authorize.authorize_url
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
authorize.verify(code)
In a web app, you're prefer that your user doesn't have to copy/paste verification codes. So you can pass in an :oauth_callback url.
When the user authorizes your app with John Deere, they are redirected to the url you provide, with the paraameter 'oauth_verifier'
that contains the verification code so the user doesn't have to provide it.
authorize = JD::Authorize.new(
API_KEY,
API_SECRET,
environment: :sandbox,
oauth_callback: 'https://example.com'
)
url = authorize.authorize_url
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
authorize.verify(params[:oauth_verifier])
Interacting with the User's John Deere Account
After authorization is complete, the Client
object will provide most of the interface for this library. A client can
be used with or without user credentials, because some API calls are specific to your application's relationship
with John Deere, not your user's. But most interactions will involve user data. Here's how to instantiate a client:
client = JD::Client.new(
API_KEY,
API_SECRET,
environment: :sandbox,
contribution_definition_id: CONTRIBUTION_DEFINITION_ID,
access: [ACCESS_TOKEN, ACCESS_SECRET]
)
Using the Client to Do Stuff
Once you're connected, the client works like a simplified version of ActiveRecord. JSON hashes from the API are
converted into objects to be easier to work with. Collections of things, like organizations, handle pagination
for you. Just iterate using each
, map
, etc, and new pages are fetched as needed.
This client is a work in progress. You can currently do the following things without resorting to API calls:
client
├── contribution_products
| ├── count
| ├── all
| ├── first
| └── find(contribution_product_id)
| └── contribution_definitions
| ├── count
| ├── all
| ├── first
| └── find(contribution_definition_id)
└── organizations
├── count
├── all
├── first
└── find(organization_id)
├── assets(attributes)
| ├── create(attributes)
| ├── count
| ├── all
| ├── first
| └── find(asset_id)
| ├── save
| ├── update(attributes)
| └── locations
| ├── create(attributes)
| ├── count
| ├── all
| └── first
└── fields
├── count
├── all
├── first
└── find(field_id)
└── flags
├── count
├── all
└── first
Contribution Product collections act like a list. In addition to all the methods included via Ruby's
Enumerable Module, contribution product
collections support the following methods:
- all
- count
- first
- find(contribution_product_id)
An individual contribution product supports the following methods and associations:
- id
- market_place_name
- market_place_description
- default_locale
- current_status
- activation_callback
- preview_images
- supported_regions
- supported_operation_centers
- links
- contribution_definitions (collection of this contribution product's contribution definitions)
client.contribution_products
client.contribution_products.count
client.contribution_products.first
contribution_product = client.contribution_products.find(1234)
contribution_product.market_place_name
contribution_product.contribution_definitions
Handles a contribution product's contribution definitions. Contribution definition collections support the following methods:
- all
- count
- first
- find(contribution_definition_id)
An individual contribution definition supports the following methods and associations:
contribution_product.contribution_definitions
client.contribution_definitions.count
client.contribution_definitions.first
contribution_definition = contribution_product.contribution_definitions.find(1234)
contribution_definition.name
Handles an account's organizations. Organization collections support the following methods:
- all
- count
- first
- find(organization_id)
An individual organization supports the following methods and associations:
- id
- name
- type
- member?
- links
- assets (collection of this organization's assets)
- fields (collection of this organization's fields)
The count
method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
all
forces the entire collection to be loaded from John Deere's API, so use with caution. Organizations cannot be
created via the API, so there is no create
method on this collection.
client.organizations
client.organizations.count
client.organizations.first
organization = client.organizations.find(1234)
organization.name
organization.type
organization.member?
organization.links
organization.assets
organization.fields
Handles an organization's assets. Asset collections support the following methods:
- create(attributes)
- all
- count
- first
- find(asset_id)
An individual asset supports the following methods and associations:
- id
- title
- category
- type
- sub_type
- links
- update(attributes)
- locations (collection of this asset's locations)
organization = client.organizations.first
organization.assets
asset = organization.assets.find(123)
asset.title
asset.category
asset.type
asset.sub_type
asset.links
The create
method creates the asset in the John Deere platform, and returns the newly created record.
asset = organization.assets.create(
title: 'Asset Title',
asset_category: 'DEVICE',
asset_type: 'SENSOR',
asset_sub_type: 'ENVIRONMENTAL'
)
asset.title
The update
method updates the local object, and also the asset on the John Deere platform.
Only the title of an asset can be updated.
asset.update(title: 'New Title')
asset.title
The save
method updates John Deere with any local changes that have been made.
asset.title = 'New Title'
asset.save
Handles an asset's locations. Asset Location collections support the following methods:
- create(attributes)
- all
- count
- first
An individual location supports the following methods:
- timestamp
- geometry
- measurement_data
asset = organizations.assets.first
asset.locations
location = asset.locations.first
location.timestamp
location.geometry
location.measurement_data
The create
method creates the location in the John Deere platform, and returns the newly created
object from John Deere. However, there will be no new information since there is no unique ID
generated. The timestamp submitted (which defaults to "now") will be rounded
to the nearest second.
locaton = asset.locatons.create(
timestamp: "2019-11-11T23:00:00.123Z",
geometry: [-95.123456, 40.123456],
measurement_data: [
{
name: 'Temperature',
value: '68.0',
unit: 'F'
}
]
)
location.timestamp
location.geometry
location.measurement_data
There is no updating or deleting of a location. The newest location record always acts as the status
for the given asset, and is what appears on the map view.
Note that locations are called "Asset Locations" in John Deere, but we call the association "locations", as in
asset.locations
, for brevity.
Handles an organization's fields. Field collections support the following methods:
- all
- count
- first
- find(field_id)
An individual field supports the following methods and associations:
- id
- name
- archived?
- links
- flags (collection of this field's flags)
The count
method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
all
forces the entire collection to be loaded from John Deere's API, so use with caution. Fields can be
created via the API, but there is no create
method on this collection yet.
organization.fields
organization.fields.count
organization.fields.first
field = organization.fields.find(1234)
field.name
field.archived?
field.links
field.flags
Handles a field's flags. Flag collections support the following methods. Note, John Deere does not provide an endpoint to retrieve a specific flag by id:
An individual flag supports the following methods and associations:
- id
- notes
- geometry
- archived?
- proximity_alert_enabled?
- links
The count
method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
all
forces the entire collection to be loaded from John Deere's API, so use with caution. Flags can be
created via the API, but there is no create
method on this collection yet.
field.flags
field.flags.count
flag = field.flags.first
flag.notes
flag.geometry
field.archived?
field.proximity_alert_enabled?
field.links
Direct API Requests
While the goal of the client is to eliminate the need to make/interpret calls to the John Deere API, it's important
to be able to make calls that are not yet fully supported by the client. Or sometimes, you need to troubleshoot.
GET
GET requests require only a resource path.
client.get('/organizations')
Abbreviated sample response:
{
"links": ["..."],
"total": 1,
"values": [
{
"@type": "Organization",
"name": "ABC Farms",
"type": "customer",
"member": true,
"id": "123123",
"links": ["..."]
}
]
}
This won't provide any client goodies like pagination or validation, but it does parse the returned JSON.
POST
POST requests require a resource path, and a hash for the request body. The client will camelize the keys, and convert to JSON.
client.post(
'/organizations/123123/assets',
{
"title"=>"i like turtles",
"assetCategory"=>"DEVICE",
"assetType"=>"SENSOR",
"assetSubType"=>"ENVIRONMENTAL",
"links"=>[
{
"@type"=>"Link",
"rel"=>"contributionDefinition",
"uri"=>"https://sandboxapi.deere.com/platform/contributionDefinitions/CONTRIBUTION_DEFINITION_ID"
}
]
}
)
John Deere's standard response is a 201 HTTP status code, with the message "Created". This method returns the full Net::HTTP response.
PUT
PUT requests require a resource path, and a hash for the request body. The client will camelize the keys, and convert to JSON.
client.put(
'/assets/123123',
{
"title"=>"i REALLY like turtles",
"assetCategory"=>"DEVICE",
"assetType"=>"SENSOR",
"assetSubType"=>"ENVIRONMENTAL",
"links"=>[
{
"@type"=>"Link",
"rel"=>"contributionDefinition",
"uri"=>"https://sandboxapi.deere.com/platform/contributionDefinitions/CONTRIBUTION_DEFINITION_ID"
}
]
}
)
John Deere's standard response is a 204 HTTP status code, with the message "No Content". This method returns the full Net::HTTP response.
DELETE
DELETE requests require only a resource path.
client.delete('/assets/123123')
John Deere's standard response is a 204 HTTP status code, with the message "No Content". This method returns the full Net::HTTP response.
Errors
Custom errors help clearly identify problems when using the client:
- UnsupportedEnvironmentError is raised when you attempt to instantiate a client with an
unrecognized environment. Valid environments are
:sandbox
or :production
. - InvalidRecordError is raised when bad input has been given, in an attempt to create or update
a record on the John Deere platform.
- MissingContributionDefinitionIdError is raised when the optional contribution_definition_id
has not been set in the client, but an operation has been attempted that requires it - like
creating an asset in the John Deere platform.
- TypeMismatchError is raised when a model is instantiated, typically when a record is received
from John Deere and is being converted into a Ruby object. Model instantiation is normally handled
by request objects, but this error is helpful if you're instantiating your own models for advanced
usage.
- NotYetImplementedError is raised when you attempt to use a feature that is earmarked for future
development, but hasn't been implemented in this client yet. These are a great chance to contribute
to this gem!
How Can I Help?
Give Us a Star!
Star this gem on GitHub. It helps developers
find and choose this gem over others that may be out there. To our knowledge, there are no other
John Deere gems that are being actively maintained.
Contribute to This Gem
The easiest way to contribute is:
- Clone the repo
- Create a feature branch
- Grep for "raise NotYetImplementedError" in the lib directory
- Replace one of these exceptions with working code, following the conventions used in the rest of the app
- TEST EVERYTHING!
- Run tests.
- You may need to regenerate all VCR cassettes from scratch.
- All VCR cassettes should be pre-recorded in
vcr_setup
- Anything that is created in the JD sandbox as a result of running the tests should be removed, also in
vcr_setup
.
- When tests are passing, submit a Pull Request.