Bumped python-garminconnect, added polylines to activity, created map card

This commit is contained in:
Ron Klinkien
2026-01-04 14:17:40 +01:00
parent e713b01846
commit 89ce3e7247
6 changed files with 311 additions and 78 deletions

189
README.md
View File

@@ -21,125 +21,161 @@ Integrate your Garmin Connect fitness data with Home Assistant. Access **110+ se
## Available Sensors
> **Note:** All sensors are enabled by default. You can disable sensors using Home Assistant's sensor configuration.
Sensor values depend on your Garmin devices and connected apps.
> Sensor values depend on your Garmin devices and connected apps.
### Activity & Steps
| Sensor | Description |
|--------|-------------|
| Total Steps | Daily step count |
| Daily Step Goal | Your configured step target |
| Yesterday Steps/Distance | Previous day's complete totals |
| Weekly Step/Distance Avg | 7-day averages |
| Total Distance | Distance walked/run in meters |
| Floors Ascended/Descended | Floors climbed |
| Sensor | Description |
| ------------------------- | ------------------------------ |
| Total Steps | Daily step count |
| Daily Step Goal | Your configured step target |
| Yesterday Steps/Distance | Previous day's complete totals |
| Weekly Step/Distance Avg | 7-day averages |
| Total Distance | Distance walked/run in meters |
| Floors Ascended/Descended | Floors climbed |
### Calories & Nutrition
| Sensor | Description |
|--------|-------------|
| Sensor | Description |
| ------------------------- | --------------------- |
| Total/Active/BMR Calories | Daily calorie metrics |
| Burned/Consumed Calories | Calorie tracking |
| Burned/Consumed Calories | Calorie tracking |
### Heart Rate
| Sensor | Description |
|--------|-------------|
| Resting Heart Rate | Daily resting HR |
| Min/Max Heart Rate | Daily HR range |
| Last 7 Days Avg HR | Weekly average |
| Sensor | Description |
| ---------------------- | ------------------------------ |
| Resting Heart Rate | Daily resting HR |
| Min/Max Heart Rate | Daily HR range |
| Last 7 Days Avg HR | Weekly average |
| HRV Weekly/Nightly Avg | Heart rate variability metrics |
| HRV Baseline | Personal HRV baseline |
| HRV Baseline | Personal HRV baseline |
### Stress & Recovery
| Sensor | Description |
|--------|-------------|
| Avg/Max Stress Level | Stress measurements (0-100) |
| Stress Durations | Time in rest/activity/low/medium/high stress |
| Sensor | Description |
| -------------------- | -------------------------------------------- |
| Avg/Max Stress Level | Stress measurements (0-100) |
| Stress Durations | Time in rest/activity/low/medium/high stress |
### Sleep
| Sensor | Description |
|--------|-------------|
| Sleep Score | Overall sleep quality score |
| Sleep/Awake Duration | Time asleep and awake |
| Deep Sleep | Time in deep sleep |
| Light Sleep | Time in light sleep |
| REM Sleep | Time in REM sleep |
| Sensor | Description |
| -------------------- | --------------------------- |
| Sleep Score | Overall sleep quality score |
| Sleep/Awake Duration | Time asleep and awake |
| Deep Sleep | Time in deep sleep |
| Light Sleep | Time in light sleep |
| REM Sleep | Time in REM sleep |
### Body Battery
| Sensor | Description |
|--------|-------------|
| Sensor | Description |
| ------------------------ | ---------------------------- |
| Body Battery Most Recent | Current energy level (0-100) |
| Charged/Drained | Energy gained/spent |
| Highest/Lowest | Daily peak and low |
| Charged/Drained | Energy gained/spent |
| Highest/Lowest | Daily peak and low |
### Body Composition
| Sensor | Description |
|--------|-------------|
| Weight/BMI | Body weight and mass index |
| Body Fat/Water | Percentage measurements |
| Muscle/Bone Mass | Mass measurements |
| Metabolic Age | Estimated metabolic age |
| Sensor | Description |
| ---------------- | -------------------------- |
| Weight/BMI | Body weight and mass index |
| Body Fat/Water | Percentage measurements |
| Muscle/Bone Mass | Mass measurements |
| Metabolic Age | Estimated metabolic age |
### Hydration
| Sensor | Description |
|--------|-------------|
| Hydration | Daily water intake (ml) |
| Hydration Goal | Target intake |
| Sweat Loss | Estimated fluid loss |
| Sensor | Description |
| -------------- | ----------------------- |
| Hydration | Daily water intake (ml) |
| Hydration Goal | Target intake |
| Sweat Loss | Estimated fluid loss |
### Blood Pressure
| Sensor | Description |
|--------|-------------|
| Systolic | Systolic blood pressure (mmHg) |
| Diastolic | Diastolic blood pressure (mmHg) |
| Pulse | Pulse from blood pressure reading (bpm) |
| Measurement Time | When the BP was measured |
| Sensor | Description |
| ---------------- | --------------------------------------- |
| Systolic | Systolic blood pressure (mmHg) |
| Diastolic | Diastolic blood pressure (mmHg) |
| Pulse | Pulse from blood pressure reading (bpm) |
| Measurement Time | When the BP was measured |
### Health Monitoring
| Sensor | Description |
|--------|-------------|
| SpO2 (Avg/Low/Latest) | Blood oxygen levels |
| HRV Status | Heart rate variability |
| Respiration Rate | Breathing measurements |
| Sensor | Description |
| --------------------- | ---------------------- |
| SpO2 (Avg/Low/Latest) | Blood oxygen levels |
| HRV Status | Heart rate variability |
| Respiration Rate | Breathing measurements |
### Fitness & Performance
| Sensor | Description |
|--------|-------------|
| Fitness Age | Estimated fitness age |
| Chronological Age | Your actual age |
| Endurance Score | Overall endurance rating |
| Training Readiness | Training readiness score (%) |
| Training Status | Current training status phrase |
| Lactate Threshold HR | Lactate threshold heart rate (bpm) |
| Sensor | Description |
| ----------------------- | ------------------------------------ |
| Fitness Age | Estimated fitness age |
| Chronological Age | Your actual age |
| Endurance Score | Overall endurance rating |
| Training Readiness | Training readiness score (%) |
| Training Status | Current training status phrase |
| Lactate Threshold HR | Lactate threshold heart rate (bpm) |
| Lactate Threshold Speed | Lactate threshold running pace (m/s) |
### Activity Tracking
| Sensor | Description |
|--------|-------------|
| Next Alarm | Next scheduled alarm time |
| Last Activity/Activities | Recent activity info |
| Last Workout/Workouts | Scheduled/planned training sessions |
| Badges/User Points/Level | Gamification metrics |
| Sensor | Description |
| ------------------------ | ----------------------------------- |
| Next Alarm | Next scheduled alarm time |
| Last Activity/Activities | Recent activity info |
| Last Workout/Workouts | Scheduled/planned training sessions |
| Badges/User Points/Level | Gamification metrics |
#### Activity Route Map
The `Last Activity` sensor includes a `polyline` attribute with GPS coordinates when the activity has GPS data (`hasPolyline: true`). This can be displayed on a map using the included custom Lovelace card.
**Installation:**
1. Copy `www/garmin-polyline-card.js` to your `<config>/www/` folder
2. Add as a resource: **Settings → Dashboards → ⋮ → Resources → Add Resource**
- URL: `/local/garmin-polyline-card.js`
- Type: JavaScript Module
3. Hard refresh your browser (Ctrl+Shift+R)
**Usage:**
```yaml
type: custom:garmin-polyline-card
entity: sensor.garmin_connect_last_activity
attribute: polyline
title: Last Activity Route
height: 400px
color: "#FF5722"
```
**Options:**
| Option | Default | Description |
| ----------- | ---------------- | ------------------------------------- |
| `entity` | (required) | Sensor entity with polyline attribute |
| `attribute` | `polyline` | Attribute containing GPS coordinates |
| `title` | `Activity Route` | Card title |
| `height` | `300px` | Map height |
| `color` | `#FF5722` | Route line color |
| `weight` | `4` | Route line thickness |
![Activity Route Map](screenshots/polyline-card.png)
### Menstrual Cycle Tracking
| Sensor | Description |
|--------|-------------|
| Cycle Phase | Current menstrual phase |
| Cycle Day | Day of the current cycle |
| Period Day | Day of the period |
| Cycle Length | Total cycle length (days) |
| Period Length | Period length (days) |
| Sensor | Description |
| ------------- | ------------------------- |
| Cycle Phase | Current menstrual phase |
| Cycle Day | Day of the current cycle |
| Period Day | Day of the period |
| Cycle Length | Total cycle length (days) |
| Period Length | Period length (days) |
> **Note:** Menstrual cycle sensors are only available if tracking is enabled in your Garmin Connect account.
@@ -383,6 +419,7 @@ Want to add support for new Garmin features? Here's how to find the API endpoint
7. **Click on a request** to see the full URL and response data
**Share your findings** in a GitHub issue with:
- The full API URL path
- Example response data (redact personal info)

