This client allows you to connect the MyJohnDeere API without having to code your own oAuth process, API requests, and pagination.
get
, create
, put
, and delete
methods to make easy, authenticated, direct API callseach
, map
, etc will fetch new pages of data as needed.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
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
This is the simplest path to authorization, though your user has to jump through an extra hoop of giving you the verification code:
# Create an authorize object, using your app's API key and secret. You can
# pass an environment (`:live` or `:sandbox`), which default to `:live`.
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
# Retrieve a valid authorization url from John Deere, where you can send
# your user for authorizing your app to the JD platform.
url = authorize.authorize_url
# Verify the code given to the user during the authorization process, and
# turn this into access credentials for your user.
authorize.verify(code)
In reality, you will likely need to re-instantiate the authorize object when the user returns, and that works without issue:
# Create an authorize object, using your app's API key and secret.
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
# Retrieve a valid authorization url from John Deere.
url = authorize.authorize_url
# Queue elevator music while your app serves other users...
# Re-create the authorize instance in a different process
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
# Proceed as normal
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.
# Create an authorize object, using your app's API key and secret.
authorize = JD::Authorize.new(
API_KEY,
API_SECRET,
environment: :sandbox,
oauth_callback: 'https://example.com'
)
# Retrieve a valid authorization url from John Deere.
# This will contain the callback url encoded into the
# query string for you.
url = authorize.authorize_url
# Queue elevator music while your app serves other users...
# Re-create the authorize instance in a different process.
# It's not necessary to re-initialize with the callback url.
authorize = JD::Authorize.new(API_KEY, API_SECRET, environment: :sandbox)
# Inside a Rails controller, you might do this:
authorize.verify(params[:oauth_verifier])
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(
# the application's API key
API_KEY,
# the application's API secret
API_SECRET,
# the chosen environment (:sandbox or :live)
environment: :sandbox,
# optional contribution_definition_id. This is needed for some requests,
# but the client can be created without it, in order to find it.
contribution_definition_id: CONTRIBUTION_DEFINITION_ID,
# the user's access credentials
access: [ACCESS_TOKEN, ACCESS_SECRET]
)
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:
An individual contribution product supports the following methods and associations:
client.contribution_products
# => collection of contribution products under this client
client.contribution_products.count
# => 1
client.contribution_products.first
# => an individual contribution product
contribution_product = client.contribution_products.find(1234)
# => an individual contribution product, fetched by ID
contribution_product.market_place_name
# => 'Market Place Name'
contribution_product.contribution_definitions
# => collection of contribution definitions belonging to this contribution product
Handles a contribution product's contribution definitions. Contribution definition collections support the following methods:
An individual contribution definition supports the following methods and associations:
contribution_product.contribution_definitions
# => collection of contribution definitions under this contribution product
client.contribution_definitions.count
# => 1
client.contribution_definitions.first
# => an individual contribution definition
contribution_definition = contribution_product.contribution_definitions.find(1234)
# => an individual contribution definition, fetched by ID
contribution_definition.name
# => 'Contribution Definition Name'
Handles an account's organizations. Organization collections support the following methods:
An individual organization supports the following methods and associations:
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
# => collection of organizations under this client
client.organizations.count
# => 15
client.organizations.first
# => an individual organization object
organization = client.organizations.find(1234)
# => an individual organization object, fetched by ID
organization.name
# => 'Smith Farms'
organization.type
# => 'customer'
organization.member?
# => true
organization.links
# => {
# 'self' => 'https://sandboxapi.deere.com/platform/organizations/1234',
# 'machines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines',
# 'wdtCapableMachines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines?capability=wdt'
# }
organization.assets
# => collection of assets belonging to this organization
organization.fields
# => collection of fields belonging to this organization
Handles an organization's assets. Asset collections support the following methods:
An individual asset supports the following methods and associations:
organization = client.organizations.first
# => the first organization returned by the client
organization.assets
# => collection of assets belonging to this organization
asset = organization.assets.find(123)
# => an individual asset object, fetched by ID
asset.title
# => 'AgThing Water Device'
asset.category
# => 'DEVICE'
asset.type
# => 'SENSOR'
asset.sub_type
# => 'OTHER'
asset.links
# => a hash of API urls related to this asset
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
# => '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
# => 'New Title', also John Deere record is updated
The save
method updates John Deere with any local changes that have been made.
asset.title = 'New Title'
asset.save
# => Successful Net::HTTPNoContent object
Handles an asset's locations. Asset Location collections support the following methods:
An individual location supports the following methods:
asset = organizations.assets.first
# => the first asset returned by the organization
asset.locations
# => collection of locations belonging to this asset
location = asset.locations.first
# => the first location returned by the asset. Note that locations do not have their own id's
# in the JD platform, and therefore cannot be requested individually via a "find" method.
location.timestamp
# => "2019-11-11T23:00:00.000Z"
# John Deere includes 3 decimal places in the format, but does not actually
# store fractions of a second, so it will always end in ".000". This is
# important, because timestamps must be unique.
location.geometry
# => a GeoJSON formatted hash, for example:
# {
# "type"=>"Feature",
# "geometry"=>{
# "geometries"=>[
# {
# "coordinates"=>[-95.123456, 40.123456],
# "type"=>"Point"
# }
# ],
# "type"=>"GeometryCollection"
# }
# }
location.measurement_data
# => the status details of this location, for example:
# [
# {
# "@type"=>"BasicMeasurement",
# "name"=>"[Soil Temperature](http://example.com/current_temperature)",
# "value"=>"21.0",
# "unit"=>"°C"
# }
# ]
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(
# You can pass fractional seconds, but they will be truncated by JD.
timestamp: "2019-11-11T23:00:00.123Z",
# JD requires more complicated JSON geometry, but this client will convert a simple
# set of lat/long coordinates into the larger format automatically.
geometry: [-95.123456, 40.123456],
# This is a list of "measurements"
measurement_data: [
{
name: 'Temperature',
value: '68.0',
unit: 'F'
}
]
)
location.timestamp
# => "2019-11-11T23:00:00.000Z"
# Note that the timestamp's fractional second is truncated by John Deere, though they
# still return the record with three digits of precision.
location.geometry
# => a GeoJSON formatted hash in its larger format
# {
# "type"=>"Feature",
# "geometry"=>{
# "geometries"=>[
# {
# "coordinates"=>[-95.123456, 40.123456],
# "type"=>"Point"
# }
# ],
# "type"=>"GeometryCollection"
# }
# }
location.measurement_data
# [
# {
# "@type"=>"BasicMeasurement",
# "name"=>"Temperature",
# "value"=>"68.0",
# "unit"=>"F"
# }
# ]
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:
An individual field supports the following methods and associations:
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
# => collection of fields under this organization
organization.fields.count
# => 15
organization.fields.first
# => an individual field object
field = organization.fields.find(1234)
# => an individual field object, fetched by ID
field.name
# => 'Smith Field'
field.archived?
# => false
field.links
# => a hash of API urls related to this field
field.flags
# => collection of flags belonging to this field
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:
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
# => collection of flags under this field
field.flags.count
# => 15
flag = field.flags.first
# => an individual flag object
flag.notes
# => 'A big rock on the left after entering the field'
flag.geometry
# => a GeoJSON formatted hash, for example:
# {
# "type"=>"Feature",
# "geometry"=>{
# "geometries"=>[
# {
# "coordinates"=>[-95.123456, 40.123456],
# "type"=>"Point"
# }
# ],
# "type"=>"GeometryCollection"
# }
# }
field.archived?
# => false
field.proximity_alert_enabled?
# => true
field.links
# => a hash of API urls related to this flag
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 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 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 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 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.
Custom errors help clearly identify problems when using the client:
:sandbox
or :production
.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.
The easiest way to contribute is:
vcr_setup
vcr_setup
.