No release in over 3 years
Low commit activity in last 3 years
This gem interacts with the My John Deere API. It handles the hard work of John Deere's oAuth 2.0 authentication. It provides REST request methods for GET, POST, PUT, and DELETE that abstract away the John Deere-specific headers and protocols. It provides convenience methods similar to ActiveRecord so you can write things like: `client.organizations.find(123).assets` or `organization.assets.create(attributes)` instead of making each of the required direct API calls manually.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.7.5
~> 5.0
~> 3.7.6

Runtime

>= 2.1.0, ~> 2.1
~> 1.4.4
 Project Readme

Ruby Client for the MyJohnDeere API

CircleCI

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
  • Installation
  • Authorizing with John Deere via oAuth 1.0
  • Interacting with the User's John Deere Account
  • Using the Client to Do Stuff
    • Contribution Products
    • Contribution Definitions
    • Organizations
    • Assets
    • Asset Locations
    • Fields
  • Direct API Requests
    • GET
    • POST
    • PUT
    • DELETE
  • Errors
  • How Can I Help?
    • Give Us a Star!
    • Contribute to This Gem

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:

# 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])

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(
  # 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]
)

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
# => 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:

  • all
  • count
  • first
  • find(contribution_definition_id)

An individual contribution definition supports the following methods and associations:

  • id
  • name
  • links
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:

  • 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
# => 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:

  • 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
# => 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:

  • create(attributes)
  • all
  • count
  • first

An individual location supports the following methods:

  • timestamp
  • geometry
  • measurement_data
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:

  • 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
# => 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:

  • all
  • count
  • first

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
# => 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

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.