← Get-Rucked

Get-Rucked + Home Assistant

⭐ Premium Feature

Pull your ruck stats straight into a Home Assistant dashboard: latest ruck, weekly/monthly totals, goals, training load (CTL/ATL/TSB), and rank. Requires an active Get-Rucked Premium subscription.

Recommended: install via HACS

The Get-Rucked integration sets everything up for you — every sensor below, plus a ready-made dashboard card — no YAML required.

  1. In HACS: open the ⋮ menu → Custom repositories → add EvergreenOdyssey/ha-getrucked, category Integration. (On Home Assistant 2026.11 and newer, add it the same way under Settings → Marketplace instead — same repo, same steps.)
  2. Install Get-Rucked, then restart Home Assistant.
  3. Settings → Devices & Services → Add Integration → Get-Rucked, then paste the token from the app: Profile → Home Assistant → Generate New Token.
  4. Add the card to a dashboard: Add Card → Get-Rucked. Pick a device, a compact or detailed layout, and which fields to show.

That's it — a "Get-Rucked" device with every sensor below, and a dashboard card, both set up automatically. Full details, screenshots, and troubleshooting live in the repo README.

Manual setup (advanced)

Don't want to use HACS? Everything below sets up the same stats as plain rest:/template: sensors and Lovelace YAML — no custom integration, no dashboard card, just the raw sensors and a hand-written tile layout.

What you need:

  • An active Get-Rucked Premium subscription
  • A Home Assistant instance you control
  • A way to edit YAML in Home Assistant — the File editor add-on, Studio Code Server, or any other method you prefer

Every YAML block below also lives as its own file in content/home-assistant/ (rest.yaml, template.yaml, dashboard.yaml, secrets.yaml.example), in case you'd rather copy it straight from GitHub.

1. Generate a token

In the Get-Rucked app: Profile → Home Assistant → Generate New Token. The raw token is shown once — tap Share to send it to yourself (email, notes app, whatever gets it onto the device running Home Assistant), since you won't be able to see it again. You can have up to 5 active tokens at once.

2. Add it to secrets.yaml

getrucked_token: "Bearer grk_PASTE_YOUR_TOKEN_HERE"

The secret must hold the full header value — "Bearer " plus your token — not just the raw token by itself.

3. Add the REST sensor

In configuration.yaml: if you already have a rest: key, merge this into its list rather than adding a second one. Set tz in the URL to your own IANA timezone — that's what determines where your "week" and "month" boundaries fall.

home-assistant/rest.yaml
# ── Merge into your existing `rest:` list in configuration.yaml ────────────
#
# Set `tz` in the resource URL to your own IANA timezone (e.g.
# America/New_York) — that's what determines where "weekly" and "monthly"
# boundaries fall. Get your token from the app: Profile → Home Assistant.
# secrets.yaml must hold the FULL header value: "Bearer grk_..." (see
# secrets.yaml.example in this folder).

rest:
  - resource: "https://wisftixjjakjrrauqmlr.supabase.co/functions/v1/ha-stats?tz=America/Los_Angeles"
    method: GET
    scan_interval: 900
    headers:
      Authorization: !secret getrucked_token
    sensor:
      - name: "GetRucked Status"
        unique_id: getrucked_status
        # Single-line template, deliberately — avoids stray newlines/whitespace
        # from multi-line Jinja blocks leaking into the state string.
        value_template: "{{ 'Premium required' if value_json.error is defined and value_json.error == 'premium_required' else ('Unavailable' if value_json.error is defined else 'OK') }}"
        json_attributes:
          - updated_at
          - units
          - calc_version
          - latest_ruck
          - weekly
          - monthly
          - goals
          - training_load
          - rank

This single sensor (sensor.getrucked_status) is the only one that polls the network, every 15 minutes. Its state is deliberately a single-line Jinja expression (no multi-line blocks) so it always resolves to exactly OK, Premium required, or Unavailable — no stray whitespace or newlines.

4. Add the template sensors

Same deal: if you already have a template: key, merge this into its list rather than adding a second one. These all read from sensor.getrucked_status's attributes rather than hitting the network themselves.

home-assistant/template.yaml
# ── Merge into your existing `template:` list in configuration.yaml ───────
#
# All units below assume an imperial Get-Rucked profile (the default). If
# your Get-Rucked profile is set to metric, the API already returns
# metric-converted numbers — you only need to change the unit_of_measurement
# labels here: "mi" -> "km" on distance sensors.

