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.
The Get-Rucked integration sets everything up for you — every sensor below, plus a ready-made dashboard card — no YAML required.
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.)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.
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:
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.
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.
secrets.yamlgetrucked_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.
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.
# ── 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
- rankThis 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.
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.
# ── 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 '' }}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).
Go to Developer Tools → States, search for sensor.getrucked_status, and confirm its state is OK. If it isn't, see Troubleshooting below.
# ── 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
- titleThis has two parts, separated by ---:
To add them:
</> code icon) and paste the first block (everything above 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.
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').
secrets.yaml is missing the "Bearer " prefix.scan_interval: 900); wait for the next cycle, or check Developer Tools → States for the raw attributes in the meantime.Anything else: support@get-rucked.com. Revoking a token in the app takes effect immediately.