Compare commits

...

34 Commits
0.1.0 ... 0.2.1

Author SHA1 Message Date
Ron Klinkien
5155e90c8a Corrected cal to kcal for Netto Galorie Goal 2022-05-24 20:04:55 +02:00
Ron Klinkien
1844b50877 Fixed unit of measurement legacy code 2022-05-24 20:01:16 +02:00
Ron Klinkien
0241207443 Merge pull request #37 from wrt54g/main
Update HACS URL
2022-05-03 08:38:36 +02:00
Sven
eaec42124b Update HACS URL 2022-05-03 08:08:11 +02:00
Ron Klinkien
8dea9bfa4b Return correct results 2022-04-22 11:44:40 +02:00
Ron Klinkien
f13f59ae46 Don't use timestamp for nextalarm sensor 2022-04-22 09:43:30 +02:00
Ron Klinkien
e958d2af89 Remove title of step user 2022-04-11 11:08:18 +02:00
Ron Klinkien
8beb401e41 Remove title of step user 2022-04-11 11:07:38 +02:00
Ron Klinkien
4cd2826643 Update README.md 2022-01-05 10:23:20 +01:00
Ron Klinkien
6a5a7fbd34 Added statistics to sensors
Updated sensor code to newer standard
2021-12-30 11:34:38 +01:00
Ron Klinkien
30fe26751c Update manifest.json 2021-12-29 21:55:46 +01:00
Ron Klinkien
05adddd8e5 Update config_flow.py 2021-12-29 21:54:35 +01:00
Ron Klinkien
3fd9388170 Update __init__.py 2021-12-29 21:54:16 +01:00
Ron Klinkien
dd7cfc021f Update manifest.json 2021-12-29 13:09:07 +01:00
Ron Klinkien
154190f9e9 Update hacs.json 2021-12-24 16:41:58 +01:00
Ron Klinkien
e2d3a61c0e Merge pull request #22 from obbers/main
Remvove country from hacs.json
2021-12-24 16:41:21 +01:00
Ron Klinkien
47717f019c Bumped garminconnect-ha 2021-12-24 13:38:00 +01:00
Ron Klinkien
03f576207f Bumped garminconnect-ha to 0.1.16
Renamed bodyMass to boneMass
2021-12-24 12:37:16 +01:00
Gary Sinclair
db334c0761 Merge branch 'cyberjunky:main' into main 2021-12-23 14:35:08 -06:00
Ron Klinkien
78f5266a5f Bumped garminconnect-ha to 1.0.15 2021-12-23 20:55:53 +01:00
Gary Sinclair
7ce7bd2277 Remove Country from hacs.json 2021-12-08 08:40:03 -06:00
Ron Klinkien
0e3838df3d Return correct value when no alarm is set 2021-09-16 17:19:12 +02:00
Ron Klinkien
39fccd6630 Merge pull request #5 from Elgatross/patch-1
Update Readme
2021-08-19 20:00:12 +02:00
Elgatross
e97b40e3dd Update README.md 2021-08-12 18:02:22 +02:00
Elgatross
2cf26ada3c Update README.md
hint to add as custom repository, since you won't find it in the default one.
2021-08-12 04:47:28 +02:00
Ron Klinkien
ace2c3f7de Merge branch 'main' of https://github.com/cyberjunky/home-assistant-garmin_connect 2021-07-09 20:54:18 +02:00
Ron Klinkien
6c6a9dffd7 retrigger checks 2021-07-09 20:54:12 +02:00
Ron Klinkien
c740f36330 Update hassfest.yml 2021-07-09 20:54:02 +02:00
Ron Klinkien
c587d52e77 Merge branch 'main' of https://github.com/cyberjunky/home-assistant-garmin_connect 2021-07-09 20:47:55 +02:00
Ron Klinkien
1b4418b652 retrigger checks 2021-07-09 20:46:49 +02:00
Ron Klinkien
933d4a19bd Update hassfest.yml 2021-07-09 20:42:37 +02:00
Ron Klinkien
1560895361 Create hassfest.yml 2021-07-09 20:40:16 +02:00
Ron Klinkien
3c1b75e804 Implemented DataCoordinator 2021-07-08 21:20:20 +02:00
Ron Klinkien
7c5d03e6f4 Update manifest.json 2021-07-07 21:08:10 +02:00
10 changed files with 105 additions and 100 deletions

15
.github/workflows/hassfest.yml vendored Normal file
View 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

View File

@@ -1,13 +1,16 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/cyberjunkynl/)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](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.

View File

@@ -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,
}

View File

@@ -1,7 +1,7 @@
"""Config flow for Garmin Connect integration."""
import logging
from garminconnect_ha import (
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,

View File

@@ -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],
}

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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."
}
}
}

View File

@@ -15,9 +15,8 @@
"password": "Password",
"username": "Username"
},
"description": "Enter your credentials.",
"title": "Garmin Connect"
"description": "Enter your credentials."
}
}
}
}
}

View File

@@ -1,7 +1,5 @@
{
"name": "Garmin Connect",
"country": "NL",
"render_readme": true,
"domains": ["sensor"]
}
}