template:
  - sensor:
      - name: "GetRucked Latest Ruck Distance"
        unique_id: getrucked_latest_ruck_distance
        unit_of_measurement: "mi"  # metric profile: "km"
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'latest_ruck').distance
             if state_attr('sensor.getrucked_status', 'latest_ruck') else none }}

      - name: "GetRucked Weekly Miles"
        unique_id: getrucked_weekly_miles
        unit_of_measurement: "mi"  # metric profile: "km"
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'weekly').total_distance
             if state_attr('sensor.getrucked_status', 'weekly') else none }}
        attributes:
          # Used by the main-board summary tile (state_content: [state, rank])
          rank: >-
            {{ state_attr('sensor.getrucked_status', 'rank').name
               if state_attr('sensor.getrucked_status', 'rank') else 'Unavailable' }}

      - name: "GetRucked Monthly Miles"
        unique_id: getrucked_monthly_miles
        unit_of_measurement: "mi"  # metric profile: "km"
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'monthly').total_distance
             if state_attr('sensor.getrucked_status', 'monthly') else none }}

      - name: "GetRucked Fitness (CTL)"
        unique_id: getrucked_fitness_ctl
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'training_load').fitness_ctl
             if state_attr('sensor.getrucked_status', 'training_load') else none }}

      - name: "GetRucked Fatigue (ATL)"
        unique_id: getrucked_fatigue_atl
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'training_load').fatigue_atl
             if state_attr('sensor.getrucked_status', 'training_load') else none }}

      - name: "GetRucked Form (TSB)"
        unique_id: getrucked_form_tsb
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'training_load').form_tsb
             if state_attr('sensor.getrucked_status', 'training_load') else none }}
        attributes:
          label: >-
            {{ state_attr('sensor.getrucked_status', 'training_load').form_label
               if state_attr('sensor.getrucked_status', 'training_load') else 'Unavailable' }}

      - name: "GetRucked Rank"
        unique_id: getrucked_rank
        icon: mdi:trophy
        state: >-
          {{ state_attr('sensor.getrucked_status', 'rank').name
             if state_attr('sensor.getrucked_status', 'rank') else 'Unavailable' }}
        attributes:
          icon_emoji: "{{ state_attr('sensor.getrucked_status', 'rank').icon if state_attr('sensor.getrucked_status', 'rank') else '' }}"
          # metric profile: swap ' mi' for ' km'
          total_miles: "{{ (state_attr('sensor.getrucked_status', 'rank').total_miles | round(1)) ~ ' mi' if state_attr('sensor.getrucked_status', 'rank') else '' }}"

      - name: "GetRucked Miles To Next Rank"
        unique_id: getrucked_miles_to_next_rank
        unit_of_measurement: "mi"  # metric profile: "km"
        state_class: measurement
        state: >-
          {{ state_attr('sensor.getrucked_status', 'rank').miles_to_next
             if state_attr('sensor.getrucked_status', 'rank') else none }}

      # Goals come back as a list, so each one needs its own sensor by index.
      # This covers your first two active goals — copy the block and bump the
      # index (goals[2], goals[3], ...) if you add more.
      - name: "GetRucked Goal 1 Percent"
        unique_id: getrucked_goal_1_percent
        unit_of_measurement: "%"
        state_class: measurement
        state: >-
          {% set goals = state_attr('sensor.getrucked_status', 'goals') %}
          {{ goals[0].percent_complete if goals and goals | length > 0 else none }}
        attributes:
          title: >-
            {% set goals = state_attr('sensor.getrucked_status', 'goals') %}
            {{ goals[0].title if goals and goals | length > 0 else '' }}

      - name: "GetRucked Goal 2 Percent"
        unique_id: getrucked_goal_2_percent
        unit_of_measurement: "%"
        state_class: measurement
        state: >-
          {% set goals = state_attr('sensor.getrucked_status', 'goals') %}
          {{ goals[1].percent_complete if goals and goals | length > 1 else none }}
        attributes:
          title: >-
            {% set goals = state_attr('sensor.getrucked_status', 'goals') %}
            {{ goals[1].title if goals and goals | length > 1 else '' }}

5. Check the configuration and restart

Go to Developer Tools → YAML, click Check Configuration, and fix anything it flags before continuing. Once it comes back clean, restart Home Assistant (from the same page, or Settings → System → Restart).

6. Verify

Go to Developer Tools → States, search for sensor.getrucked_status, and confirm its state is OK. If it isn't, see Troubleshooting below.

