Ich tracke meine sportlichen Aktivitäten seit einer Weile über eine Garmin-Uhr. Skating, Radfahren, Tennis. Irgendwann wollte ich die Daten nicht nur in der Garmin-App sehen, sondern direkt in Home Assistant. Also habe ich mir ein eigenes Fitness-Dashboard gebaut, das Monats- und Jahresstatistiken, Fortschrittsanzeigen und ein automatisches Wochen-Highlight anzeigt. Das Ganze läuft komplett über YAML, mit Input Helpern, Template-Sensoren und ein paar Automationen. Ich zeige dir Schritt für Schritt, wie du das bei dir einrichten kannst.
Was das Dashboard kann
Das Dashboard zeigt mir auf einen Blick, wo ich sportlich gerade stehe. Kilometer und Minuten pro Monat und pro Jahr, getrennt nach Sportart. Dazu ein Jahresfortschritt für mein Skating-Ziel, eine Hochrechnung ob ich das Ziel bis Jahresende schaffe, und das sportliche Highlight der Woche. Alles automatisch berechnet, ich muss da nichts manuell eintragen.
Die Daten kommen aus der Garmin-Integration und werden über Automationen in Input Helper geschrieben. Template-Sensoren rechnen daraus Fortschritte und Prognosen. Im Dashboard selbst stecken Mushroom Cards, Gauge Cards und ein Statistics Graph für den Skating-Verlauf der letzten 30 Tage.
Voraussetzungen
Damit das Setup funktioniert, brauchst du eine laufende Garmin-Anbindung in Home Assistant. Konkret brauchst du einen Sensor sensor.last_activities, der eine Liste der letzten Workouts als Attribut last_activities bereitstellt. Die offizielle Garmin Connect Integration liefert genau das.
Dazu brauchst du die Packages-Struktur in deiner configuration.yaml, damit du alles sauber in eine eigene Datei auslagern kannst:
homeassistant:
packages: !include_dir_named packages/An optionalen Frontend-Erweiterungen empfehle ich Mushroom Cards und card-mod aus HACS. Die sind nicht zwingend nötig, machen das Dashboard aber deutlich übersichtlicher.
Schritt 1: Input Helper anlegen
Die Basis bilden Input Helper, die Kilometer, Minuten und Einheiten pro Sportart speichern. Ich habe das nach Monat und Jahr getrennt, damit ich die Monatswerte am Ersten automatisch zurücksetzen kann, ohne die Jahreswerte zu verlieren.
Erstelle eine Datei packages/garmin.yaml mit folgendem Inhalt:
1input_text:
2 garmin_last_logged_activity_id:
3 name: Garmin – Letzte geloggte Aktivität
4 max: 20
5
6
7input_number:
8 # Tennis
9 garmin_tennis_month_km:
10 name: Garmin – Tennis Monat (km)
11 min: 0
12 max: 10000
13 step: 0.01
14 unit_of_measurement: "km"
15 mode: box
16
17 garmin_tennis_year_km:
18 name: Garmin – Tennis Jahr (km)
19 min: 0
20 max: 50000
21 step: 0.01
22 unit_of_measurement: "km"
23 mode: box
24
25 garmin_tennis_month_min:
26 name: Garmin – Tennis Monat (Min)
27 min: 0
28 max: 10000
29 step: 1
30 unit_of_measurement: "min"
31 mode: box
32
33 garmin_tennis_year_min:
34 name: Garmin – Tennis Jahr (Min)
35 min: 0
36 max: 50000
37 step: 1
38 unit_of_measurement: "min"
39 mode: box
40
41 # Radfahren
42 garmin_bike_month_km:
43 name: Garmin – Radfahren Monat (km)
44 min: 0
45 max: 10000
46 step: 0.01
47 unit_of_measurement: "km"
48 mode: box
49
50 garmin_bike_year_km:
51 name: Garmin – Radfahren Jahr (km)
52 min: 0
53 max: 50000
54 step: 0.01
55 unit_of_measurement: "km"
56 mode: box
57
58 garmin_bike_month_min:
59 name: Garmin – Radfahren Monat (Min)
60 min: 0
61 max: 10000
62 step: 1
63 unit_of_measurement: "min"
64 mode: box
65
66 garmin_bike_year_min:
67 name: Garmin – Radfahren Jahr (Min)
68 min: 0
69 max: 50000
70 step: 1
71 unit_of_measurement: "min"
72 mode: box
73
74 # Skaten
75 garmin_skate_month_km:
76 name: Garmin – Skating Monat (km)
77 min: 0
78 max: 10000
79 step: 0.01
80 unit_of_measurement: "km"
81 mode: box
82
83 garmin_skate_year_km:
84 name: Garmin – Skating Jahr (km)
85 min: 0
86 max: 50000
87 step: 0.01
88 unit_of_measurement: "km"
89 mode: box
90
91 garmin_skate_month_min:
92 name: Garmin – Skating Monat (Min)
93 min: 0
94 max: 10000
95 step: 1
96 unit_of_measurement: "min"
97 mode: box
98
99 garmin_skate_year_min:
100 name: Garmin – Skating Jahr (Min)
101 min: 0
102 max: 50000
103 step: 1
104 unit_of_measurement: "min"
105 mode: box
106
107 # Skating-Jahresziel in km (passe den initialen Wert in der UI an dein eigenes Ziel an)
108 garmin_skating_jahresziel:
109 name: Garmin – Skating Jahresziel
110 min: 0
111 max: 50000
112 step: 1
113 unit_of_measurement: "km"
114 mode: box
115
116counter:
117 garmin_tennis_sessions:
118 name: Garmin – Tennis-Einheiten
119 initial: 0
120 step: 1
121
122 garmin_bike_sessions:
123 name: Garmin – Radfahr-Einheiten
124 initial: 0
125 step: 1
126
127 garmin_skate_sessions:
128 name: Garmin – Skating-Einheiten
129 initial: 0
130 step: 1Der input_text für garmin_last_logged_activity_id ist wichtig: Damit merkt sich die Automation später, welche Aktivität zuletzt verarbeitet wurde. So werden keine Einheiten doppelt gezählt.
Schritt 2: Template-Sensoren erstellen
Auf Basis der Input Helper kannst du jetzt Template-Sensoren bauen, die automatisch rechnen. Der Skating-Jahresfortschritt zum Beispiel zeigt dir in Prozent, wie nah du an deinem Jahresziel bist. Die Prognose rechnet hoch, ob du es bei gleichbleibendem Tempo bis Jahresende schaffst.
Füge diese Sensoren in die gleiche packages/garmin.yaml ein:
1template:
2 # Jahresfortschritt in Prozent
3 - sensor:
4 - name: "Garmin Skating Jahresfortschritt"
5 unique_id: garmin_skating_year_progress
6 unit_of_measurement: "%"
7 state_class: measurement
8 state: >
9 {% set km = states('input_number.garmin_skate_year_km') | float(0) %}
10 {% set ziel = states('input_number.garmin_skating_jahresziel') | float(0) %}
11 {% if ziel > 0 %}
12 {{ ((km / ziel) * 100) | round(1) }}
13 {% else %}0{% endif %}
14 attributes:
15 ziel_km: "{{ states('input_number.garmin_skating_jahresziel') | int(0) }}"
16 availability: >
17 {{ states('input_number.garmin_skate_year_km') not in ['unknown', 'unavailable'] }}
18
19 # Skating-Minuten Monat als Sensor mit device_class
20 - sensor:
21 - name: "Garmin Skating Minuten Monat"
22 unique_id: garmin_skating_min_month
23 unit_of_measurement: "min"
24 device_class: duration
25 state_class: measurement
26 state: >
27 {{ states('input_number.garmin_skate_month_min') | float(0) }}
28 availability: >
29 {{ states('input_number.garmin_skate_month_min') not in ['unknown', 'unavailable'] }}
30
31 # Skating-Projektion auf das Jahresende (kein hardcoded Jahr,
32 # der friendly_name wird automatisch zum aktuellen Jahr)
33 - sensor:
34 - name: "Skating-Projektion {{ now().year }}"
35 unique_id: garmin_skating_projection
36 unit_of_measurement: "km"
37 state_class: measurement
38 device_class: distance
39 state: >
40 {% set current_km = states('input_number.garmin_skate_year_km') | float(0) %}
41 {% set day = now().timetuple().tm_yday %}
42 {% if day > 0 %}
43 {{ (current_km / day * 365) | round(1) }}
44 {% else %}0{% endif %}
45 availability: >
46 {{ states('input_number.garmin_skate_year_km') not in ['unknown', 'unavailable'] }}
47
48 # Wochen-Highlight: längste Aktivität der letzten 7 Tage.
49 # Trigger-based, damit die Loop nur einmal läuft und nicht pro Attribut neu.
50 - trigger:
51 - platform: state
52 entity_id: sensor.last_activities
53 attribute: last_activities
54 - platform: time_pattern
55 hours: "/1"
56 sensor:
57 - name: "Garmin Wochen-Highlight"
58 unique_id: garmin_weekly_highlight
59 state: "{{ best.activityName if best else 'Keine Aktivität' }}"
60 attributes:
61 activityName: "{{ best.activityName if best else '–' }}"
62 distance: "{{ ((best.distance | float(0)) / 1000) | round(1) if best else 0 }}"
63 duration: "{{ ((best.duration | float(0)) / 60) | round(0) if best else 0 }}"
64 calories: "{{ best.calories | round(0) if best and best.calories is defined else 0 }}"
65 date: "{{ (best.startTime | string)[:10] if best and best.startTime is defined else '–' }}"
66 variables:
67 best: >
68 {% set acts = state_attr('sensor.last_activities', 'last_activities') | default([]) %}
69 {% set week_start = now() - timedelta(days=7) %}
70 {% set ns = namespace(best=none, best_dist=0) %}
71 {% for a in acts %}
72 {% set t = a.startTime | as_datetime(none) %}
73 {% if t and t >= week_start and (a.distance | float(0)) > ns.best_dist %}
74 {% set ns.best = a %}
75 {% set ns.best_dist = a.distance | float(0) %}
76 {% endif %}
77 {% endfor %}
78 {{ ns.best }}Das Skating-Jahresziel setzt du nach dem Speichern in der Home-Assistant-UI über den Helper Garmin – Skating Jahresziel. Bei mir steht es auf der jeweiligen Jahreszahl, also 2026 km für 2026. Ob das klappt, sehe ich dann im Dezember. Der Friendly-Name vom Projektions-Sensor passt sich automatisch ans aktuelle Jahr an, da musst du nichts manuell umbenennen.
Schritt 3: Automationen für die Datenpflege
Damit die Input Helper automatisch gefüllt werden, brauchst du drei Automationen. Die erste setzt am Monatsanfang die Monatswerte zurück, die zweite einmal jährlich am 1. Januar die Jahreswerte und Counter, und die dritte erkennt neue Aktivitäten und schreibt Kilometer, Minuten und Einheiten in die passenden Helper.
Die Monatsreset-Automation:
1alias: Garmin – Monatswerte zurücksetzen
2mode: single
3triggers:
4 - trigger: time
5 at: "00:10:00"
6 alias: Täglicher Trigger um 00:10 Uhr
7 - trigger: time
8 at: "01:00:00"
9 alias: Fallback Trigger um 01:00 Uhr
10conditions:
11 - condition: template
12 alias: Nur ausführen wenn heute der 1. des Monats ist
13 value_template: "{{ now().day == 1 }}"
14actions:
15 - alias: Monatswerte (km & Minuten) auf 0 setzen
16 action: input_number.set_value
17 target:
18 entity_id:
19 - input_number.garmin_tennis_month_km
20 - input_number.garmin_tennis_month_min
21 - input_number.garmin_bike_month_km
22 - input_number.garmin_bike_month_min
23 - input_number.garmin_skate_month_km
24 - input_number.garmin_skate_month_min
25 data:
26 value: 0Die Jahreswerte-Reset-Automation läuft analog, nur am 1. Januar und setzt zusätzlich die Counter zurück:
1alias: Garmin – Jahreswerte zurücksetzen
2mode: single
3triggers:
4 - trigger: time
5 at: "00:15:00"
6conditions:
7 - condition: template
8 value_template: "{{ now().month == 1 and now().day == 1 }}"
9actions:
10 - alias: Jahreswerte auf 0 setzen
11 action: input_number.set_value
12 target:
13 entity_id:
14 - input_number.garmin_tennis_year_km
15 - input_number.garmin_tennis_year_min
16 - input_number.garmin_bike_year_km
17 - input_number.garmin_bike_year_min
18 - input_number.garmin_skate_year_km
19 - input_number.garmin_skate_year_min
20 data:
21 value: 0
22 - alias: Counter auf 0 zurücksetzen
23 action: counter.reset
24 target:
25 entity_id:
26 - counter.garmin_tennis_sessions
27 - counter.garmin_bike_sessions
28 - counter.garmin_skate_sessionsJetzt zur eigentlichen Aktivitäts-Automation. Sie hört auf Änderungen am Attribut last_activities von sensor.last_activities, schaut welche Aktivitäten neuer sind als die zuletzt geloggte ID, und akkumuliert je nach Sportart Kilometer, Minuten und Einheiten.
Drei Stolpersteine, die ich beim Bauen mehrfach getroffen habe und in der Version unten gleich mit gefixt sind:
mode: queuedstattsingle. Wennlast_activitiessich kurz nacheinander mehrfach aktualisiert (passiert beim ersten Garmin-Sync nach einer Aktivität), würdemode: singledie zweiten Trigger einfach verwerfen. Mitqueuedwerden sie der Reihe nach abgearbeitet.last_ideinmal vor der Schleife in eine Variable lesen. Wenn man die Variable in jeder Iteration neu aus dem Helper-State lädt, steht sie bei einem Mid-Sequence-Abbruch (z.B. Service-Call-Timeout) inkonsistent da, und Werte werden teils doppelt gezählt. Stattdessen einmal alsstart_last_idschnappen, am Ende einmal auf die höchste verarbeitete ID setzen.actsvor der Schleife sanitisieren. Die Garmin-Connect-Integration liefertstartTimein einigen HA-Versionen als Python-datetime-Objekt.repeat.for_eachmag aber nur reine Listen aus Strings/Zahlen und wirft sonst „Repeat for_each must be a list of items". Der Workaround ist, vor der Schleife eine neue Liste zu bauen, die nur die vier Felder hält, die wir wirklich brauchen, und alles inint/float/stringzwingt.
Hier die komplette Automation:
1alias: Garmin – Neue Aktivität protokollieren
2mode: queued
3max: 10
4triggers:
5 - trigger: state
6 entity_id: sensor.last_activities
7 attribute: last_activities
8conditions:
9 - condition: template
10 alias: Mindestens eine neue Aktivität vorhanden
11 value_template: >-
12 {% set raw = state_attr('sensor.last_activities', 'last_activities') | default([]) %}
13 {% set last_id = states('input_text.garmin_last_logged_activity_id') | int(0) %}
14 {% if raw | length == 0 %}{{ false }}
15 {% else %}
16 {% set newest_id = raw | map(attribute='activityId') | map('int', 0) | max %}
17 {{ newest_id > last_id }}
18 {% endif %}
19variables:
20 start_last_id: "{{ states('input_text.garmin_last_logged_activity_id') | int(0) }}"
21 acts: >-
22 {% set raw = state_attr('sensor.last_activities', 'last_activities') | default([]) %}
23 {% set ns = namespace(items=[]) %}
24 {% for x in raw %}
25 {% set ns.items = ns.items + [{
26 'activityId': x.activityId | int(0),
27 'activityType': x.activityType | default(''),
28 'distance': x.distance | float(0),
29 'duration': x.duration | float(0)
30 }] %}
31 {% endfor %}
32 {{ ns.items }}
33actions:
34 - alias: Alle neuen Aktivitäten verarbeiten
35 repeat:
36 for_each: "{{ acts }}"
37 sequence:
38 - alias: Variablen für diese Aktivität setzen
39 variables:
40 act_id: "{{ repeat.item.activityId | int(0) }}"
41 typekey: "{{ repeat.item.activityType | default('') }}"
42 km: "{{ (repeat.item.distance | float(0)) / 1000 }}"
43 mins: "{{ ((repeat.item.duration | float(0)) / 60) | round(0) }}"
44 - alias: Nur verarbeiten wenn ID neuer als zu Beginn geloggte
45 condition: template
46 value_template: "{{ act_id > start_last_id }}"
47 - alias: Werte je nach Aktivitätstyp akkumulieren
48 choose:
49 - alias: Tennis
50 conditions:
51 - condition: template
52 value_template: "{{ typekey == 'tennis_v2' }}"
53 sequence:
54 - action: input_number.set_value
55 target: { entity_id: input_number.garmin_tennis_month_km }
56 data:
57 value: "{{ (states('input_number.garmin_tennis_month_km') | float(0) + km | float(0)) | round(2) }}"
58 - action: input_number.set_value
59 target: { entity_id: input_number.garmin_tennis_year_km }
60 data:
61 value: "{{ (states('input_number.garmin_tennis_year_km') | float(0) + km | float(0)) | round(2) }}"
62 - action: input_number.set_value
63 target: { entity_id: input_number.garmin_tennis_month_min }
64 data:
65 value: "{{ (states('input_number.garmin_tennis_month_min') | float(0) + mins | float(0)) | round(0) }}"
66 - action: input_number.set_value
67 target: { entity_id: input_number.garmin_tennis_year_min }
68 data:
69 value: "{{ (states('input_number.garmin_tennis_year_min') | float(0) + mins | float(0)) | round(0) }}"
70 - action: counter.increment
71 target: { entity_id: counter.garmin_tennis_sessions }
72
73 - alias: Radfahren
74 conditions:
75 - condition: or
76 conditions:
77 - condition: template
78 value_template: "{{ typekey == 'cycling' }}"
79 - condition: template
80 value_template: "{{ typekey == 'e_bike_fitness' }}"
81 sequence:
82 - action: input_number.set_value
83 target: { entity_id: input_number.garmin_bike_month_km }
84 data:
85 value: "{{ (states('input_number.garmin_bike_month_km') | float(0) + km | float(0)) | round(2) }}"
86 - action: input_number.set_value
87 target: { entity_id: input_number.garmin_bike_year_km }
88 data:
89 value: "{{ (states('input_number.garmin_bike_year_km') | float(0) + km | float(0)) | round(2) }}"
90 - action: input_number.set_value
91 target: { entity_id: input_number.garmin_bike_month_min }
92 data:
93 value: "{{ (states('input_number.garmin_bike_month_min') | float(0) + mins | float(0)) | round(0) }}"
94 - action: input_number.set_value
95 target: { entity_id: input_number.garmin_bike_year_min }
96 data:
97 value: "{{ (states('input_number.garmin_bike_year_min') | float(0) + mins | float(0)) | round(0) }}"
98 - action: counter.increment
99 target: { entity_id: counter.garmin_bike_sessions }
100
101 - alias: Skating
102 conditions:
103 - condition: template
104 value_template: "{{ typekey == 'inline_skating' }}"
105 sequence:
106 - action: input_number.set_value
107 target: { entity_id: input_number.garmin_skate_month_km }
108 data:
109 value: "{{ (states('input_number.garmin_skate_month_km') | float(0) + km | float(0)) | round(2) }}"
110 - action: input_number.set_value
111 target: { entity_id: input_number.garmin_skate_year_km }
112 data:
113 value: "{{ (states('input_number.garmin_skate_year_km') | float(0) + km | float(0)) | round(2) }}"
114 - action: input_number.set_value
115 target: { entity_id: input_number.garmin_skate_month_min }
116 data:
117 value: "{{ (states('input_number.garmin_skate_month_min') | float(0) + mins | float(0)) | round(0) }}"
118 - action: input_number.set_value
119 target: { entity_id: input_number.garmin_skate_year_min }
120 data:
121 value: "{{ (states('input_number.garmin_skate_year_min') | float(0) + mins | float(0)) | round(0) }}"
122 - action: counter.increment
123 target: { entity_id: counter.garmin_skate_sessions }
124 - alias: Letzte geloggte ID auf höchste verarbeitete ID setzen
125 action: input_text.set_value
126 target: { entity_id: input_text.garmin_last_logged_activity_id }
127 data:
128 value: "{{ acts | map(attribute='activityId') | map('int', 0) | max | string }}"Damit ist sichergestellt: Jede Aktivität wird genau einmal gezählt, der Zustand bleibt auch bei Restarts und schnellen Folge-Triggern stabil, und die Schleife stolpert nicht mehr über datetime-Objekte. Wenn du eine weitere Sportart ergänzen willst, kopierst du einen der drei choose-Blöcke und passt typekey, die Helper-Namen und den Counter an.
Schritt 4: Dashboard anlegen
Für die Darstellung empfehle ich ein separates Dashboard. Geh dafür in Home Assistant auf Einstellungen > Dashboards > Dashboard hinzufügen und wechsle dann in den YAML-Modus.
Das Dashboard nutzt Sections mit maximal 3 Spalten. In der ersten Section siehst du Jahresziel und Fortschritt als Gauge, die Skating-Projektion und dein Fitness-Alter. In der zweiten Section stehen die letzten fünf Aktivitäten mit dynamischen Icons, je nach Sportart. Die dritte zeigt den Skating-Verlauf der letzten 30 Tage als Balkendiagramm. Danach kommen die Detailkarten für jede Sportart einzeln.
Die komplette Dashboard-YAML findest du in der Config-Sammlung. Kopiere den Inhalt, passe die Entity-IDs an dein Setup an und füge ihn im YAML-Modus ein.
Sportarten anpassen und erweitern
Das Setup ist bewusst so gebaut, dass du es einfach erweitern kannst. Wenn du statt Tennis zum Beispiel Laufen trackst, legst du dir die gleichen Input Helper an, mit garmin_run_month_km, garmin_run_year_km und so weiter. Die Automation erkennt den Aktivitätstyp über den Namen aus der Garmin API und ordnet die Werte entsprechend zu.
Bei mir laufen gerade drei Sportarten parallel. Das reicht für meinen Alltag. Du könntest aber ohne Probleme noch Schwimmen, Wandern oder was auch immer ergänzen. Das Muster ist immer das gleiche: Input Helper pro Sportart, Template-Sensoren für Berechnungen, Mushroom Cards im Dashboard.
Warum das Ganze in Home Assistant?
Garmin Connect hat natürlich selbst Statistiken. Aber ich finde es wahnsinnig praktisch, die Fitnessdaten zusammen mit allem anderen Smart-Home-Kram auf einem Dashboard zu haben. Morgens kurz aufs Tablet geschaut und ich sehe Wetter, Energieverbrauch und wie viele Kilometer ich diesen Monat schon geskatet bin. Alles an einem Ort.
Dazu kommt: Die Daten bleiben lokal. Keine Cloud-Abhängigkeit für die Auswertung. Und wenn Garmin irgendwann die App umstellt oder Features streicht, habe ich meine Zahlen trotzdem noch in Home Assistant.
Hast du auch Fitnessdaten in Home Assistant eingebunden? Schreib mir gerne in die Kommentare, was du damit trackst.