View File

@@ -189,7 +189,30 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
)
summary["lastActivities"] = last_activities
summary["lastActivity"] = last_activities[0] if last_activities else {}
last_activity = last_activities[0] if last_activities else {}
# Fetch polyline for last activity if it has GPS data
if last_activity and last_activity.get("hasPolyline"):
try:
activity_id = last_activity.get("activityId")
activity_details = await self.hass.async_add_executor_job(
self.api.get_activity_details, activity_id, 100, 4000
)
if activity_details:
polyline_data = activity_details.get("geoPolylineDTO", {})
raw_polyline = polyline_data.get("polyline", [])
# Simplify polyline to just lat/lon to reduce attribute size
# Full polyline with all fields can be 350+ bytes per point
# Simplified: ~50 bytes per point, fits HA 16KB limit
last_activity["polyline"] = [
{"lat": p.get("lat"), "lon": p.get("lon")}
for p in raw_polyline
if p.get("lat") is not None and p.get("lon") is not None
]
except Exception as err:
_LOGGER.debug("Failed to fetch polyline for activity: %s", err)
summary["lastActivity"] = last_activity
# Fetch workouts (scheduled/planned training sessions)
try:

View File

@@ -10,7 +10,7 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"requirements": [
"garminconnect>=0.2.37"
"garminconnect>=0.2.38"
],
"version": "1.0.0-beta-01"
}