7. Add the dashboard

home-assistant/dashboard.yaml
# ── Main board: add this single tile to an existing section ────────────────
# Shows weekly miles as the primary stat with rank underneath; tapping opens
# the full "rucking" subview below.

- type: tile
  entity: sensor.getrucked_weekly_miles
  name: Get-Rucked
  icon: mdi:walk
  state_content:
    - state
    - rank
  tap_action:
    action: navigate
    navigation_path: /desk-board/rucking

---
# ── Subview: add as its own entry in your dashboard's `views:` list ────────
# path "rucking" + your dashboard's own URL slug is what makes the tile's
# navigation_path above (/desk-board/rucking) resolve correctly — adjust the
# tile's navigation_path if your dashboard's slug isn't "desk-board".

- title: Rucking
  path: rucking
  subview: true
  type: sections
  sections:
    - type: grid
      cards:
        - type: heading
          heading: Get-Rucked
          icon: mdi:walk

        - type: tile
          entity: sensor.getrucked_latest_ruck_distance
          name: Latest Ruck
          icon: mdi:map-marker-distance

        - type: tile
          entity: sensor.getrucked_weekly_miles
          name: This Week
          icon: mdi:calendar-week
          state_content:
            - state
            - rank

        - type: tile
          entity: sensor.getrucked_monthly_miles
          name: This Month
          icon: mdi:calendar-month

        - type: tile
          entity: sensor.getrucked_fitness_ctl
          name: Fitness (CTL)
          icon: mdi:heart-pulse

        - type: tile
          entity: sensor.getrucked_fatigue_atl
          name: Fatigue (ATL)
          icon: mdi:heart-flash

        - type: tile
          entity: sensor.getrucked_form_tsb
          name: Form (TSB)
          icon: mdi:chart-line
          state_content:
            - state
            - label

        - type: tile
          entity: sensor.getrucked_rank
          name: Rank
          icon: mdi:trophy
          state_content:
            - state
            - total_miles

        - type: tile
          entity: sensor.getrucked_miles_to_next_rank
          name: To Next Rank
          icon: mdi:flag-checkered

        - type: tile
          entity: sensor.getrucked_goal_1_percent
          name: Goal 1
          icon: mdi:target
          state_content:
            - state
            - title

        - type: tile
          entity: sensor.getrucked_goal_2_percent
          name: Goal 2
          icon: mdi:target
          state_content:
            - state
            - title

This has two parts, separated by ---:

  • Main tile (the first block): shows weekly miles as its state and your current rank underneath, and taps through to a "Rucking" subview.
  • Subview (the second block): the full tile set — latest ruck, weekly, monthly, fitness/fatigue/form, rank, miles to next rank, and both goals.

To add them:

  • Main tile — open your dashboard, Edit Dashboard → Add Card, choose Tile, then switch the card editor to YAML mode (the </> code icon) and paste the first block (everything above the ---).
  • Subview — Edit Dashboard → Add View, then open that new view's ⋮ menu → Edit in YAML, and paste the second block (everything below the ---), replacing the view's default content.

Tile cards use state_content (a list, e.g. [state, label]) to show an attribute alongside the state — tile cards do not support secondary_info/attribute (that's a different card type's config).

Change /desk-board/ in navigation_path to match your own dashboard's URL slug — check your browser's address bar while viewing your dashboard to find it.

8. Metric users

The API already returns metric-converted numbers (km, kg, etc.) if your Get-Rucked profile is set to metric. The only thing to change here is cosmetic: swap "mi" → "km" in template.yaml's unit_of_measurement lines (each one is marked with a comment) and in the Rank sensor's total_miles attribute (swap ' mi' → ' km').

Troubleshooting

  • Sensor shows "Premium required" or "Unavailable" after your subscription lapses — resubscribe in the app. Your existing token resumes working on its own once Premium is active again; there's no need to regenerate it.
  • Sensor shows "Unavailable", or the logbook shows a 401 — the token is wrong, has been revoked in the app, or secrets.yaml is missing the "Bearer " prefix.
  • Configuration check fails with an indentation error — Home Assistant YAML uses spaces only; make sure your editor isn't inserting tabs.
  • Numbers aren't updating — the sensor only polls every 15 minutes (scan_interval: 900); wait for the next cycle, or check Developer Tools → States for the raw attributes in the meantime.

Help

Anything else: support@get-rucked.com. Revoking a token in the app takes effect immediately.

← Get-Rucked