mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-08 20:38:00 -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
|
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
|
### Enable Debug Logging
|
||||||
|
|
||||||
Add the relevant lines below to the `configuration.yaml`:
|
Add the relevant lines below to the `configuration.yaml`:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
"""Services for Garmin Connect integration."""
|
"""Services for Garmin Connect integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
@@ -9,11 +12,15 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .coordinator import GarminConnectDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Service schemas
|
# Service schemas
|
||||||
SERVICE_ADD_BODY_COMPOSITION = "add_body_composition"
|
SERVICE_ADD_BODY_COMPOSITION = "add_body_composition"
|
||||||
SERVICE_ADD_BLOOD_PRESSURE = "add_blood_pressure"
|
SERVICE_ADD_BLOOD_PRESSURE = "add_blood_pressure"
|
||||||
|
SERVICE_CREATE_ACTIVITY = "create_activity"
|
||||||
|
|
||||||
ADD_BODY_COMPOSITION_SCHEMA = vol.Schema(
|
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."""
|
"""Get the first available coordinator from config entries."""
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
if not entries:
|
if not entries:
|
||||||
@@ -61,7 +86,7 @@ def _get_coordinator(hass: HomeAssistant):
|
|||||||
translation_key="integration_not_loaded",
|
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:
|
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)},
|
translation_placeholders={"error": str(err)},
|
||||||
) from 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
|
# Register services
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -162,8 +258,24 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
schema=ADD_BLOOD_PRESSURE_SCHEMA,
|
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:
|
async def async_unload_services(hass: HomeAssistant) -> None:
|
||||||
"""Unload Garmin Connect services."""
|
"""Unload Garmin Connect services."""
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_BODY_COMPOSITION)
|
hass.services.async_remove(DOMAIN, SERVICE_ADD_BODY_COMPOSITION)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_BLOOD_PRESSURE)
|
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.
|
description: Additional notes for the measurement.
|
||||||
required: false
|
required: false
|
||||||
example: "Measured with Beurer BC54"
|
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:
|
selector:
|
||||||
text:
|
text:
|
||||||
@@ -52,6 +52,15 @@
|
|||||||
},
|
},
|
||||||
"integration_not_loaded": {
|
"integration_not_loaded": {
|
||||||
"message": "Garmin Connect integration not fully 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"
|
"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) |
|
| `activity_type` | Yes | Activity type (e.g., running, cycling) |
|
||||||
| `setting` | Yes | Setting option (set as default, unset default, set this as default unset others) |
|
| `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 updates
|
||||||
|
|
||||||
Data is polled from Garmin Connect every 5 minutes. Due to API rate limits, more frequent polling is not recommended.
|
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
|
codespell==2.4.1
|
||||||
|
fit-tool==0.9.13
|
||||||
mypy==1.19.1
|
mypy==1.19.1
|
||||||
pre-commit==4.5.1
|
pre-commit==4.5.1
|
||||||
pre-commit-hooks==6.0.0
|
pre-commit-hooks==6.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user