Skip to content

Use Garmin API from Elixir. Fetch activities, steps, and more!

License

Notifications You must be signed in to change notification settings

arathunku/nimrag

Repository files navigation

Nimrag

Actions Status Hex.pm Documentation License

Use Garmin API from Elixir. Fetch activities, steps, and more!

Nimrag is Garmin in reverse, because we have to reverse engineer the API and auth. ¯_(ツ)_/¯

Installation

The package can be installed by adding Nimrag to your list of dependencies in mix.exs:

def deps do
  [
    {:nimrag, "~> 0.1.0"}
  ]
end

If you'd like to use it from Livebook, take a look at this example or watch demo:

2024-04-08_18-53-28.mp4

Usage

Initial auth

Garmin doesn't have any official public API for individuals, only businesses. It means we're required to use username, password and (optionally) MFA code to obtain OAuth tokens. OAuth1 token is valid for up to a year and it's used to generate OAuth2 token that expires quickly, OAuth2 token is used for making the API calls. After OAuth1 token expires, we need to repeat the authentication process.

Please see Nimrag.Auth docs for more details about authentication, and see Nimrag.Credentials on how to avoid providing plaintext credentials directly in code.

# If you're using it for the first time, we need to get OAuth Tokens first.
credentials = Nimrag.Credentials.new("username", "password")
# you may get prompted for MFA token on stdin
{:ok, client} = Nimrag.Auth.login_sso()

# OPTIONAL: If you'd like to store OAuth tokens in ~/.config/nimrag and not log in every time
:ok = Nimrag.Credentials.write_fs_oauth1_token(client)
:ok = Nimrag.Credentials.write_fs_oauth2_token(client)

General

Use functions from Nimrag to fetch data from Garmin's API.

# Restore previously cached in ~/.nimrag OAuth tokens
client = Nimrag.Client.new() |> Nimrag.Client.with_auth(Nimrag.Credentials.read_oauth_tokens!())

# Fetch your profile
{:ok, %Nimrag.Api.Profile{} = profile, client} = Nimrag.profile(client)

# Fetch your latest activity
{:ok, %Nimrag.Api.Activity{} = activity, client} = Nimrag.last_activity(client)

# Call at the end of the session to cache new OAuth2 token
:ok = Nimrag.Credentials.write_fs_oauth2_token(client)

Fallback to raw responses

Nimrag module has also functions with _req suffix. They return {:ok, Req.Response{}, client} and do not process nor validate returned body. Other functions may return, if applicable, structs with known fields.

This is very important split between response and transformation. Garmin's API may change at any time but it should still be possible to fallback to raw response if needed, so that any user of the library didn't have to wait for a fix.

API calls return {:ok, data, client} or {:error, error}. On success, client is there so that it could be chained with always up to date OAuth2 token that will get automatically updated when it's near expiration.

There's at this moment no extensive coverage of API endpoints, feel free to submit PR with new structs and endpoints, see Contributing.

Rate limit

By default, Nimrag uses Hammer for rate limiting requests. If you are already using Hammer, you can configure backend key via config.

config :nimrag, hammer: [backend: :custom]

By default, Hammer's ETS based backend will be started.

API note {: .warning}

Nimrag is not using public Garmin's API so please be good citizens and don't hammer their servers.

See Nimrag.Client.new/1 for more details about changing the api limits.

Contributing

Please do! Garmin has a lot of endpoints, some are useful, some are less useful and responses contain a lot of fields!

You can discover new endpoints by setting up mitmproxy and capturing traffic from mobile app or website. You can also take a look at python-garminconnect.

For local setup, the project has minimal dependencies and is easy to install

# fork and clone the repo
$ mix deps.get
# ensure everything works!
$ mix check
# do your changes
$ mix check
# submit PR!
# THANK YOU!

How to add new API endpoints, fields

  1. Add new function in Nimrag module, one with _req suffix and one without. Functions with _req should returns direct Nimrag.Api result.

  2. Call _req function in test env and save its response as fixture.

    Example for Nimrag.profile/1:

    $ MIX_ENV=test iex -S mix
    client = Nimrag.Client.new() |> Nimrag.Client.with_auth(Nimrag.Credentials.read_oauth_tokens!())
    client |> Nimrag.profile_req() |> Nimrag.ApiHelper.store_response_as_test_fixture()
  3. Add tests for new function in [test/nimrag_test.exs]

  4. Define new Schematic schema in Nimrag.Api, and ensure all tests pass.

License

Copyright © 2024 Michal Forys

This project is licensed under the MIT license.