Compare commits

...

48 Commits
0.1.0 ... 0.2.6

Author SHA1 Message Date
Ron
d118638429 Corrected units/device type for some body related sensors 2023-01-06 12:50:22 +01:00
Ron
3103929f5f Updated version and bumped garminconnect pkg version 2023-01-06 12:49:27 +01:00
Ron
842355d273 Added debug logging 2023-01-06 12:48:23 +01:00
Ron
e2728b6bc2 Update manifest.json 2023-01-04 20:08:21 +01:00
Ron
b75133a2d4 Fixed error with stress qualfier 2023-01-04 20:08:07 +01:00
Ron
ecbed70aee Update manifest.json 2023-01-04 19:43:06 +01:00
Ron
a761bc00a3 Update sensor.py 2023-01-04 19:42:46 +01:00
Ron
517bd5af5d Converted alarm time value to datetime 2023-01-04 19:42:20 +01:00
Ron
dc4e7301d8 Update manifest.json 2023-01-04 19:04:57 +01:00
Ron
e7ab9ef561 Update sensor.py 2023-01-04 19:04:04 +01:00
Ron
5438cd2d59 Updated version 2023-01-04 15:04:00 +01:00
Ron
4113550397 Implemented SensorDeviceClass 2023-01-04 15:03:23 +01:00
Ron
4b53fdb162 Implemented SensorDeviceClass 2023-01-04 15:02:47 +01:00
Ron Klinkien
6a1b5240d7 Update README.md 2022-05-24 21:19:04 +02:00
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 153 additions and 130 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,15 +1,19 @@
[![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.
- Inside HACS click 'Explore & download repositories'
- Search for 'Garmin Connect'.
- Click Install below the found integration.
- Click on found integration.
- Click Download this repository with HACS.
- Restart Home-Assistant.
- Follow configuration steps below.

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,68 @@ 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()
)
_LOGGER.debug(summary)
body = await self.hass.async_add_executor_job(
self._api.get_body_composition, date.today().isoformat()
)
_LOGGER.debug(body)
alarms = await self.hass.async_add_executor_job(self._api.get_device_alarms)
_LOGGER.debug(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

@@ -2,16 +2,15 @@
from datetime import timedelta
from homeassistant.const import (
DEVICE_CLASS_TIMESTAMP,
LENGTH_METERS,
MASS_KILOGRAMS,
PERCENTAGE,
TIME_MINUTES,
TIME_YEARS,
)
from homeassistant.components.sensor import SensorDeviceClass
DOMAIN = "garmin_connect"
ATTRIBUTION = "connect.garmin.com"
DATA_COORDINATOR = "coordinator"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
@@ -37,26 +36,26 @@ 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,
"mdi:walk",
None,
SensorDeviceClass.DISTANCE,
True,
],
"wellnessStartTimeLocal": [
"Wellness Start Time",
None,
"mdi:clock",
DEVICE_CLASS_TIMESTAMP,
SensorDeviceClass.TIMESTAMP,
False,
],
"wellnessEndTimeLocal": [
"Wellness End Time",
None,
"mdi:clock",
DEVICE_CLASS_TIMESTAMP,
SensorDeviceClass.TIMESTAMP,
False,
],
"wellnessDescription": ["Wellness Description", "", "mdi:clock", None, False],
@@ -64,7 +63,7 @@ GARMIN_ENTITY_LIST = {
"Wellness Distance Mtr",
LENGTH_METERS,
"mdi:walk",
None,
SensorDeviceClass.DISTANCE,
False,
],
"wellnessActiveKilocalories": [
@@ -103,14 +102,14 @@ GARMIN_ENTITY_LIST = {
"Floors Ascended Mtr",
LENGTH_METERS,
"mdi:stairs",
None,
SensorDeviceClass.DISTANCE,
False,
],
"floorsDescendedInMeters": [
"Floors Descended Mtr",
LENGTH_METERS,
"mdi:stairs",
None,
SensorDeviceClass.DISTANCE,
False,
],
"floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, True],
@@ -198,49 +197,49 @@ GARMIN_ENTITY_LIST = {
"Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"restStressPercentage": [
"Rest Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"activityStressPercentage": [
"Activity Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"uncategorizedStressPercentage": [
"Uncat. Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"lowStressPercentage": [
"Low Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"mediumStressPercentage": [
"Medium Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"highStressPercentage": [
"High Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorDeviceClass.BATTERY,
False,
],
"moderateIntensityMinutes": [
@@ -268,35 +267,35 @@ GARMIN_ENTITY_LIST = {
"Body Battery Charged",
PERCENTAGE,
"mdi:battery-charging-100",
None,
SensorDeviceClass.BATTERY,
True,
],
"bodyBatteryDrainedValue": [
"Body Battery Drained",
PERCENTAGE,
"mdi:battery-alert-variant-outline",
None,
SensorDeviceClass.BATTERY,
True,
],
"bodyBatteryHighestValue": [
"Body Battery Highest",
PERCENTAGE,
"mdi:battery-heart",
None,
SensorDeviceClass.BATTERY,
True,
],
"bodyBatteryLowestValue": [
"Body Battery Lowest",
PERCENTAGE,
"mdi:battery-heart-outline",
None,
SensorDeviceClass.BATTERY,
True,
],
"bodyBatteryMostRecentValue": [
"Body Battery Most Recent",
PERCENTAGE,
"mdi:battery-positive",
None,
SensorDeviceClass.BATTERY,
True,
],
"averageSpo2": ["Average SPO2", PERCENTAGE, "mdi:diabetes", None, True],
@@ -306,7 +305,7 @@ GARMIN_ENTITY_LIST = {
"Latest SPO2 Time",
None,
"mdi:diabetes",
DEVICE_CLASS_TIMESTAMP,
SensorDeviceClass.TIMESTAMP,
False,
],
"averageMonitoringEnvironmentAltitude": [
@@ -341,17 +340,17 @@ GARMIN_ENTITY_LIST = {
"Latest Respiration Update",
None,
"mdi:progress-clock",
DEVICE_CLASS_TIMESTAMP,
SensorDeviceClass.TIMESTAMP,
False,
],
"weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", None, False],
"weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", SensorDeviceClass.WEIGHT, False],
"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],
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", None, False],
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", SensorDeviceClass.BATTERY, False],
"bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", SensorDeviceClass.BATTERY, False],
"boneMass": ["Bone Mass", MASS_KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, False],
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", SensorDeviceClass.WEIGHT, False],
"physiqueRating": ["Physique Rating", None, "mdi:numeric", None, False],
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", None, False],
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", SensorDeviceClass.BATTERY, 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", SensorDeviceClass.TIMESTAMP, 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.50"],
"codeowners": ["@cyberjunky"],
"config_flow": true,
"iot_class": "cloud_polling",
"version": "0.1.0"
"version": "0.2.6"
}

View File

@@ -2,10 +2,16 @@
from __future__ import annotations
import logging
import datetime
import pytz
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
SensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID, DEVICE_CLASS_TIMESTAMP
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
@@ -15,7 +21,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 +88,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 +114,23 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
self.coordinator.data[self._type]
)
if active_alarms:
value = active_alarms[0]
date_time_obj = datetime.datetime.strptime(active_alarms[0], "%Y-%m-%dT%H:%M:%S")
timezone = pytz.timezone('UTC')
timezone_date_time_obj = timezone.localize(date_time_obj)
return timezone_date_time_obj
else:
return None
elif self._type == "stressQualifier":
return value
if self._device_class == DEVICE_CLASS_TIMESTAMP:
return value
if self._device_class == SensorDeviceClass.TIMESTAMP:
date_time_obj = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
timezone = pytz.timezone('UTC')
timezone_date_time_obj = timezone.localize(date_time_obj)
return timezone_date_time_obj
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 +138,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 +169,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"]
}
}