Added create- and upload activity services, more type checking

This commit is contained in:
Ron Klinkien
2026-01-03 17:02:32 +01:00
parent ef9c1efe27
commit f8a9a0d61c
7 changed files with 274 additions and 2 deletions

View File

@@ -247,6 +247,30 @@ data:
notes: Measured with Beurer BC54
```
### Create Activity
Creates an activity in Garmin Connect:
```yaml
action: garmin_connect.create_activity
data:
activity_name: "Morning Run"
activity_type: running
start_datetime: "2025-01-21T08:30:00"
duration_min: 30
distance_km: 5.0
```
### Upload Activity
Uploads an activity file (FIT, GPX, TCX) to Garmin Connect:
```yaml
action: garmin_connect.upload_activity
data:
file_path: "/config/activities/run.fit"
```
### Enable Debug Logging
Add the relevant lines below to the `configuration.yaml`:

View File

@@ -1,6 +1,9 @@
"""Services for Garmin Connect integration."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
@@ -9,11 +12,15 @@ from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
if TYPE_CHECKING:
from .coordinator import GarminConnectDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
# Service schemas
SERVICE_ADD_BODY_COMPOSITION = "add_body_composition"
SERVICE_ADD_BLOOD_PRESSURE = "add_blood_pressure"
SERVICE_CREATE_ACTIVITY = "create_activity"
ADD_BODY_COMPOSITION_SCHEMA = vol.Schema(
{
@@ -43,8 +50,26 @@ ADD_BLOOD_PRESSURE_SCHEMA = vol.Schema(
}
)
CREATE_ACTIVITY_SCHEMA = vol.Schema(
{
vol.Required("activity_name"): cv.string,
vol.Required("activity_type"): cv.string,
vol.Required("start_datetime"): cv.string,
vol.Required("duration_min"): int,
vol.Optional("distance_km", default=0.0): vol.Coerce(float),
vol.Optional("time_zone"): cv.string,
}
)
def _get_coordinator(hass: HomeAssistant):
SERVICE_UPLOAD_ACTIVITY = "upload_activity"
UPLOAD_ACTIVITY_SCHEMA = vol.Schema(
{
vol.Required("file_path"): cv.string,
}
)
def _get_coordinator(hass: HomeAssistant) -> GarminConnectDataUpdateCoordinator:
"""Get the first available coordinator from config entries."""
entries = hass.config_entries.async_entries(DOMAIN)
if not entries:
@@ -61,7 +86,7 @@ def _get_coordinator(hass: HomeAssistant):
translation_key="integration_not_loaded",
)
return entry.runtime_data
return entry.runtime_data # type: ignore[no-any-return]
async def async_setup_services(hass: HomeAssistant) -> None:
@@ -147,6 +172,77 @@ async def async_setup_services(hass: HomeAssistant) -> None:
translation_placeholders={"error": str(err)},
) from err
async def handle_create_activity(call: ServiceCall) -> None:
"""Handle create_activity service call."""
coordinator = _get_coordinator(hass)
activity_name = call.data.get("activity_name")
activity_type = call.data.get("activity_type")
start_datetime = call.data.get("start_datetime")
# API requires milliseconds format: "2023-12-02T10:00:00.000"
if start_datetime and "." not in start_datetime:
start_datetime = f"{start_datetime}.000"
duration_min = call.data.get("duration_min")
distance_km = call.data.get("distance_km", 0.0)
# Default to HA's configured timezone
time_zone = call.data.get("time_zone") or str(hass.config.time_zone)
if not await coordinator.async_login():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="login_failed",
)
try:
await hass.async_add_executor_job(
coordinator.api.create_manual_activity,
start_datetime,
time_zone,
activity_type,
distance_km,
duration_min,
activity_name,
)
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="create_activity_failed",
translation_placeholders={"error": str(err)},
) from err
async def handle_upload_activity(call: ServiceCall) -> None:
"""Handle upload_activity service call."""
coordinator = _get_coordinator(hass)
file_path = call.data.get("file_path")
if not await coordinator.async_login():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="login_failed",
)
# Check if file exists
import os
if not os.path.isfile(file_path):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="file_not_found",
translation_placeholders={"file_path": file_path},
)
try:
await hass.async_add_executor_job(
coordinator.api.upload_activity,
file_path,
)
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="upload_activity_failed",
translation_placeholders={"error": str(err)},
) from err
# Register services
hass.services.async_register(
DOMAIN,
@@ -162,8 +258,24 @@ async def async_setup_services(hass: HomeAssistant) -> None:
schema=ADD_BLOOD_PRESSURE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_ACTIVITY,
handle_create_activity,
schema=CREATE_ACTIVITY_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_UPLOAD_ACTIVITY,
handle_upload_activity,
schema=UPLOAD_ACTIVITY_SCHEMA,
)
async def async_unload_services(hass: HomeAssistant) -> None:
"""Unload Garmin Connect services."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_BODY_COMPOSITION)
hass.services.async_remove(DOMAIN, SERVICE_ADD_BLOOD_PRESSURE)
hass.services.async_remove(DOMAIN, SERVICE_CREATE_ACTIVITY)
hass.services.async_remove(DOMAIN, SERVICE_UPLOAD_ACTIVITY)

View File

@@ -223,5 +223,81 @@ add_blood_pressure:
description: Additional notes for the measurement.
required: false
example: "Measured with Beurer BC54"
selector:
text:
create_activity:
name: Create activity
description: Create an activity in Garmin Connect.
fields:
activity_name:
name: Activity name
description: Name of the activity.
required: true
example: "Morning Run"
selector:
text:
activity_type:
name: Activity type
description: Type of activity (e.g., running, cycling, walking).
required: true
example: running
default: running
selector:
select:
options:
- running
- cycling
- walking
- hiking
- swimming
- fitness_equipment
- other
start_datetime:
name: Start date and time
description: When the activity started (ISO format).
required: true
example: "2024-01-15T08:30:00"
selector:
text:
duration_min:
name: Duration
description: Duration of the activity in minutes.
required: true
example: 30
selector:
number:
min: 1
max: 1440
step: 1
unit_of_measurement: min
distance_km:
name: Distance
description: Distance covered in kilometers (optional).
required: false
example: 5.0
selector:
number:
min: 0
max: 1000
step: 0.1
unit_of_measurement: km
time_zone:
name: Time zone
description: Time zone for the activity (defaults to HA's configured timezone).
required: false
example: "Europe/Amsterdam"
selector:
text:
upload_activity:
name: Upload activity
description: Upload an activity file (FIT, GPX, TCX) to Garmin Connect.
fields:
file_path:
name: File path
description: Path to the activity file on the Home Assistant system.
required: true
example: "/config/activities/run.fit"
selector:
text:

View File

@@ -52,6 +52,15 @@
},
"integration_not_loaded": {
"message": "Garmin Connect integration not fully loaded"
},
"create_activity_failed": {
"message": "Failed to create activity: {error}"
},
"upload_activity_failed": {
"message": "Failed to upload activity: {error}"
},
"file_not_found": {
"message": "File not found: {file_path}"
}
}
}

View File

@@ -395,5 +395,34 @@
"name": "Device last synced"
}
}
},
"exceptions": {
"login_failed": {
"message": "Failed to login to Garmin Connect, unable to update"
},
"add_body_composition_failed": {
"message": "Failed to add body composition: {error}"
},
"add_blood_pressure_failed": {
"message": "Failed to add blood pressure: {error}"
},
"set_active_gear_failed": {
"message": "Failed to set active gear: {error}"
},
"no_integration_configured": {
"message": "No Garmin Connect integration configured"
},
"integration_not_loaded": {
"message": "Garmin Connect integration not fully loaded"
},
"create_activity_failed": {
"message": "Failed to create activity: {error}"
},
"upload_activity_failed": {
"message": "Failed to upload activity: {error}"
},
"file_not_found": {
"message": "File not found: {file_path}"
}
}
}

View File

@@ -154,6 +154,27 @@ Set a gear item as the default for an activity type.
| `activity_type` | Yes | Activity type (e.g., running, cycling) |
| `setting` | Yes | Setting option (set as default, unset default, set this as default unset others) |
### Create activity
Create an activity in Garmin Connect.
| Data attribute | Required | Description |
| ---------------------- | -------- | ----------- |
| `activity_name` | Yes | Name of the activity |
| `activity_type` | Yes | Activity type (e.g., running, cycling, walking) |
| `start_datetime` | Yes | Start time (ISO format) |
| `duration_min` | Yes | Duration in minutes |
| `distance_km` | No | Distance in kilometers |
| `time_zone` | No | Time zone (defaults to HA timezone) |
### Upload activity
Upload an activity file (FIT, GPX, TCX) to Garmin Connect.
| Data attribute | Required | Description |
| ---------------------- | -------- | ----------- |
| `file_path` | Yes | Path to activity file on HA system |
## Data updates
Data is polled from Garmin Connect every 5 minutes. Due to API rate limits, more frequent polling is not recommended.

View File

@@ -1,4 +1,5 @@
codespell==2.4.1
fit-tool==0.9.13
mypy==1.19.1
pre-commit==4.5.1
pre-commit-hooks==6.0.0