View File

@@ -53,6 +53,9 @@ ACTIVITY_ESSENTIAL_KEYS = {
"maxRunningCadenceInStepsPerMinute",
# Type (simplified)
"activityType",
# Polyline/GPS (for map display)
"hasPolyline",
"polyline",
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

170
www/garmin-polyline-card.js Normal file
View File

@@ -0,0 +1,170 @@
/**
* Garmin Activity Polyline Map Card
* A simple custom Lovelace card to display activity routes from sensor attributes
*/
class GarminPolylineCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._hass = null;
this._config = null;
this._map = null;
this._polyline = null;
}
setConfig(config) {
if (!config.entity) {
throw new Error('You need to define an entity');
}
this._config = {
entity: config.entity,
attribute: config.attribute || 'polyline',
title: config.title || 'Activity Route',
height: config.height || '300px',
color: config.color || '#FF5722',
weight: config.weight || 4,
...config
};
}
set hass(hass) {
this._hass = hass;
this._updateMap();
}
_updateMap() {
if (!this._hass || !this._config) return;
const stateObj = this._hass.states[this._config.entity];
if (!stateObj) return;
const polylineData = stateObj.attributes[this._config.attribute];
if (!polylineData || !Array.isArray(polylineData) || polylineData.length === 0) {
this._renderNoData();
return;
}
// Convert to Leaflet format [[lat, lon], ...]
const coordinates = polylineData
.filter(p => p.lat != null && p.lon != null)
.map(p => [p.lat, p.lon]);
if (coordinates.length === 0) {
this._renderNoData();
return;
}
this._renderMap(coordinates, stateObj);
}
_renderNoData() {
this.shadowRoot.innerHTML = `
<ha-card header="${this._config.title}">
<div style="padding: 16px; text-align: center; color: var(--secondary-text-color);">
No route data available
</div>
</ha-card>
`;
this._map = null;
}
_renderMap(coordinates, stateObj) {
const activityName = stateObj.state || 'Activity';
// Check if we already have a map container
if (!this._map) {
this.shadowRoot.innerHTML = `
<ha-card header="${this._config.title}">
<div id="map" style="height: ${this._config.height}; width: 100%;"></div>
<div style="padding: 8px 16px; font-size: 12px; color: var(--secondary-text-color);">
${activityName}${coordinates.length} points
</div>
</ha-card>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
`;
// Load Leaflet if not already loaded
if (!window.L) {
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = () => this._initMap(coordinates);
document.head.appendChild(script);
} else {
setTimeout(() => this._initMap(coordinates), 100);
}
} else {
// Update existing polyline
if (this._polyline) {
this._polyline.setLatLngs(coordinates);
this._map.fitBounds(this._polyline.getBounds(), { padding: [20, 20] });
}
}
}
_initMap(coordinates) {
const mapContainer = this.shadowRoot.getElementById('map');
if (!mapContainer || !window.L) return;
// Create map
this._map = L.map(mapContainer, {
zoomControl: true,
scrollWheelZoom: false
});
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this._map);
// Add polyline
this._polyline = L.polyline(coordinates, {
color: this._config.color,
weight: this._config.weight,
opacity: 0.8
}).addTo(this._map);
// Fit map to polyline bounds
this._map.fitBounds(this._polyline.getBounds(), { padding: [20, 20] });
// Add start/end markers
if (coordinates.length > 0) {
L.circleMarker(coordinates[0], {
radius: 8,
color: '#4CAF50',
fillColor: '#4CAF50',
fillOpacity: 1
}).addTo(this._map).bindPopup('Start');
L.circleMarker(coordinates[coordinates.length - 1], {
radius: 8,
color: '#F44336',
fillColor: '#F44336',
fillOpacity: 1
}).addTo(this._map).bindPopup('End');
}
}
getCardSize() {
return 4;
}
static getStubConfig() {
return {
entity: 'sensor.garmin_connect_last_activity',
attribute: 'polyline',
title: 'Activity Route'
};
}
}
customElements.define('garmin-polyline-card', GarminPolylineCard);
// Register with Home Assistant
window.customCards = window.customCards || [];
window.customCards.push({
type: 'garmin-polyline-card',
name: 'Garmin Polyline Card',
description: 'Display Garmin activity routes on a map'
});
console.info('%c GARMIN-POLYLINE-CARD %c loaded ', 'background: #FF5722; color: white;', '');