mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-07 20:13:57 -05:00
Added create- and upload activity services, more type checking
This commit is contained in:
24
README.md
24
README.md
@@ -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`:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user