mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-07 20:13:57 -05:00
Bumped python-garminconnect, added polylines to activity, created map card
This commit is contained in:
189
README.md
189
README.md
@@ -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 |
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -53,6 +53,9 @@ ACTIVITY_ESSENTIAL_KEYS = {
|
||||
"maxRunningCadenceInStepsPerMinute",
|
||||
# Type (simplified)
|
||||
"activityType",
|
||||
# Polyline/GPS (for map display)
|
||||
"hasPolyline",
|
||||
"polyline",
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
screenshots/polyline-card.png
Normal file
BIN
screenshots/polyline-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
170
www/garmin-polyline-card.js
Normal file
170
www/garmin-polyline-card.js
Normal 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;', '');
|
||||
Reference in New Issue
Block a user