mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-09 12:57:58 -05:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5155e90c8a | ||
|
|
1844b50877 | ||
|
|
0241207443 | ||
|
|
eaec42124b | ||
|
|
8dea9bfa4b | ||
|
|
f13f59ae46 | ||
|
|
e958d2af89 | ||
|
|
8beb401e41 | ||
|
|
4cd2826643 | ||
|
|
6a5a7fbd34 | ||
|
|
30fe26751c | ||
|
|
05adddd8e5 | ||
|
|
3fd9388170 | ||
|
|
dd7cfc021f | ||
|
|
154190f9e9 | ||
|
|
e2d3a61c0e | ||
|
|
47717f019c | ||
|
|
03f576207f | ||
|
|
db334c0761 | ||
|
|
78f5266a5f | ||
|
|
7ce7bd2277 | ||
|
|
0e3838df3d | ||
|
|
39fccd6630 | ||
|
|
e97b40e3dd | ||
|
|
2cf26ada3c | ||
|
|
ace2c3f7de | ||
|
|
6c6a9dffd7 | ||
|
|
c740f36330 | ||
|
|
c587d52e77 | ||
|
|
1b4418b652 | ||
|
|
933d4a19bd | ||
|
|
1560895361 | ||
|
|
3c1b75e804 | ||
|
|
7c5d03e6f4 |
15
.github/workflows/hassfest.yml
vendored
Normal file
15
.github/workflows/hassfest.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
name: Validate with hassfest
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v2"
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
@@ -1,13 +1,16 @@
|
||||
[](https://github.com/custom-components/hacs) [](https://www.python.org/) [](https://www.paypal.me/cyberjunkynl/)
|
||||
[](https://github.com/hacs/integration) [](https://www.python.org/) [](https://www.paypal.me/cyberjunkynl/)
|
||||
|
||||
# Garmin Connect
|
||||
The Garmin Connect integration allows you to expose data from Garmin Connect to Home Assistant.
|
||||
|
||||
NOTE: This integration doesn't support 2FA on Garmin Connect, so if you have enabled it -and want to keep it- this integration doesn't work, it will try to login repeatedly and generate lots of 2FA codes via email.
|
||||
The change of adding support for it is unlikely since the Garmin Connect API is closed source, and will not be open for open-sourced projects.
|
||||
|
||||
## Installation
|
||||
|
||||
### HACS - Recommended
|
||||
- Have [HACS](https://hacs.xyz) installed, this will allow you to easily manage and track updates.
|
||||
- Add https://github.com/cyberjunky/home-assistant-garmin_connect to custom repositories in HACS
|
||||
- Search for 'Garmin Connect'.
|
||||
- Click Install below the found integration.
|
||||
- Restart Home-Assistant.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from datetime import date
|
||||
import logging
|
||||
|
||||
from garminconnect_ha import (
|
||||
from garminconnect import (
|
||||
Garmin,
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectConnectionError,
|
||||
@@ -25,38 +25,11 @@ PLATFORMS = ["sensor"]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Garmin Connect from a config entry."""
|
||||
|
||||
username: str = entry.data[CONF_USERNAME]
|
||||
password: str = entry.data[CONF_PASSWORD]
|
||||
coordinator = GarminConnectDataUpdateCoordinator(hass, entry=entry)
|
||||
|
||||
api = Garmin(username, password)
|
||||
try:
|
||||
await hass.async_add_executor_job(api.login)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
) as err:
|
||||
_LOGGER.error("Error occurred during Garmin Connect login request: %s", err)
|
||||
return False
|
||||
except (GarminConnectConnectionError) as err:
|
||||
_LOGGER.error(
|
||||
"Connection error occurred during Garmin Connect login request: %s", err
|
||||
)
|
||||
raise ConfigEntryNotReady from err
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error occurred during Garmin Connect login request")
|
||||
if not await coordinator.async_login():
|
||||
return False
|
||||
|
||||
async def async_update_data():
|
||||
_LOGGER.debug("Updating data for %s", username)
|
||||
return await async_update_garmin_data(hass, api)
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=username,
|
||||
update_method=async_update_data,
|
||||
update_interval=DEFAULT_UPDATE_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
@@ -77,25 +50,65 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_update_garmin_data(hass, api):
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
summary = await hass.async_add_executor_job(
|
||||
api.get_user_summary, date.today().isoformat()
|
||||
)
|
||||
body = await hass.async_add_executor_job(
|
||||
api.get_body_composition, date.today().isoformat()
|
||||
)
|
||||
alarms = await hass.async_add_executor_job(api.get_device_alarms)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
GarminConnectConnectionError,
|
||||
) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Garmin Connect Data Update Coordinator."""
|
||||
|
||||
return {
|
||||
**summary,
|
||||
**body["totalAverage"],
|
||||
"nextAlarm": alarms,
|
||||
}
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize the Garmin Connect hub."""
|
||||
self.entry = entry
|
||||
self.hass = hass
|
||||
|
||||
self._api = Garmin(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
|
||||
)
|
||||
|
||||
async def async_login(self) -> bool:
|
||||
"""Login to Garmin Connect."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self._api.login)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
) as err:
|
||||
_LOGGER.error("Error occurred during Garmin Connect login request: %s", err)
|
||||
return False
|
||||
except (GarminConnectConnectionError) as err:
|
||||
_LOGGER.error(
|
||||
"Connection error occurred during Garmin Connect login request: %s", err
|
||||
)
|
||||
raise ConfigEntryNotReady from err
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Unknown error occurred during Garmin Connect login request"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch data from Garmin Connect."""
|
||||
try:
|
||||
summary = await self.hass.async_add_executor_job(
|
||||
self._api.get_user_summary, date.today().isoformat()
|
||||
)
|
||||
body = await self.hass.async_add_executor_job(
|
||||
self._api.get_body_composition, date.today().isoformat()
|
||||
)
|
||||
alarms = await self.hass.async_add_executor_job(self._api.get_device_alarms)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
GarminConnectConnectionError,
|
||||
) as error:
|
||||
_LOGGER.debug("Trying to relogin to Garmin Connect")
|
||||
if not await self.async_login():
|
||||
raise UpdateFailed(error) from error
|
||||
return {}
|
||||
|
||||
return {
|
||||
**summary,
|
||||
**body["totalAverage"],
|
||||
"nextAlarm": alarms,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for Garmin Connect integration."""
|
||||
import logging
|
||||
|
||||
from garminconnect_ha import (
|
||||
from garminconnect import (
|
||||
Garmin,
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectConnectionError,
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
DOMAIN = "garmin_connect"
|
||||
ATTRIBUTION = "connect.garmin.com"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
@@ -37,7 +36,7 @@ GARMIN_ENTITY_LIST = {
|
||||
None,
|
||||
False,
|
||||
],
|
||||
"netCalorieGoal": ["Net Calorie Goal", "cal", "mdi:food", None, False],
|
||||
"netCalorieGoal": ["Net Calorie Goal", "kcal", "mdi:food", None, False],
|
||||
"totalDistanceMeters": [
|
||||
"Total Distance Mtr",
|
||||
LENGTH_METERS,
|
||||
@@ -348,10 +347,10 @@ GARMIN_ENTITY_LIST = {
|
||||
"bmi": ["BMI", "bmi", "mdi:food", None, False],
|
||||
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, False],
|
||||
"bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, False],
|
||||
"bodyMass": ["Body Mass", MASS_KILOGRAMS, "mdi:food", None, False],
|
||||
"boneMass": ["Bone Mass", MASS_KILOGRAMS, "mdi:bone", None, False],
|
||||
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", None, False],
|
||||
"physiqueRating": ["Physique Rating", None, "mdi:numeric", None, False],
|
||||
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", None, False],
|
||||
"metabolicAge": ["Metabolic Age", TIME_YEARS, "mdi:calendar-heart", None, False],
|
||||
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True],
|
||||
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", None, True],
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
"domain": "garmin_connect",
|
||||
"name": "Garmin Connect",
|
||||
"documentation": "https://github.com/cyberjunky/home-assistant-garmin_connect",
|
||||
"requirements": ["garminconnect_ha==0.1.12"],
|
||||
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
|
||||
"requirements": ["garminconnect==0.1.24"],
|
||||
"codeowners": ["@cyberjunky"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.8"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import SensorEntity, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID, DEVICE_CLASS_TIMESTAMP
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import (
|
||||
|
||||
from .alarm_util import calculate_next_active_alarms
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN as GARMIN_DOMAIN,
|
||||
GARMIN_ENTITY_LIST,
|
||||
@@ -83,24 +82,18 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
|
||||
self._unique_id = unique_id
|
||||
self._type = sensor_type
|
||||
self._name = name
|
||||
self._unit = unit
|
||||
self._icon = icon
|
||||
self._device_class = device_class
|
||||
self._enabled_default = enabled_default
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
self._attr_name = name
|
||||
self._attr_device_class = self._device_class
|
||||
self._attr_icon = icon
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._attr_unique_id = f"{self._unique_id}_{self._type}"
|
||||
self._attr_state_class = SensorStateClass.TOTAL
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
if not self.coordinator.data or not self.coordinator.data[self._type]:
|
||||
return None
|
||||
@@ -115,23 +108,15 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
self.coordinator.data[self._type]
|
||||
)
|
||||
if active_alarms:
|
||||
value = active_alarms[0]
|
||||
return active_alarms[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
if self._device_class == DEVICE_CLASS_TIMESTAMP:
|
||||
return value
|
||||
|
||||
return round(value, 2)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return f"{self._unique_id}_{self._type}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return attributes for sensor."""
|
||||
@@ -139,9 +124,7 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
return {}
|
||||
|
||||
attributes = {
|
||||
"source": self.coordinator.data["source"],
|
||||
"last_synced": self.coordinator.data["lastSyncTimestampGMT"],
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
}
|
||||
if self._type == "nextAlarm":
|
||||
attributes["next_alarms"] = calculate_next_active_alarms(
|
||||
@@ -172,8 +155,3 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
and self.coordinator.data
|
||||
and self._type in self.coordinator.data
|
||||
)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "Enter your credentials.",
|
||||
"title": "Garmin Connect"
|
||||
"description": "Enter your credentials."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Enter your credentials.",
|
||||
"title": "Garmin Connect"
|
||||
"description": "Enter your credentials."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user