Compare commits

...

275 Commits

Author SHA1 Message Date
Ron Klinkien
63559704c9 Updated README 2026-01-04 15:29:52 +01:00
Ron Klinkien
3a7fb30cdf Added goal sensors, fixed polyline card 2026-01-04 15:29:41 +01:00
Ron Klinkien
89ce3e7247 Bumped python-garminconnect, added polylines to activity, created map card 2026-01-04 14:17:40 +01:00
Ron Klinkien
e713b01846 Added morning training readiness, fallback midnight 2026-01-04 12:04:55 +01:00
Ron Klinkien
e2fc279d89 Added Training Readiness sensor Training Status sensor Lactate Threshold HR sensor and Lactate Threshold Speed sensor 2026-01-03 19:28:06 +01:00
Ron Klinkien
cd520530db Added last workout and last workouts sensors 2026-01-03 18:49:43 +01:00
Ron Klinkien
dc45503bd5 Add daily total run/cycle distance example to README 2026-01-03 18:21:39 +01:00
Ron Klinkien
a81d4f955d Added add gear to activity service, added gear_uuid attribute to gear sensors 2026-01-03 18:03:35 +01:00
Ron Klinkien
9ea284bf95 Fixed last activity attribute too big issue 2026-01-03 17:17:45 +01:00
Ron Klinkien
f8a9a0d61c Added create- and upload activity services, more type checking 2026-01-03 17:02:32 +01:00
Ron Klinkien
ef9c1efe27 Preserve more values, remove debug lines 2026-01-03 14:19:57 +01:00
Ron Klinkien
28ad661ab7 Added blood pressure sensors 2026-01-03 13:22:25 +01:00
Ron Klinkien
57fb7ecfcd More improvements 2026-01-03 11:30:06 +01:00
Ron Klinkien
9edefcd828 Fixed syntax 2026-01-03 11:25:31 +01:00
Ron Klinkien
039aae3e9f On the road to get gold quality scale, refactored services/actions 2026-01-03 11:20:23 +01:00
Ron Klinkien
ecc40ce127 Removed lastupdated attributes, created diag sensor, added endurance score, fixed midnight timegap (test) 2026-01-03 10:03:49 +01:00
Ron Klinkien
10cc9608b9 Added HRV and weekly steps sensors 2026-01-02 19:09:03 +01:00
Ron Klinkien
d45400e3bc Retain data overnight for certain sensors 2026-01-02 17:14:56 +01:00
Ron Klinkien
2d7aa0a9bc Added menstrual sensors 2026-01-02 16:53:26 +01:00
Ron Klinkien
e20ae8f8b5 Integers display as integers 2026-01-02 15:55:04 +01:00
Ron Klinkien
d7cba144ca More testing, fixes 2026-01-02 15:44:40 +01:00
Ron Klinkien
d9acf3f820 Added tests, extensive linting 2026-01-02 14:44:32 +01:00
Ron Klinkien
6478a34cbe Fixed wrong naming 2026-01-02 13:12:28 +01:00
Ron Klinkien
cad8247422 v1.0.0 - HA Core Compliance Release 2026-01-02 13:05:05 +01:00
Ron Klinkien
9f9ba069f4 Fixed deprecations issue 2025-11-09 11:25:52 +00:00
Ron Klinkien
a34dd7abfc Fix KeyError for userProfileId and add reload support
- Fix KeyError when userProfileId is missing from summary after HA updates
- Check if userProfileId exists before accessing it in 3 locations
- Remove duplicate get_gear() call, reuse data from first fetch
- Add async_reload_entry() function for reload without restart
- Update README with reload instructions
- Improves integration reliability and user experience
2025-11-06 15:49:58 +01:00
Ron Klinkien
c73065a7b4 Better migration 2025-11-05 08:56:22 +00:00
Ron Klinkien
b1e8953393 Fix enable/disable of entitites 2025-11-04 21:09:32 +00:00
Ron Klinkien
a2aebe7790 Sections 2025-11-04 20:49:28 +00:00
Ron Klinkien
2899837881 New options 2025-11-04 21:08:04 +01:00
Ron Klinkien
1776cb4037 More robust config flow when integration is reinstalled 2025-11-04 18:03:35 +00:00
Ron Klinkien
f26e78de04 Bumped version 2025-11-04 17:38:48 +00:00
Ron Klinkien
8d00693332 Fixed config migration issue 2025-11-04 17:37:43 +00:00
Ron
79dec62b81 Add example for Withings to Garmin integration
Added a full example for the Withings to Garmin integration, detailing triggers, conditions, and actions.
2025-11-04 17:53:37 +01:00
Ron
037d7e70c5 Enhance donation and support information in README
Updated donation section with new support options and badges.
2025-11-04 17:41:43 +01:00
Ron Klinkien
cb1422c22b Bumped python-garminconnect 2025-11-04 16:36:56 +00:00
Ron Klinkien
dc5151c641 Fixed syntax issues 2025-11-04 15:59:46 +01:00
Ron Klinkien
1edb3c91b6 Fixed another syntax error 2025-11-04 15:50:38 +01:00
Ron Klinkien
68a9cf3e10 Fixed syntax error 2025-11-04 15:47:17 +01:00
Ron
26dd2143be Add Endurance Score to README 2025-11-04 15:44:20 +01:00
Ron
17fc91d968 Merge pull request #352 from jfparis/endurance
Added the endurance score as an optional entity
2025-11-04 15:42:43 +01:00
Ron
014b99a3b3 Merge branch 'main' into endurance 2025-11-04 15:41:40 +01:00
Ron Klinkien
0f15469f58 Bumped home assistant version 2025-11-04 15:32:33 +01:00
Ron
925ab448ef Merge pull request #353 from cyberjunky/dependabot/pip/ruff-0.14.3
Bump ruff from 0.12.1 to 0.14.3
2025-11-04 15:19:31 +01:00
Ron
8ae83a4ce0 Merge branch 'main' into dependabot/pip/ruff-0.14.3 2025-11-04 15:19:24 +01:00
Ron
425906d3e1 Merge pull request #350 from cyberjunky/dependabot/pip/homeassistant-2025.10.4
Bump homeassistant from 2025.3.3 to 2025.10.4
2025-11-04 15:18:51 +01:00
Ron
88e7d66345 Merge pull request #349 from cyberjunky/dependabot/pip/colorlog-6.10.1
Bump colorlog from 6.9.0 to 6.10.1
2025-11-04 15:18:38 +01:00
Ron
a3e7261650 Merge pull request #345 from cyberjunky/dependabot/pip/pyupgrade-3.21.0
Bump pyupgrade from 3.20.0 to 3.21.0
2025-11-04 15:18:26 +01:00
dependabot[bot]
f90dfaaccc Bump pyupgrade from 3.20.0 to 3.21.0
Bumps [pyupgrade](https://github.com/asottile/pyupgrade) from 3.20.0 to 3.21.0.
- [Commits](https://github.com/asottile/pyupgrade/compare/v3.20.0...v3.21.0)

---
updated-dependencies:
- dependency-name: pyupgrade
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 14:17:40 +00:00
Ron
fd26ec0a29 Merge pull request #344 from cyberjunky/dependabot/pip/isort-7.0.0
Bump isort from 6.0.1 to 7.0.0
2025-11-04 15:17:39 +01:00
dependabot[bot]
c4892306f0 Bump ruff from 0.12.1 to 0.14.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.1 to 0.14.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.12.1...0.14.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 14:16:51 +00:00
Ron
5d721ce9f0 Merge pull request #331 from cyberjunky/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-11-04 15:16:50 +01:00
dependabot[bot]
bc0d248fbb Bump isort from 6.0.1 to 7.0.0
Bumps [isort](https://github.com/PyCQA/isort) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/6.0.1...7.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 14:16:48 +00:00
Ron
bd2dcd075d Merge pull request #328 from cyberjunky/dependabot/pip/pre-commit-hooks-6.0.0
Bump pre-commit-hooks from 5.0.0 to 6.0.0
2025-11-04 15:16:32 +01:00
Ron
a606d8bd74 Merge branch 'main' into dependabot/pip/pre-commit-hooks-6.0.0 2025-11-04 15:15:58 +01:00
Ron
8a37761560 Merge pull request #326 from cyberjunky/dependabot/pip/pre-commit-4.3.0
Bump pre-commit from 4.2.0 to 4.3.0
2025-11-04 15:14:53 +01:00
Ron
b47c80ecc9 Merge pull request #306 from cyberjunky/mfa
Added support for MFA
2025-11-04 15:14:24 +01:00
Jean-François Paris
359eaea923 endurance score: fix bug when garmin api returns blank 2025-10-30 14:22:35 +00:00
Jean-François Paris
94e1416e5d Added the endurance score as an optional entity 2025-10-27 21:24:11 +00:00
dependabot[bot]
0d0abbbc2d Bump homeassistant from 2025.3.3 to 2025.10.4
Bumps [homeassistant](https://github.com/home-assistant/core) from 2025.3.3 to 2025.10.4.
- [Release notes](https://github.com/home-assistant/core/releases)
- [Commits](https://github.com/home-assistant/core/compare/2025.3.3...2025.10.4)

---
updated-dependencies:
- dependency-name: homeassistant
  dependency-version: 2025.10.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 06:21:57 +00:00
dependabot[bot]
fb44c2cd1b Bump colorlog from 6.9.0 to 6.10.1
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.9.0 to 6.10.1.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.9.0...v6.10.1)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-version: 6.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 06:17:56 +00:00
dependabot[bot]
0071642d77 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 08:50:10 +00:00
dependabot[bot]
42a56b73a3 Bump pre-commit-hooks from 5.0.0 to 6.0.0
Bumps [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/pre-commit/pre-commit-hooks/releases)
- [Changelog](https://github.com/pre-commit/pre-commit-hooks/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: pre-commit-hooks
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 10:14:36 +00:00
dependabot[bot]
65d1e99d88 Bump pre-commit from 4.2.0 to 4.3.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.2.0...v4.3.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 09:33:35 +00:00
Ron
fdcdc01b7b Merge pull request #313 from cyberjunky/dependabot/pip/ruff-0.12.1
Bump ruff from 0.12.0 to 0.12.1
2025-07-03 11:49:00 +02:00
dependabot[bot]
7b3a0bc970 Bump ruff from 0.12.0 to 0.12.1
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.12.0...0.12.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 07:41:13 +00:00
Ron
9ed3f37762 Merge pull request #312 from cyberjunky/coderabbitai/docstrings/u59f
📝 Add docstrings to `mfa`
2025-06-23 13:28:48 +02:00
Ron
5cf1038c35 Update README.md 2025-06-23 13:28:02 +02:00
coderabbitai[bot]
d6dc1198e4 📝 Add docstrings to mfa
Docstrings generation was requested by @cyberjunky.

* https://github.com/cyberjunky/home-assistant-garmin_connect/pull/306#issuecomment-2943501760

The following files were modified:

* `custom_components/garmin_connect/__init__.py`
* `custom_components/garmin_connect/config_flow.py`
* `custom_components/garmin_connect/sensor.py`
2025-06-23 11:21:32 +00:00
Ron
bca3360261 Update README.md 2025-06-23 13:14:36 +02:00
Ron
ca1acbc9f4 Merge pull request #311 from cyberjunky/dependabot/pip/ruff-0.12.0
Bump ruff from 0.11.12 to 0.12.0
2025-06-23 12:40:43 +02:00
dependabot[bot]
4c3cd3e8d3 Bump ruff from 0.11.12 to 0.12.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.12 to 0.12.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.12...0.12.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 07:16:37 +00:00
Ron Klinkien
799ad51305 Fixed return types 2025-06-05 11:12:59 +00:00
Ron
0a7fb1b9fa Merge pull request #307 from cyberjunky/coderabbitai/docstrings/u59f
📝 Add docstrings to `mfa`
2025-06-05 13:05:32 +02:00
Ron Klinkien
dc5bf13660 Fixed indentation in config_flow.py 2025-06-05 11:03:53 +00:00
coderabbitai[bot]
9a83f6aca8 📝 Add docstrings to mfa
Docstrings generation was requested by @cyberjunky.

* https://github.com/cyberjunky/home-assistant-garmin_connect/pull/306#issuecomment-2943501760

The following files were modified:

* `custom_components/garmin_connect/__init__.py`
* `custom_components/garmin_connect/config_flow.py`
* `custom_components/garmin_connect/sensor.py`
2025-06-05 10:54:20 +00:00
Ron
d0e7a15d35 Merge branch 'main' into mfa 2025-06-05 12:53:36 +02:00
Ron
509ee86bc1 Update custom_components/garmin_connect/config_flow.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-05 12:08:43 +02:00
Ron
a5632b4650 Update custom_components/garmin_connect/config_flow.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-05 12:08:24 +02:00
Ron
13e00af7a0 Update custom_components/garmin_connect/__init__.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-05 12:08:12 +02:00
Ron
8ba2734c9f Merge pull request #304 from cyberjunky/dependabot/pip/ruff-0.11.12
Bump ruff from 0.11.5 to 0.11.12
2025-06-05 11:46:43 +02:00
Ron
d0d519b738 Merge pull request #303 from cyberjunky/dependabot/pip/setuptools-80.9.0
Bump setuptools from 78.1.0 to 80.9.0
2025-06-05 11:46:32 +02:00
dependabot[bot]
c8592e9e43 Bump setuptools from 78.1.0 to 80.9.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 78.1.0 to 80.9.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v78.1.0...v80.9.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.9.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 09:45:43 +00:00
dependabot[bot]
57ffa3f47b Bump ruff from 0.11.5 to 0.11.12
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.5 to 0.11.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.5...0.11.12)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 09:45:40 +00:00
Ron
4edc2993bf Merge pull request #300 from cyberjunky/dependabot/pip/pyupgrade-3.20.0
Bump pyupgrade from 3.19.1 to 3.20.0
2025-06-05 11:44:42 +02:00
Ron
015110f5e6 Merge pull request #298 from cyberjunky/dependabot/pip/pip-297a1d6f26
Bump setuptools from 78.1.0 to 78.1.1 in the pip group
2025-06-05 11:44:33 +02:00
Ron Klinkien
d25506e3d2 Bumped garminconnect to 0.2.27 2025-06-05 09:43:31 +00:00
dependabot[bot]
6c0f7eba64 Bump pyupgrade from 3.19.1 to 3.20.0
Bumps [pyupgrade](https://github.com/asottile/pyupgrade) from 3.19.1 to 3.20.0.
- [Commits](https://github.com/asottile/pyupgrade/compare/v3.19.1...v3.20.0)

---
updated-dependencies:
- dependency-name: pyupgrade
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 06:34:01 +00:00
dependabot[bot]
21204bed29 Bump setuptools from 78.1.0 to 78.1.1 in the pip group
Bumps the pip group with 1 update: [setuptools](https://github.com/pypa/setuptools).


Updates `setuptools` from 78.1.0 to 78.1.1
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v78.1.0...v78.1.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.1
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-20 01:42:15 +00:00
Ron Klinkien
9cf1b3a3bb Migrate from old config flow to new config flow
Catch authentication errors and show them in the UI
Limit last activity to 5 in attributes
2025-04-19 10:38:01 +00:00
Ron Klinkien
84ad53891b Added new sensors to README.md 2025-04-18 18:31:07 +00:00
Ron Klinkien
4163be285f Added hydration sensors 2025-04-18 18:30:04 +00:00
Ron
ee5df999a0 Update README.md 2025-04-18 20:09:58 +02:00
Ron Klinkien
a179131a94 Added Age sensors
Only store last 10 activities in attribute
2025-04-18 18:06:12 +00:00
Ron
9d57849ced Merge pull request #281 from cyberjunky/dependabot/pip/ruff-0.11.5
Bump ruff from 0.11.4 to 0.11.5
2025-04-18 17:44:35 +02:00
Ron
4a9901958e Update README.md 2025-04-18 17:26:12 +02:00
Ron Klinkien
7c119b9fe5 First beta version with MFA support. 2025-04-18 15:16:59 +00:00
dependabot[bot]
89ea628936 Bump ruff from 0.11.4 to 0.11.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.4 to 0.11.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.4...0.11.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 06:10:16 +00:00
Ron
8e0a27d9b7 Merge pull request #279 from cyberjunky/dependabot/pip/ruff-0.11.4
Bump ruff from 0.11.2 to 0.11.4
2025-04-08 15:58:25 +02:00
dependabot[bot]
83840b8829 Bump ruff from 0.11.2 to 0.11.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.2 to 0.11.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.2...0.11.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 07:13:47 +00:00
Ron
b41cf2c007 Merge pull request #265 from cyberjunky/dependabot/pip/isort-6.0.1
Bump isort from 6.0.0 to 6.0.1
2025-04-04 09:32:31 +02:00
Ron
01981a0e9b Merge branch 'main' into dependabot/pip/isort-6.0.1 2025-04-04 09:32:24 +02:00
Ron
107c0af24f Merge pull request #272 from cyberjunky/dependabot/pip/homeassistant-2025.3.3
Bump homeassistant from 2025.2.1 to 2025.3.3
2025-04-04 09:31:45 +02:00
Ron
401d2e6b59 Merge pull request #274 from cyberjunky/dependabot/pip/ruff-0.11.2
Bump ruff from 0.9.5 to 0.11.2
2025-04-04 09:31:34 +02:00
Ron
0e22122c00 Merge pull request #275 from cyberjunky/dependabot/pip/pre-commit-4.2.0
Bump pre-commit from 4.1.0 to 4.2.0
2025-04-04 09:31:24 +02:00
Ron
6cb7e46d0e Merge pull request #277 from cyberjunky/dependabot/pip/setuptools-78.1.0
Bump setuptools from 75.8.0 to 78.1.0
2025-04-04 09:31:12 +02:00
dependabot[bot]
c357da5a81 Bump setuptools from 75.8.0 to 78.1.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 75.8.0 to 78.1.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v75.8.0...v78.1.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 06:48:22 +00:00
dependabot[bot]
680d95bddc Bump ruff from 0.9.5 to 0.11.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.5 to 0.11.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.5...0.11.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 06:54:46 +00:00
dependabot[bot]
36bbd2f891 Bump pre-commit from 4.1.0 to 4.2.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 06:54:46 +00:00
dependabot[bot]
68246a587e Bump homeassistant from 2025.2.1 to 2025.3.3
Bumps [homeassistant](https://github.com/home-assistant/core) from 2025.2.1 to 2025.3.3.
- [Release notes](https://github.com/home-assistant/core/releases)
- [Commits](https://github.com/home-assistant/core/compare/2025.2.1...2025.3.3)

---
updated-dependencies:
- dependency-name: homeassistant
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 06:57:52 +00:00
dependabot[bot]
704eb68fbc Bump isort from 6.0.0 to 6.0.1
Bumps [isort](https://github.com/PyCQA/isort) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/6.0.0...6.0.1)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 06:26:21 +00:00
Ron
7a217ab183 Merge pull request #252 from cyberjunky/dependabot/pip/homeassistant-2025.2.1
Bump homeassistant from 2025.1.4 to 2025.2.1
2025-02-16 17:10:53 +01:00
Ron
7b2f4ac570 Merge pull request #253 from cyberjunky/dependabot/pip/isort-6.0.0
Bump isort from 5.13.2 to 6.0.0
2025-02-16 17:10:36 +01:00
dependabot[bot]
adda8c1afb Bump isort from 5.13.2 to 6.0.0
Bumps [isort](https://github.com/pycqa/isort) from 5.13.2 to 6.0.0.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.13.2...6.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 06:29:43 +00:00
dependabot[bot]
627f99f702 Bump homeassistant from 2025.1.4 to 2025.2.1
Bumps [homeassistant](https://github.com/home-assistant/core) from 2025.1.4 to 2025.2.1.
- [Release notes](https://github.com/home-assistant/core/releases)
- [Commits](https://github.com/home-assistant/core/compare/2025.1.4...2025.2.1)

---
updated-dependencies:
- dependency-name: homeassistant
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 06:29:39 +00:00
Ron Klinkien
83e8e52d72 Fixes 2025-02-08 18:18:15 +00:00
Ron
c94104443d Merge pull request #251 from cyberjunky/dependabot/pip/ruff-0.9.5
Bump ruff from 0.9.2 to 0.9.5
2025-02-08 18:39:40 +01:00
Ron
13b2bb64b0 Merge branch 'main' into dependabot/pip/ruff-0.9.5 2025-02-08 18:38:08 +01:00
Ron
d9b99cd9ea Merge pull request #250 from cyberjunky/dependabot/pip/codespell-2.4.1
Bump codespell from 2.3.0 to 2.4.1
2025-02-08 18:37:41 +01:00
Ron
00a15e077a Merge branch 'main' into dependabot/pip/codespell-2.4.1 2025-02-08 18:37:13 +01:00
dependabot[bot]
4a5564bc45 Bump ruff from 0.9.2 to 0.9.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.2 to 0.9.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-08 17:36:34 +00:00
Ron
b35de63aae Merge pull request #245 from cyberjunky/dependabot/pip/ruff-0.9.3
Bump ruff from 0.9.2 to 0.9.3
2025-02-08 18:36:34 +01:00
dependabot[bot]
c52166ded0 Bump codespell from 2.3.0 to 2.4.1
Bumps [codespell](https://github.com/codespell-project/codespell) from 2.3.0 to 2.4.1.
- [Release notes](https://github.com/codespell-project/codespell/releases)
- [Commits](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.1)

---
updated-dependencies:
- dependency-name: codespell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-08 17:36:28 +00:00
Ron
d6bc6f7f69 Merge pull request #244 from cyberjunky/dependabot/pip/codespell-2.4.0
Bump codespell from 2.3.0 to 2.4.0
2025-02-08 18:36:19 +01:00
Ron
a41b847e45 Merge pull request #243 from cyberjunky/dependabot/pip/pre-commit-4.1.0
Bump pre-commit from 4.0.1 to 4.1.0
2025-02-08 18:35:36 +01:00
Ron
9c42b634e7 Merge pull request #242 from cyberjunky/dependabot/pip/homeassistant-2025.1.4
Bump homeassistant from 2025.1.2 to 2025.1.4
2025-02-08 18:35:24 +01:00
Ron Klinkien
2f19c5b1d6 MFA WIP 2025-02-08 18:30:58 +01:00
dependabot[bot]
28a95b2ab2 Bump ruff from 0.9.2 to 0.9.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.2 to 0.9.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 06:27:55 +00:00
dependabot[bot]
d1609b5c5b Bump codespell from 2.3.0 to 2.4.0
Bumps [codespell](https://github.com/codespell-project/codespell) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/codespell-project/codespell/releases)
- [Commits](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: codespell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 06:27:45 +00:00
dependabot[bot]
6b5d8fc0b6 Bump pre-commit from 4.0.1 to 4.1.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.0.1...v4.1.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 06:27:43 +00:00
dependabot[bot]
be907979ba Bump homeassistant from 2025.1.2 to 2025.1.4
Bumps [homeassistant](https://github.com/home-assistant/core) from 2025.1.2 to 2025.1.4.
- [Release notes](https://github.com/home-assistant/core/releases)
- [Commits](https://github.com/home-assistant/core/compare/2025.1.2...2025.1.4)

---
updated-dependencies:
- dependency-name: homeassistant
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 06:27:40 +00:00
Ron
d884162c8f Update issue templates 2025-01-22 14:39:28 +01:00
Ron
194f377016 Create .coderabbit.yaml 2025-01-22 14:37:22 +01:00
Ron Klinkien
dfc4b5c723 Added User Points, User Level and Last Activity sensors 2025-01-21 10:52:42 +00:00
Ron Klinkien
06ee9e15b7 Bumped version 2025-01-21 08:12:39 +00:00
Ron Klinkien
164fc53b6d Documented services examples 2025-01-21 08:11:52 +00:00
Ron
2eeb42b4da Merge pull request #236 from cyberjunky/dependabot/pip/ruff-0.9.2
Bump ruff from 0.7.2 to 0.9.2
2025-01-20 19:11:01 +01:00
Ron Klinkien
0cb06c7cc4 Fixed selection of 10 last badges 2025-01-20 18:01:57 +00:00
dependabot[bot]
99fe655919 Bump ruff from 0.7.2 to 0.9.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.2 to 0.9.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.7.2...0.9.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 11:47:00 +00:00
Ron Klinkien
36d6f7720d Many bug fixes and fixes of deprecated code, update of dev environment and dependencies 2025-01-20 10:16:05 +00:00
Ron
34d88ff534 Merge pull request #228 from lukas-hermans/main
Add total sleep duration sensor
2025-01-17 13:08:44 +01:00
Ron
4a3ac50097 Merge pull request #233 from cyberjunky/dependabot/pip/ruff-0.9.2
Bump ruff from 0.8.3 to 0.9.2
2025-01-17 12:39:34 +01:00
dependabot[bot]
16a179ab9b Bump ruff from 0.8.3 to 0.9.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.3 to 0.9.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.9.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-17 11:32:38 +00:00
Ron
8269a44345 Merge pull request #224 from cyberjunky/dependabot/pip/pylint-3.3.3
Bump pylint from 3.3.2 to 3.3.3
2025-01-17 12:32:04 +01:00
Ron
3be17f04c4 Merge pull request #223 from cyberjunky/dependabot/pip/mypy-1.14.1
Bump mypy from 1.13.0 to 1.14.1
2025-01-17 12:31:53 +01:00
Ron
46b11708bb Merge pull request #218 from cyberjunky/dependabot/pip/pip-gte-24.1.1-and-lt-24.4
Update pip requirement from <24.2,>=24.1.1 to >=24.1.1,<24.4
2025-01-17 12:31:38 +01:00
Ron
115db56262 Merge pull request #217 from cyberjunky/dependabot/pip/colorlog-6.9.0
Bump colorlog from 6.8.2 to 6.9.0
2025-01-17 12:31:25 +01:00
Ron
792c50e291 Merge pull request #232 from jfparis/badges
Fixes to badge sensor
2025-01-17 12:31:03 +01:00
Jean-François Paris
0f0d9ad8f5 Better icon for badges sensor 2025-01-13 22:31:26 +00:00
Jean-François Paris
2a16ea4727 Fixed 3 sensors with state/Sensor class mixed up 2025-01-13 22:28:27 +00:00
Lukas Hermans
bd567dc0b2 Add total sleep duration 2025-01-07 19:52:13 +01:00
dependabot[bot]
1620038867 Bump pylint from 3.3.2 to 3.3.3
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 20:03:47 +00:00
dependabot[bot]
9cc0b4ee43 Bump mypy from 1.13.0 to 1.14.1
Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 20:03:01 +00:00
dependabot[bot]
a2ce110dcf Update pip requirement from <24.2,>=24.1.1 to >=24.1.1,<24.4
Updates the requirements on [pip](https://github.com/pypa/pip) to permit the latest version.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/24.1.1...24.3.1)

---
updated-dependencies:
- dependency-name: pip
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 20:01:58 +00:00
dependabot[bot]
a2c73638d4 Bump colorlog from 6.8.2 to 6.9.0
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.8.2 to 6.9.0.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.8.2...v6.9.0)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 20:01:54 +00:00
Ron Klinkien
585242e77c Another hrvdata missing fix. 2024-12-20 12:03:31 +01:00
Ron Klinkien
dbcb5d7103 Fixes related to hrvstatus and body composition data 2024-12-20 10:02:34 +01:00
Ron
3a14a7a108 Update hacs.json 2024-12-17 09:02:43 +01:00
Ron
5f9aeb4914 Merge pull request #210 from cyberjunky/dependabot/pip/vulture-2.14
Bump vulture from 2.11 to 2.14
2024-12-17 08:54:02 +01:00
dependabot[bot]
5934224198 Bump vulture from 2.11 to 2.14
Bumps [vulture](https://github.com/jendrikseipp/vulture) from 2.11 to 2.14.
- [Release notes](https://github.com/jendrikseipp/vulture/releases)
- [Changelog](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jendrikseipp/vulture/compare/v2.11...v2.14)

---
updated-dependencies:
- dependency-name: vulture
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 07:53:29 +00:00
Ron
fbbbf92728 Merge pull request #213 from cyberjunky/dependabot/pip/ruff-0.8.3
Bump ruff from 0.5.0 to 0.8.3
2024-12-17 08:52:45 +01:00
Ron
cd3190a799 Merge pull request #209 from cyberjunky/dependabot/pip/pre-commit-4.0.1
Bump pre-commit from 3.7.1 to 4.0.1
2024-12-17 08:52:21 +01:00
dependabot[bot]
4015141d23 Bump pre-commit from 3.7.1 to 4.0.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.1 to 4.0.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.1...v4.0.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 07:51:20 +00:00
dependabot[bot]
91cdd4ad77 Bump ruff from 0.5.0 to 0.8.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.0 to 0.8.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.5.0...0.8.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 07:51:17 +00:00
Ron
cdff89f2ca Merge pull request #208 from cyberjunky/dependabot/pip/pylint-3.3.2
Bump pylint from 3.2.5 to 3.3.2
2024-12-17 08:50:17 +01:00
Ron
689df3de94 Merge pull request #206 from cyberjunky/dependabot/pip/mypy-1.13.0
Bump mypy from 1.10.1 to 1.13.0
2024-12-17 08:50:06 +01:00
Ron
8c16842247 Merge pull request #157 from kruemelro/main
Always fetch latest data, added blood pressure service
2024-12-17 08:46:38 +01:00
Ron
fa0e60648f Merge pull request #204 from jfparis/hrv_status
Add a sensor to retreive HRV status when available
2024-12-17 08:40:48 +01:00
Ron
5a8fd38741 Merge pull request #203 from jfparis/measurement_units_v2
Correct states class of several entities
2024-12-17 08:37:10 +01:00
Ron Klinkien
f16f96e5de Pin garth version to 0.4.47 2024-12-12 08:32:30 +01:00
dependabot[bot]
25944ba0fe Bump pylint from 3.2.5 to 3.3.2
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.5 to 3.3.2.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.3.2)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 21:07:37 +00:00
dependabot[bot]
d3fa73c5a1 Bump mypy from 1.10.1 to 1.13.0
Bumps [mypy](https://github.com/python/mypy) from 1.10.1 to 1.13.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.1...v1.13.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 21:07:24 +00:00
Jean-François Paris
252f67ff7f Add a sensor to retreive HRV status when available 2024-11-28 10:50:59 +00:00
Jean-François Paris
b366807571 Gives body indicators related data the 'measurement' class 2024-11-27 21:36:32 +00:00
Jean-François Paris
437aae6d34 Gives respitation related data the 'measurement' class 2024-11-27 21:35:06 +00:00
Jean-François Paris
51ccb196c0 Gives SPO2 related data the 'measurement' class 2024-11-27 21:32:12 +00:00
Jean-François Paris
8de123eff2 Gives stress related data the 'measurement' class 2024-11-27 21:28:53 +00:00
Jean-François Paris
d4e9a4edc6 Gives heart rates related data the 'measurement' class 2024-11-27 21:22:15 +00:00
Ron Klinkien
5a5b42a560 Bumped version of garminconnect 2024-11-27 20:20:57 +01:00
Ron Klinkien
e2deaed42b Dev version, added garmin badges 2024-11-27 10:14:55 +01:00
Ron Klinkien
78b6641506 Updated sk.json 2024-11-27 10:14:17 +01:00
Ron Klinkien
29f0832e8b Updated LICENSE 2024-11-27 10:14:17 +01:00
Ron
07473ef701 Merge pull request #139 from kozerskil/badges
Added badges
2024-11-27 10:13:26 +01:00
Ron
b8156a6a7d Merge pull request #186 from cyberjunky/dependabot/pip/pylint-3.2.5
Bump pylint from 3.0.3 to 3.2.5
2024-11-27 09:41:25 +01:00
Ron
7546a40012 Merge branch 'main' into dependabot/pip/pylint-3.2.5 2024-11-27 09:40:48 +01:00
Ron
e6f7947e2c Merge pull request #187 from cyberjunky/dependabot/pip/ruff-0.5.0
Bump ruff from 0.3.5 to 0.5.0
2024-11-27 09:39:53 +01:00
Ron
dda115539f Merge branch 'main' into dependabot/pip/ruff-0.5.0 2024-11-27 09:39:41 +01:00
Ron
304cd1f227 Merge pull request #188 from cyberjunky/dependabot/pip/mypy-1.10.1
Bump mypy from 1.8.0 to 1.10.1
2024-11-27 09:38:59 +01:00
Ron
6dc64df4d9 Merge branch 'main' into dependabot/pip/mypy-1.10.1 2024-11-27 09:38:23 +01:00
Ron
c3af1cc392 Merge pull request #185 from cyberjunky/dependabot/pip/pip-gte-24.1.1-and-lt-24.2
Update pip requirement from <24.1,>=21.0 to >=24.1.1,<24.2
2024-11-27 09:37:25 +01:00
Ron
4fa9f446de Merge pull request #173 from cyberjunky/dependabot/pip/pre-commit-3.7.1
Bump pre-commit from 3.6.0 to 3.7.1
2024-11-27 09:37:11 +01:00
Ron
9431374a71 Delete .github/workflows/ci.yml 2024-11-27 09:20:29 +01:00
Ron Klinkien
684acf436c Bumped python-garminconnect version to temp fix Garmin's API change 2024-11-27 09:18:47 +01:00
dependabot[bot]
3b54e25db5 Bump mypy from 1.8.0 to 1.10.1
Bumps [mypy](https://github.com/python/mypy) from 1.8.0 to 1.10.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.8.0...v1.10.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 19:10:08 +00:00
dependabot[bot]
73eb0cea73 Bump ruff from 0.3.5 to 0.5.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.3.5 to 0.5.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.3.5...0.5.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 19:10:03 +00:00
dependabot[bot]
8099a99077 Bump pylint from 3.0.3 to 3.2.5
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.0.3 to 3.2.5.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.0.3...v3.2.5)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 19:09:53 +00:00
dependabot[bot]
8647e95e30 Update pip requirement from <24.1,>=21.0 to >=24.1.1,<24.2
Updates the requirements on [pip](https://github.com/pypa/pip) to permit the latest version.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/21.0...24.1.1)

---
updated-dependencies:
- dependency-name: pip
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 19:09:49 +00:00
dependabot[bot]
270327d7d8 Bump pre-commit from 3.6.0 to 3.7.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.0 to 3.7.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v3.7.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 19:18:19 +00:00
kruemelro
14c6694239 changed value datatype to int 2024-04-08 08:57:53 +02:00
Ron Klinkien
d42edcabc6 Fixed hacs validation 2024-04-06 12:29:33 +00:00
Ron Klinkien
b827597dab Revert unique id for now, added dev files 2024-04-06 14:13:10 +02:00
Ron
cb16c0198c Merge pull request #126 from cyberjunky/dependabot/pip/vulture-2.11
Bump vulture from 2.10 to 2.11
2024-04-06 10:54:49 +02:00
Ron
d8478aea37 Merge branch 'main' into dependabot/pip/vulture-2.11 2024-04-06 10:54:39 +02:00
Ron
3392a1458a Merge pull request #128 from cyberjunky/dependabot/pip/colorlog-6.8.2
Bump colorlog from 6.8.0 to 6.8.2
2024-04-06 10:54:00 +02:00
Ron
09e10f0da2 Merge pull request #130 from cyberjunky/dependabot/pip/pip-gte-21.0-and-lt-24.1
Update pip requirement from <23.4,>=21.0 to >=21.0,<24.1
2024-04-06 10:53:49 +02:00
Ron
265c0fbb21 Merge branch 'main' into dependabot/pip/pip-gte-21.0-and-lt-24.1 2024-04-06 10:53:41 +02:00
Ron
9c9cf6609c Merge pull request #155 from AnotherGroupChat/main
Bump garminconnect version
2024-04-06 10:52:33 +02:00
Ron
91c35eb486 Merge pull request #152 from cyberjunky/dependabot/pip/pre-commit-3.7.0
Bump pre-commit from 3.6.0 to 3.7.0
2024-04-06 10:51:09 +02:00
Ron
5334378493 Merge pull request #156 from cyberjunky/dependabot/pip/ruff-0.3.5
Bump ruff from 0.1.13 to 0.3.5
2024-04-06 10:50:57 +02:00
kruemelro
3d5200581d try and error ;-) 2024-04-05 10:17:20 +02:00
kruemelro
e4331ecf8c added missing entity_id 2024-04-05 10:05:43 +02:00
kruemelro
47096eabeb added blood_pressure function 2024-04-05 09:43:52 +02:00
kruemelro
09e80e4255 Update manifest.json 2024-04-03 08:04:11 +02:00
kruemelro
62377b5d55 changed body return values
instead of the total average, take the first measurement
2024-04-02 12:16:16 +02:00
kruemelro
9136466115 fixed typo 2024-04-02 11:49:48 +02:00
kruemelro
3bf77a9056 Update manifest.json 2024-04-02 11:40:38 +02:00
kruemelro
83ceb35666 Update garminconnect to 0.2.15 2024-04-02 11:26:08 +02:00
kruemelro
003a4c57ef changed get_body_composition
changed get_body_composition to load data of past week not only today
2024-04-02 11:24:46 +02:00
dependabot[bot]
e99ec5c770 Bump ruff from 0.1.13 to 0.3.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.13 to 0.3.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.13...v0.3.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 19:21:08 +00:00
dylan madisetti
38fd1fb631 Bump connect version 2024-03-31 13:05:56 -04:00
dependabot[bot]
9b8f536601 Bump pre-commit from 3.6.0 to 3.7.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 19:19:34 +00:00
luk
261f41940f Add badges 2024-02-13 13:47:51 +01:00
dependabot[bot]
27551067ba Update pip requirement from <23.4,>=21.0 to >=21.0,<24.1
Updates the requirements on [pip](https://github.com/pypa/pip) to permit the latest version.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/21.0...24.0)

---
updated-dependencies:
- dependency-name: pip
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 19:50:05 +00:00
dependabot[bot]
d5dddd5f73 Bump colorlog from 6.8.0 to 6.8.2
Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 6.8.0 to 6.8.2.
- [Release notes](https://github.com/borntyping/python-colorlog/releases)
- [Commits](https://github.com/borntyping/python-colorlog/compare/v6.8.0...v6.8.2)

---
updated-dependencies:
- dependency-name: colorlog
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 19:37:29 +00:00
dependabot[bot]
4e033b91a5 Bump vulture from 2.10 to 2.11
Bumps [vulture](https://github.com/jendrikseipp/vulture) from 2.10 to 2.11.
- [Release notes](https://github.com/jendrikseipp/vulture/releases)
- [Changelog](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jendrikseipp/vulture/compare/v2.10...v2.11)

---
updated-dependencies:
- dependency-name: vulture
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 19:28:07 +00:00
Ron
a12306d79d Merge pull request #120 from ronlut/main
Add user- and integration name to sensor names and ids
2024-01-16 13:32:04 +01:00
Ron
6abe6ace3c Merge branch 'main' into main 2024-01-16 13:26:02 +01:00
Ron
5f31a7492d Merge pull request #121 from cyberjunky/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2024-01-16 13:20:50 +01:00
Ron
b8abef2954 Merge pull request #122 from cyberjunky/dependabot/pip/ruff-0.1.13
Bump ruff from 0.1.11 to 0.1.13
2024-01-16 13:20:42 +01:00
Ron
a3096b4c80 Merge pull request #123 from cyberjunky/dependabot/pip/pre-commit-3.6.0
Bump pre-commit from 3.5.0 to 3.6.0
2024-01-16 13:20:33 +01:00
dependabot[bot]
23e7ab94c0 Bump pre-commit from 3.5.0 to 3.6.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-16 12:18:50 +00:00
dependabot[bot]
7fb469121c Bump ruff from 0.1.11 to 0.1.13
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.11 to 0.1.13.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.11...v0.1.13)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-16 12:18:44 +00:00
dependabot[bot]
d5c88528b1 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-16 12:18:26 +00:00
cyberjunky
7ab5141a6c Added development enviroment 2024-01-16 12:17:17 +00:00
Rony Lutsky
7ad8d099e1 unique_id with domain 2024-01-11 00:02:42 +02:00
Rony Lutsky
9198f469c2 use username for name and entity_id 2024-01-11 00:01:39 +02:00
Ron
7b227663dc Update README.md 2024-01-08 17:42:53 +01:00
Ron
afc9baa4a2 Update services.yaml 2024-01-03 12:01:19 +01:00
Ron
f15658548d Update services.yaml 2024-01-02 19:07:29 +01:00
Ron
e64573c6aa Update manifest.json 2024-01-02 18:51:21 +01:00
Ron
71f24148a2 Update sk.json 2024-01-02 18:47:22 +01:00
Ron
891230ce7a Update sk.json 2024-01-02 18:47:12 +01:00
Ron
c72e7a1879 Update README.md 2024-01-02 18:28:02 +01:00
Ron
65720791c2 Create hassfest.yaml 2024-01-02 13:57:02 +01:00
Ron
1029e21e23 Merge pull request #116 from alexives/add_body_composition_service
Add service for body composition
2024-01-02 12:33:56 +01:00
Ron
ac923b7517 Update services.yaml 2024-01-02 12:31:50 +01:00
Ron
cf5d39fb56 Update sensor.py 2024-01-02 12:31:16 +01:00
Ron
77dbbc9f47 Update manifest.json 2024-01-02 12:30:43 +01:00
Ron
2326be7455 Update const.py 2024-01-02 12:30:21 +01:00
Ron
bfb720ac5d Update __init__.py 2024-01-02 12:29:46 +01:00
Alex Ives
d20c9bedb2 Add service for body composition
Relates to https://github.com/cyberjunky/home-assistant-garmin_connect/issues/74
2023-12-30 15:42:19 -06:00
Ron
5e6f7ff6e1 Create FUNDING.yml 2023-12-26 20:26:50 +01:00
Ron
9d90c366d9 Merge pull request #113 from misa1515/patch-3
Update sk.json
2023-12-23 17:54:49 +01:00
Ron
891ddbce27 Bumped python-garminconnect version 2023-12-22 13:28:59 +01:00
misa1515
dc345c4d53 Update sk.json 2023-11-21 13:55:51 +01:00
Ron Klinkien
89222f0299 Small textual changes 2023-09-15 13:02:43 +02:00
Ron Klinkien
18304be84a Bumped python-garminconnect package to 0.2.3 to fix stability issues 2023-09-15 13:01:34 +02:00
Ron
ffd478cc60 Fixed typo 2023-07-12 13:23:13 +02:00
Ron
4e6b9310a1 Merge pull request #89 from jfparis/sleep_score
Pull sleepscore from Garmin interface
2023-07-12 12:58:23 +02:00
Jean-François Paris
e9fba7abbe Pull sleepscore from Garmin interface 2023-06-23 23:34:24 +01:00
Ron
17fa2bcc63 Update version 2023-05-22 10:17:21 +02:00
Ron
b26f36c41f Merge pull request #72 from ViPeR5000/patch-2
Update translation
2023-05-22 10:14:53 +02:00
Ron
845cce3bda Merge pull request #49 from ray0711/gear_sensor
Added gear sensors & gear service
2023-05-22 10:14:12 +02:00
Ron
15800b00d5 Merge pull request #80 from gorzelak/main
Adding last activities sensor
2023-05-22 10:11:49 +02:00
Ron
c9e9740137 Merge pull request #79 from pailloM/main
Remove pytz dependency in sensor.py
2023-05-22 10:10:23 +02:00
Krzysztof Gorzelak
b7f2a6e653 Adding last activities sensor 2023-05-05 10:09:46 +02:00
Paillomams
be150124b7 remove pytz dependency 2023-04-23 00:46:24 -04:00
Ron
28876a8709 Update manifest.json 2023-04-19 08:23:38 +02:00
ray0711
287a9a04c3 Merge branch 'main' of https://github.com/cyberjunky/home-assistant-garmin_connect into gear_sensor 2023-04-16 10:15:05 +02:00
Rui Melo
d57b8e0223 update
fix word
2023-03-03 11:25:43 +00:00
Raimund Huber
fa48f9f4e2 List default for actitivies on sensor, bugfixes 2022-12-08 20:10:58 +01:00
Raimund Huber
4f4d0b8425 Gear service to update default gear per activity on garmin connect 2022-12-06 22:02:51 +01:00
Raimund Huber
8760bc0a78 unreleased python-garminconnect version 2022-12-04 12:35:28 +01:00
Raimund Huber
8efd365163 Fixup exception for non-numeric sensor values e.g. stress_qualifier = UNKNOWN 2022-12-04 12:34:47 +01:00
Raimund Huber
cc6e0af4e5 add basic gear sensors 2022-12-04 12:03:10 +01:00
82 changed files with 5443 additions and 1417 deletions

22
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,22 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # Schema for CodeRabbit configurations
language: "en-US"
early_access: true
reviews:
profile: "assertive"
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
path_filters:
- "!tests/**/cassettes/**"
path_instructions:
- path: "tests/**"
instructions: |
- test functions shouldn't have a return type hint
- it's ok to use `assert` instead of `pytest.assume()`
chat:
auto_reply: true

62
.devcontainer.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "Garmin Connect Integration",
"image": "mcr.microsoft.com/devcontainers/python:1-3.13",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant"
},
"0-8122": {
"label": "Auto-Forwarded - Other",
"onAutoForward": "ignore"
},
"8124-999999": {
"label": "Auto-Forwarded - Other",
"onAutoForward": "ignore"
}
},
"customizations": {
"extensions": [
"charliermarsh.ruff",
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance",
"GitHub.copilot"
],
"vscode": {
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.formatting.provider": "ruff",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.rulers": [
100
],
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports": "always"
},
"files.trimTrailingWhitespace": true
},
"extensions": [
"GitHub.copilot",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-vscode.makefile-tools",
"ryanluker.vscode-coverage-gutters"
]
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/rust:1": {}
}
}

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: [cyberjunky] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

54
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
---
name: "Bug report"
description: "Report a bug with the integration"
body:
- type: markdown
attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have enabled debug logging for my installation.
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This issue is not a duplicate issue of any [previous issues](https://github.com/cyberjunky/home-assistant-garmin_connect/issues?q=is%3Aissue+label%3A%22Bug%22+)..
required: true
- type: textarea
attributes:
label: "Describe the issue"
description: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed."
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,46 @@
---
name: "Feature request"
description: "Suggest an idea for this project"
body:
- type: markdown
attributes:
value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea.
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have filled out the template to the best of my ability.
required: true
- label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request).
required: true
- label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/cyberjunky/home-assistant-hvcgroep/issues?q=is%3Aissue+label%3A%22Feature+Request%22+).
required: true
- type: textarea
attributes:
label: "Is your feature request related to a problem? Please describe."
description: "A clear and concise description of what the problem is."
placeholder: "I'm always frustrated when [...]"
validations:
required: true
- type: textarea
attributes:
label: "Describe the solution you'd like"
description: "A clear and concise description of what you want to happen."
validations:
required: true
- type: textarea
attributes:
label: "Describe alternatives you've considered"
description: "A clear and concise description of any alternative solutions or features you've considered."
validations:
required: true
- type: textarea
attributes:
label: "Additional context"
description: "Add any other context or screenshots about the feature request here."
validations:
required: true

29
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"

92
.github/pre-commit-config.yaml vendored Normal file
View File

@@ -0,0 +1,92 @@
repos:
- repo: local
hooks:
- id: codespell
name: Check code for common misspellings
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: codespell
args:
- --quiet-level=2
- --ignore-words-list=hass,ba,fo
- --skip=tests/fixtures/*,custom_components/garmin_connect/translations/*
- id: pyupgrade
name: Run pyupgrade
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: pyupgrade
files: ^.*.py$
args:
- "--py311-plus"
- id: ruff-check
name: Run ruff check
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: ruff
args:
- check
files: ^((action|custom_components|script|tests)/.+)?[^/]+\.py$
- id: mypy
name: Run mypy type checker
language: system
types: [python]
stages: [commit-msg, post-commit, manual]
entry: mypy
args:
- --ignore-missing-imports
- --check-untyped-defs
files: ^custom_components/.+\.py$
- id: check-yaml
name: Check YAML files
language: system
types: [yaml]
stages: [commit-msg, post-commit, manual]
entry: check-yaml
args:
- --unsafe
- id: check-executables-have-shebangs
name: Check that executables have shebangs
language: system
types: [text, executable]
entry: check-executables-have-shebangs
stages: [commit-msg, post-commit, manual]
- id: check-json
name: Check JSON files
language: system
types: [json]
stages: [commit-msg, post-commit, manual]
entry: check-json
- id: requirements-txt-fixer
name: Check requirements files
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: requirements-txt-fixer
files: ^requirements_.*.txt$
- id: check-ast
name: Check Python AST
language: system
types: [python]
stages: [commit-msg, post-commit, manual]
entry: check-ast
- id: mixed-line-ending
name: Check line endings
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: mixed-line-ending
args:
- --fix=lf

25
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
changelog:
categories:
- title: '💥 Breaking changes'
labels:
- 'Breaking Change'
- title: '🛎️ Experimental'
labels:
- 'Experimental'
- title: '✨ New features'
labels:
- 'pr: new-feature'
- title: '⚡ Enhancements'
labels:
- 'pr: enhancement'
- title: '♻️ Refactor'
labels:
- 'pr: refactor'
- title: '🐛 Bug Fixes'
labels:
- 'pr: bugfix'

36
.github/workflows/hassfest.yaml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Validate
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
push:
branches:
- main
pull_request:
branches:
- main
permissions: {}
jobs:
hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest
name: Hassfest validation
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run hassfest validation
uses: home-assistant/actions/hassfest@87c064c607f3c5cc673a24258d0c98d23033bfc3 # master
hacs: # https://github.com/hacs/action
name: HACS validation
runs-on: ubuntu-latest
steps:
- name: Run HACS validation
uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0
with:
category: integration
# Remove this 'ignore' key when you have added brand images for your integration to https://github.com/home-assistant/brands
ignore: brands

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
# Custom
config/*
!config/configuration.yaml
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.languageServer": "None"
}

40
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"tasks": [
{
"command": "scripts/develop",
"label": "Run Home Assistant",
"problemMatcher": [],
"type": "shell",
"detail": "Runs Home Assistant"
},
{
"command": "scripts/setup",
"label": "Upgrade environment",
"problemMatcher": [],
"type": "shell",
"detail": "Upgrades the environment"
},
{
"command": "scripts/test",
"label": "Run tests",
"problemMatcher": [],
"type": "shell",
"detail": "Runs tests"
},
{
"command": "scripts/lint",
"label": "Run lint checks",
"problemMatcher": [],
"type": "shell",
"detail": "Runs lint checks"
},
{
"label": "Rebuild and Reopen Container (Instructions)",
"type": "shell",
"command": "echo 'To rebuild container: Press Ctrl+Shift+P and run Dev Containers: Rebuild and Reopen in Container'",
"problemMatcher": [],
"detail": "Prints instructions for rebuilding the container"
}
],
"version": "2.0.0"
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ron Klinkien
Copyright (c) 2021-2026 Ron Klinkien
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

565
README.md
View File

@@ -1,141 +1,388 @@
[![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/)
[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
![Project Maintenance][maintenance-shield]
[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky)
# 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.
Integrate your Garmin Connect fitness data with Home Assistant. Access **110+ sensors** covering health metrics, activities, body composition, and gear tracking—plus add measurements directly to Garmin Connect via services.
## Installation
## Supported Features
### 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 on found integration.
- Click Download this repository with HACS.
- Restart Home-Assistant.
- Follow configuration steps below.
### Manual
- Copy directory `custom_components/garmin_connect` to your `<config dir>/custom_components` directory.
- Restart Home-Assistant.
- Follow configuration steps below.
## Configuration
Adding Garmin Connect to your Home Assistant instance can be done via the integrations user interface.
- Browse to your Home Assistant instance.
- In the sidebar click on Configuration.
- From the configuration menu select: Integrations.
- In the bottom right, click on the Add Integration button.
- From the list, search and select “Garmin Connect”.
- Follow the instruction on screen to complete the set up
After successful set up a standard set of sensors are enabled. You can enable more if needed by using the Integrations page.
Please be aware that Garmin Connect has very low rate limits, max. once every ~5 minutes.
- **Health Metrics** - Steps, calories, heart rate, stress, sleep, body battery, SpO2
- **Body Composition** - Weight, BMI, body fat, muscle mass, hydration
- **Fitness Tracking** - Activities, intensity minutes, fitness age, endurance score
- **Gear Management** - Track usage of shoes, bikes, and other equipment
- **Services** - Add body composition and blood pressure data, manage gear defaults
## Available Sensors
Not every sensor holds meaningful values, it depends on the tracking and health devices you use, or the apps you have connected.
> **Note:** All sensors are enabled by default. You can disable sensors using Home Assistant's sensor configuration.
> Sensor values depend on your Garmin devices and connected apps.
Enabled by default:
### Activity & Steps
```text
Total Steps
Daily Step Goal
Total KiloCalories
Active KiloCalories
BMR KiloCalories
Consumed KiloCalories
Burned KiloCalories
Total Distance Mtr
Active Time
Sedentary Time
Sleeping Time
Awake Duration
Sleep Duration
Floors Ascended
Floors Descended
Floors Ascended Goal
Min Heart Rate
Max Heart Rate
Resting Heart Rate
Avg Stress Level
Max Stress Level
Rest Stress Duration
Activity Stress Duration
Uncat. Stress Duration
Total Stress Duration
Low Stress Duration
Medium Stress Duration
High Stress Duration
Body Battery Charged
Body Battery Drained
Body Battery Highest
Body Battery Lowest
Body Battery Most Recent
Average SPO2
Lowest SPO2
Latest SPO2
| Sensor | Description |
| ------------------------- | ------------------------------ |
| Total Steps | Daily step count |
| Daily Step Goal | Your configured step target |
| Yesterday Steps/Distance | Previous day's complete totals |
| Weekly Step/Distance Avg | 7-day averages |
| Total Distance | Distance walked/run in meters |
| Floors Ascended/Descended | Floors climbed |
### Calories & Nutrition
| Sensor | Description |
| ------------------------- | --------------------- |
| Total/Active/BMR Calories | Daily calorie metrics |
| Burned/Consumed Calories | Calorie tracking |
### Heart Rate
| Sensor | Description |
| ---------------------- | ------------------------------ |
| Resting Heart Rate | Daily resting HR |
| Min/Max Heart Rate | Daily HR range |
| Last 7 Days Avg HR | Weekly average |
| HRV Weekly/Nightly Avg | Heart rate variability metrics |
| HRV Baseline | Personal HRV baseline |
### Stress & Recovery
| Sensor | Description |
| -------------------- | -------------------------------------------- |
| Avg/Max Stress Level | Stress measurements (0-100) |
| Stress Durations | Time in rest/activity/low/medium/high stress |
### Sleep
| Sensor | Description |
| -------------------- | --------------------------- |
| Sleep Score | Overall sleep quality score |
| Sleep/Awake Duration | Time asleep and awake |
| Deep Sleep | Time in deep sleep |
| Light Sleep | Time in light sleep |
| REM Sleep | Time in REM sleep |
### Body Battery
| Sensor | Description |
| ------------------------ | ---------------------------- |
| Body Battery Most Recent | Current energy level (0-100) |
| Charged/Drained | Energy gained/spent |
| Highest/Lowest | Daily peak and low |
### Body Composition
| Sensor | Description |
| ---------------- | -------------------------- |
| Weight/BMI | Body weight and mass index |
| Body Fat/Water | Percentage measurements |
| Muscle/Bone Mass | Mass measurements |
| Metabolic Age | Estimated metabolic age |
### Hydration
| Sensor | Description |
| -------------- | ----------------------- |
| Hydration | Daily water intake (ml) |
| Hydration Goal | Target intake |
| Sweat Loss | Estimated fluid loss |
### Blood Pressure
| Sensor | Description |
| ---------------- | --------------------------------------- |
| Systolic | Systolic blood pressure (mmHg) |
| Diastolic | Diastolic blood pressure (mmHg) |
| Pulse | Pulse from blood pressure reading (bpm) |
| Measurement Time | When the BP was measured |
### Health Monitoring
| Sensor | Description |
| --------------------- | ---------------------- |
| SpO2 (Avg/Low/Latest) | Blood oxygen levels |
| HRV Status | Heart rate variability |
| Respiration Rate | Breathing measurements |
### Fitness & Performance
| Sensor | Description |
| -------------------------- | ------------------------------------ |
| Fitness Age | Estimated fitness age |
| Chronological Age | Your actual age |
| Endurance Score | Overall endurance rating |
| Training Readiness | Training readiness score (%) |
| Morning Training Readiness | Wake-up training readiness score (%) |
| Training Status | Current training status phrase |
| Lactate Threshold HR | Lactate threshold heart rate (bpm) |
| Lactate Threshold Speed | Lactate threshold running pace (m/s) |
### Goal Tracking
| Sensor | Description |
| ------------- | ----------------------------------------- |
| Active Goals | Number of in-progress goals with progress |
| Future Goals | Upcoming scheduled goals |
| Goals History | Last 10 completed goals with status |
> **Note:** Goal sensors include detailed attributes: `goalType`, `targetValue`, `currentValue`, `progressPercent`, `startDate`, `endDate`, and `activityType`.
### Activity Tracking
| Sensor | Description |
| ------------------------ | ----------------------------------- |
| Next Alarm | Next scheduled alarm time |
| Last Activity/Activities | Recent activity info |
| Last Workout/Workouts | Scheduled/planned training sessions |
| Badges/User Points/Level | Gamification metrics |
#### Activity Route Map
The `Last Activity` sensor includes a `polyline` attribute with GPS coordinates when the activity has GPS data (`hasPolyline: true`). This can be displayed on a map using the included custom Lovelace card.
**Installation:**
1. Copy `www/garmin-polyline-card.js` to your `<config>/www/` folder
2. Add as a resource: **Settings → Dashboards → ⋮ → Resources → Add Resource**
- URL: `/local/garmin-polyline-card.js`
- Type: JavaScript Module
3. Hard refresh your browser (Ctrl+Shift+R)
**Usage:**
```yaml
type: custom:garmin-polyline-card
entity: sensor.garmin_connect_last_activity
attribute: polyline
title: Last Activity Route
height: 400px
color: "#FF5722"
```
Disabled by default:
**Options:**
```text
Remaining KiloCalories
Net Remaining KiloCalories
Net Calorie Goal
Wellness Start Time
Wellness End Time
Wellness Description
Wellness Distance Mtr
Wellness Active KiloCalories
Wellness KiloCalories
Highly Active Time
Floors Ascended Mtr
Floors Descended Mtr
Min Avg Heart Rate
Max Avg Heart Rate
Abnormal HR Counts
Last 7 Days Avg Heart Rate
Stress Qualifier
Stress Duration
Stress Percentage
Rest Stress Percentage
Activity Stress Percentage
Uncat. Stress Percentage
Low Stress Percentage
Medium Stress Percentage
High Stress Percentage
Latest SPO2 Time
Average Altitude
Moderate Intensity
Vigorous Intensity
Intensity Goal
Latest Respiration Update
Highest Respiration
Lowest Respiration
Latest Respiration
| Option | Default | Description |
| ----------- | ---------------- | ------------------------------------- |
| `entity` | (required) | Sensor entity with polyline attribute |
| `attribute` | `polyline` | Attribute containing GPS coordinates |
| `title` | `Activity Route` | Card title |
| `height` | `300px` | Map height |
| `color` | `#FF5722` | Route line color |
| `weight` | `4` | Route line thickness |
Weight
BMI
Body Fat
Body Water
Body Mass
Muscle Mass
Physique Rating
Visceral Fat
Metabolic Age
```
![Activity Route Map](screenshots/polyline-card.png)
### Menstrual Cycle Tracking
| Sensor | Description |
| ------------- | ------------------------- |
| Cycle Phase | Current menstrual phase |
| Cycle Day | Day of the current cycle |
| Period Day | Day of the period |
| Cycle Length | Total cycle length (days) |
| Period Length | Period length (days) |
> **Note:** Menstrual cycle sensors are only available if tracking is enabled in your Garmin Connect account.
### Gear Tracking
Gear sensors are dynamically created for each piece of equipment registered in Garmin Connect (shoes, bikes, etc.). They track total distance and usage statistics.
## Screenshots
![screenshot](https://github.com/cyberjunky/home-assistant-garmin_connect/blob/main/screenshots/garmin_connect.png?raw=true "Screenshot Garmin Connect")
## Debugging
## Requirements
- A Garmin Connect account.
- A device that syncs to the Garmin Connect website.
### HACS (Recommended)
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=cyberjunky&repository=home-assistant-garmin_connect&category=integration)
Alternatively:
1. Install [HACS](https://hacs.xyz) if not already installed
2. Search for "Garmin Connect" in HACS
3. Click **Download**
4. Restart Home Assistant
5. Add via Settings → Devices & Services
### Manual Installation
1. Copy `custom_components/garmin_connect` to your `<config>/custom_components/` directory
2. Restart Home Assistant
3. Add via Settings → Devices & Services
## Configuration
### Adding the Integration
1. Navigate to **Settings****Devices & Services**
2. Click **+ Add Integration**
3. Search for **"Garmin Connect"**
4. Enter your configuration:
- **Username**: Your Garmin Connect username
- **Password**: Your Garmin Connect password
- **MFA Code**: Your Garmin Connect MFA code (optional)
## Advanced Usage
### Automation Examples
**Add Body Composition**
```yaml
action: garmin_connect.add_body_composition
data:
weight: 87
bmi: 25.5
bone_mass: 4.8
```
See the action template for other available values to add.
**Withings scale data to Garmin**
```yaml
alias: WithingsToGarmin
description: Sync weight from Withings to Garmin Connect
triggers:
- trigger: state
entity_id:
- sensor.withings_weight
conditions:
- condition: numeric_state
entity_id: sensor.withings_weight
above: 55
below: 100
actions:
- action: garmin_connect.add_body_composition
data:
weight: "{{ states('sensor.withings_weight') }}"
timestamp: "{{ now().isoformat() }}"
bmi: >-
{{ (states('sensor.withings_weight') | float(0) / 1.72**2) | round(1) }}
bone_mass: "{{ states('sensor.withings_bone_mass') }}"
muscle_mass: "{{ states('sensor.withings_muscle_mass') }}"
percent_fat: "{{ states('sensor.withings_fat_ratio') }}"
mode: single
```
**Set Active Gear**
```yaml
action: garmin_connect.set_active_gear
target:
entity_id: sensor.garmin_connect_adidas
data:
activity_type: running
setting: set as default
```
**Add Blood Pressure**
```yaml
action: garmin_connect.add_blood_pressure
data:
systolic: 120
diastolic: 80
pulse: 60
timestamp: "2025-01-21T07:34:00"
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: "morning_run.fit"
```
**Add Gear to Activity**
Associates gear (shoes, bike, etc.) with a specific activity:
```yaml
action: garmin_connect.add_gear_to_activity
data:
activity_id: 12345678901
gear_uuid: "abc12345-def6-7890-ghij-klmn12345678"
```
**Automatically assign alternate running shoes after a run**
```yaml
automation:
- alias: "Assign alternate shoes to last running activity"
trigger:
- platform: state
entity_id: sensor.garmin_connect_last_activity
condition:
- condition: template
value_template: "{{ state_attr('sensor.garmin_connect_last_activity', 'activityType') == 'running' }}"
action:
- service: garmin_connect.add_gear_to_activity
data:
activity_id: "{{ state_attr('sensor.garmin_connect_last_activity', 'activityId') }}"
gear_uuid: "{{ state_attr('sensor.garmnin_connect_adidas', 'gear_uuid') }}"
```
### Template Sensor Examples
**Daily Running Distance** - Track how far you run each day:
```yaml
template:
- sensor:
- name: "Today's Running Distance"
unit_of_measurement: "km"
state: >
{% set today = now().strftime('%Y-%m-%d') %}
{% set activities = state_attr('sensor.garmin_connect_last_activities', 'last_activities') | default([]) %}
{% set running = namespace(total=0) %}
{% for a in activities if a.activityType == 'running' and today in a.startTimeLocal %}
{% set running.total = running.total + a.distance %}
{% endfor %}
{{ (running.total / 1000) | round(2) }}
- name: "Today's Cycling Distance"
unit_of_measurement: "km"
state: >
{% set today = now().strftime('%Y-%m-%d') %}
{% set activities = state_attr('sensor.garmin_connect_last_activities', 'last_activities') | default([]) %}
{% set cycling = namespace(total=0) %}
{% for a in activities if a.activityType == 'cycling' and today in a.startTimeLocal %}
{% set cycling.total = cycling.total + a.distance %}
{% endfor %}
{{ (cycling.total / 1000) | round(2) }}
```
### Enable Debug Logging
Add the relevant lines below to the `configuration.yaml`:
@@ -146,5 +393,81 @@ logger:
custom_components.garmin_connect: debug
```
## Donation
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/cyberjunkynl/)
Alternatively, enable debug logging via the UI in **Settings****Devices & Services****Garmin Connect****Enable debug logging**:
![Enable Debug Logging](screenshots/enabledebug.png)
Then perform any steps to reproduce the issue and disable debug logging again. It will download the relevant log file automatically.
## Development
Quick-start (from project root):
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements_lint.txt
./scripts/lint # runs pre-commit + vulture
# or: ruff check .
# to auto-fix: ruff check . --fix
```
### Discovering New API Endpoints
Want to add support for new Garmin features? Here's how to find the API endpoints:
1. **Login to [Garmin Connect](https://connect.garmin.com)** in your browser
2. **Open Developer Tools** (F12 or Right-click → Inspect)
3. Go to the **Network** tab
4. **Filter by "Fetch/XHR"** to see API calls only
5. **Navigate through the feature** you want to capture
6. **Look for API calls** - they typically go to:
- `connect.garmin.com/proxy/*`
- `connect.garmin.com/activity-service/*`
- `connect.garmin.com/metrics-service/*`
- `connect.garmin.com/*-service/*`
7. **Click on a request** to see the full URL and response data
**Share your findings** in a GitHub issue with:
- The full API URL path
- Example response data (redact personal info)
## 💖 Support This Project
If you find this library useful for your projects, please consider supporting its continued development and maintenance:
### 🌟 Ways to Support
- **⭐ Star this repository** - Help others discover the project
- **💰 Financial Support** - Contribute to development and hosting costs
- **🐛 Report Issues** - Help improve stability and compatibility
- **📖 Spread the Word** - Share with other developers
### 💳 Financial Support Options
[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
[![Sponsor on GitHub](https://img.shields.io/badge/Sponsor-GitHub-red.svg?style=for-the-badge&logo=github)](https://github.com/sponsors/cyberjunky)
**Why Support?**
- Keeps the project actively maintained
- Enables faster bug fixes and new features
- Supports infrastructure costs (testing, AI, CI/CD)
- Shows appreciation for hundreds of hours of development
Every contribution, no matter the size, makes a difference and is greatly appreciated! 🙏
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
---
[releases-shield]: https://img.shields.io/github/release/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
[releases]: https://github.com/cyberjunky/home-assistant-garmin_connect/releases
[commits-shield]: https://img.shields.io/github/commit-activity/y/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
[commits]: https://github.com/cyberjunky/home-assistant-garmin_connect/commits/main
[license-shield]: https://img.shields.io/github/license/cyberjunky/home-assistant-garmin_connect.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-cyberjunky-blue.svg?style=for-the-badge

41
SECURITY.md Normal file
View File

@@ -0,0 +1,41 @@
# Security Policy
## Reporting a Vulnerability
If you discover a security vulnerability in this integration, please report it privately by creating a [security advisory](https://github.com/cyberjunky/home-assistant-garmin_connect/security/advisories) on GitHub.
**Please do NOT open a public issue for security vulnerabilities.** This allows us to address the issue before it becomes public knowledge.
## Security Considerations
### Network Communication
This integration communicates with Garmin Connect over the internet (HTTPS):
- Keep your Home Assistant instance on a secure network
### Credential Storage
Garmin Connect connection details (username, password, mfa_code) are used to fetch a session token. This session token is stored in Home Assistant's configuration:
- Keep your Home Assistant configuration and data secure
- Do not share your Home Assistant backups without sanitizing sensitive data
### Best Practices
1. **Keep Home Assistant updated** - Security patches are released regularly
2. **Install from official sources** - Use HACS or official GitHub releases
3. **Review the code** - As an open-source project, you can audit the code before use
4. **Secure your network** - Restrict access to your Home Assistant instance
5. **Use strong authentication** - Enable Home Assistant's user authentication
## Disclosure Timeline
When a vulnerability is confirmed:
1. We will assess the severity and impact
2. A fix will be prepared for the latest version
3. A new release will be published
4. A security advisory will be published on GitHub (with credit to the reporter if desired)
Thank you for helping keep this project secure!

12
config/configuration.yaml Normal file
View File

@@ -0,0 +1,12 @@
# https://www.home-assistant.io/integrations/default_config/
default_config:
# https://www.home-assistant.io/integrations/homeassistant/
homeassistant:
debug: true
# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:
custom_components.garmin_connect: debug

View File

@@ -1,29 +1,91 @@
"""The Garmin Connect integration."""
from datetime import date
import logging
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
from garminconnect import Garmin
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_COORDINATOR, DEFAULT_UPDATE_INTERVAL, DOMAIN
from .const import DOMAIN
from .coordinator import GarminConnectDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old config entry to new format."""
_LOGGER.debug("Migrating config entry from version %s", entry.version)
if entry.version == 1:
# Has USERNAME + PASSWORD but no TOKEN (old auth method)
if (
CONF_TOKEN not in entry.data
and CONF_USERNAME in entry.data
and CONF_PASSWORD in entry.data
):
_LOGGER.info("Migrating from username/password to token-based auth")
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
in_china = hass.config.country == "CN"
api = Garmin(email=username, password=password, is_cn=in_china)
try:
await hass.async_add_executor_job(api.login)
tokens = api.garth.dumps()
new_data = {
CONF_ID: entry.data.get(CONF_ID, username),
CONF_TOKEN: tokens,
}
hass.config_entries.async_update_entry(entry, data=new_data)
_LOGGER.info("Migration successful")
return True
except Exception as err: # pylint: disable=broad-except
_LOGGER.error("Migration failed: %s", err)
return False
# Has USERNAME + TOKEN but no ID (partially migrated)
elif (
CONF_ID not in entry.data
and CONF_USERNAME in entry.data
and CONF_TOKEN in entry.data
):
_LOGGER.info("Migrating: converting USERNAME to ID")
new_data = {
CONF_ID: entry.data[CONF_USERNAME],
CONF_TOKEN: entry.data[CONF_TOKEN],
}
hass.config_entries.async_update_entry(entry, data=new_data)
return True
# Missing TOKEN (incomplete/corrupted)
elif CONF_TOKEN not in entry.data:
if CONF_ID not in entry.data:
_LOGGER.info("Adding placeholder ID for reauth flow")
new_data = {
**entry.data,
CONF_ID: entry.entry_id,
}
hass.config_entries.async_update_entry(entry, data=new_data)
_LOGGER.info("Config entry incomplete, reauthentication required")
return True
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Garmin Connect from a config entry."""
from .services import async_setup_services
coordinator = GarminConnectDataUpdateCoordinator(hass, entry=entry)
@@ -32,91 +94,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_COORDINATOR: coordinator,
}
# Use runtime_data pattern (modern approach)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register integration-level services (only once)
if not hass.services.has_service(DOMAIN, "add_body_composition"):
await async_setup_services(hass)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
from .services import async_unload_services
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
# Unload services only if this is the last entry (no entries remaining after unload)
remaining_entries = len(hass.config_entries.async_entries(DOMAIN))
if unload_ok and remaining_entries == 1: # This entry is being unloaded
await async_unload_services(hass)
class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
"""Garmin Connect Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the Garmin Connect hub."""
self.entry = entry
self.hass = hass
self.in_china = False
country = self.hass.config.country
if country == "CN":
self.in_china = True
self._api = Garmin(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], self.in_china)
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,
}
return bool(unload_ok)

View File

@@ -1,49 +0,0 @@
"""Utility method for converting Garmin Connect alarms to python datetime."""
from datetime import date, datetime, timedelta
import logging
_LOGGER = logging.getLogger(__name__)
DAY_TO_NUMBER = {
"Mo": 1,
"M": 1,
"Tu": 2,
"We": 3,
"W": 3,
"Th": 4,
"Fr": 5,
"F": 5,
"Sa": 6,
"Su": 7,
}
def calculate_next_active_alarms(alarms):
"""
Calculate garmin next active alarms from settings.
Alarms are sorted by time
"""
active_alarms = []
for alarm_setting in alarms:
if alarm_setting["alarmMode"] != "ON":
continue
for day in alarm_setting["alarmDays"]:
alarm_time = alarm_setting["alarmTime"]
if day == "ONCE":
midnight = datetime.combine(date.today(), datetime.min.time())
alarm = midnight + timedelta(minutes=alarm_time)
if alarm < datetime.now():
alarm += timedelta(days=1)
else:
start_of_week = datetime.combine(
date.today() - timedelta(days=datetime.today().isoweekday() % 7),
datetime.min.time(),
)
days_to_add = DAY_TO_NUMBER[day] % 7
alarm = start_of_week + timedelta(minutes=alarm_time, days=days_to_add)
if alarm < datetime.now():
alarm += timedelta(days=7)
active_alarms.append(alarm.isoformat())
return sorted(active_alarms) if active_alarms else None

View File

@@ -1,72 +1,177 @@
"""Config flow for Garmin Connect integration."""
import logging
import logging
from collections.abc import Mapping
from typing import Any, cast
import garth
import requests
import voluptuous as vol
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant import config_entries
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN
from .const import CONF_MFA, DOMAIN
_LOGGER = logging.getLogger(__name__)
class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class GarminConnectConfigFlowHandler(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
"""Handle a config flow for Garmin Connect."""
VERSION = 1
async def _show_setup_form(self, errors=None):
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
),
errors=errors or {},
def __init__(self) -> None:
"""Initialize the config flow."""
self.data_schema = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
self.mfa_data_schema = {
vol.Required(CONF_MFA): str,
}
self._api = None
self._login_result1: Any = None
self._login_result2: Any = None
self._mfa_code: str | None = None
self._username: str | None = None
self._password: str | None = None
self._in_china = False
async def _async_garmin_connect_login(self, step_id: str) -> ConfigFlowResult:
"""Authenticate with Garmin Connect."""
errors = {}
if self.hass.config.country == "CN":
self._in_china = True
self._api = Garmin(
email=self._username,
password=self._password,
return_on_mfa=True,
is_cn=self._in_china,
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is None:
return await self._show_setup_form()
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]
api = Garmin(username, password)
errors = {}
try:
await self.hass.async_add_executor_job(api.login)
self._login_result1, self._login_result2 = (
await self.hass.async_add_executor_job(self._api.login) # type: ignore[attr-defined]
)
if self._login_result1 == "needs_mfa":
return await self.async_step_mfa()
except GarminConnectConnectionError:
errors["base"] = "cannot_connect"
return await self._show_setup_form(errors)
errors = {"base": "cannot_connect"}
except GarminConnectAuthenticationError:
errors["base"] = "invalid_auth"
return await self._show_setup_form(errors)
errors = {"base": "invalid_auth"}
except GarminConnectTooManyRequestsError:
errors["base"] = "too_many_requests"
return await self._show_setup_form(errors)
errors = {"base": "too_many_requests"}
except requests.exceptions.HTTPError as err:
if err.response.status_code == 403:
errors = {"base": "invalid_auth"}
elif err.response.status_code == 429:
errors = {"base": "too_many_requests"}
else:
errors = {"base": "cannot_connect"}
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return await self._show_setup_form(errors)
errors = {"base": "unknown"}
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
if errors:
return self.async_show_form(
step_id=step_id,
data_schema=vol.Schema(self.data_schema),
errors=errors,
)
return self.async_create_entry(
title=username,
data={
CONF_ID: username,
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
)
return await self._async_create_entry()
async def _async_garmin_connect_mfa_login(self) -> ConfigFlowResult:
"""Complete MFA authentication."""
try:
await self.hass.async_add_executor_job(
self._api.resume_login, self._login_result2, self._mfa_code # type: ignore[attr-defined]
)
except garth.exc.GarthException as err:
_LOGGER.error("MFA login error: %s", err)
return self.async_show_form(
step_id="mfa",
data_schema=vol.Schema(self.mfa_data_schema),
errors={"base": "invalid_mfa_code"},
)
return await self._async_create_entry()
async def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry."""
config_data = {
CONF_ID: self._username,
CONF_TOKEN: self._api.garth.dumps(), # type: ignore[attr-defined]
}
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=config_data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=cast(str, self._username), data=config_data)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=vol.Schema(self.data_schema)
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
return await self._async_garmin_connect_login(step_id="user")
async def async_step_mfa(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the MFA step."""
if user_input is None:
return self.async_show_form(
step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema)
)
self._mfa_code = user_input[CONF_MFA]
return await self._async_garmin_connect_mfa_login()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthorization."""
self._username = entry_data.get(CONF_USERNAME) or entry_data.get(CONF_ID)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthorization confirmation."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME, default=self._username): str,
vol.Required(CONF_PASSWORD): str,
}
),
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
return await self._async_garmin_connect_login(step_id="reauth_confirm")

View File

@@ -1,402 +1,60 @@
"""Constants for the Garmin Connect integration."""
from datetime import timedelta
from homeassistant.const import (
LENGTH_METERS,
MASS_KILOGRAMS,
PERCENTAGE,
TIME_MINUTES,
TIME_YEARS,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
)
from datetime import timedelta
DOMAIN = "garmin_connect"
DATA_COORDINATOR = "coordinator"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
CONF_MFA = "mfa_code"
GARMIN_ENTITY_LIST = {
"totalSteps": ["Total Steps", "steps", "mdi:walk", None, SensorStateClass.TOTAL, True],
"dailyStepGoal": ["Daily Step Goal", "steps", "mdi:walk", None, SensorStateClass.TOTAL, True],
"totalKilocalories": ["Total KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, True],
"activeKilocalories": ["Active KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, True],
"bmrKilocalories": ["BMR KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, True],
"consumedKilocalories": ["Consumed KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"burnedKilocalories": ["Burned KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, True],
"remainingKilocalories": [
"Remaining KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
False,
],
"netRemainingKilocalories": [
"Net Remaining KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
False,
],
"netCalorieGoal": ["Net Calorie Goal", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"totalDistanceMeters": [
"Total Distance Mtr",
LENGTH_METERS,
"mdi:walk",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
True,
],
"wellnessStartTimeLocal": [
"Wellness Start Time",
None,
"mdi:clock",
SensorDeviceClass.TIMESTAMP,
None,
False,
],
"wellnessEndTimeLocal": [
"Wellness End Time",
None,
"mdi:clock",
SensorDeviceClass.TIMESTAMP,
None,
False,
],
"wellnessDescription": ["Wellness Description", "", "mdi:clock", None, SensorStateClass.TOTAL, False],
"wellnessDistanceMeters": [
"Wellness Distance Mtr",
LENGTH_METERS,
"mdi:walk",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
False,
],
"wellnessActiveKilocalories": [
"Wellness Active KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
False,
],
"wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"highlyActiveSeconds": [
"Highly Active Time",
TIME_MINUTES,
"mdi:fire",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
False,
],
"activeSeconds": ["Active Time", TIME_MINUTES, "mdi:fire", None, SensorStateClass.TOTAL, True],
"sedentarySeconds": ["Sedentary Time", TIME_MINUTES, "mdi:seat", None, SensorStateClass.TOTAL, True],
"sleepingSeconds": ["Sleeping Time", TIME_MINUTES, "mdi:sleep", None, SensorStateClass.TOTAL, True],
"measurableAwakeDuration": [
"Awake Duration",
TIME_MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"measurableAsleepDuration": [
"Sleep Duration",
TIME_MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"floorsAscendedInMeters": [
"Floors Ascended Mtr",
LENGTH_METERS,
"mdi:stairs",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
False,
],
"floorsDescendedInMeters": [
"Floors Descended Mtr",
LENGTH_METERS,
"mdi:stairs",
SensorDeviceClass.DISTANCE,
SensorStateClass.TOTAL,
False,
],
"floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
"floorsDescended": ["Floors Descended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
"userFloorsAscendedGoal": [
"Floors Ascended Goal",
"floors",
"mdi:stairs",
None,
SensorStateClass.TOTAL,
True,
],
"minHeartRate": ["Min Heart Rate", "bpm", "mdi:heart-pulse", None, SensorStateClass.TOTAL, True],
"maxHeartRate": ["Max Heart Rate", "bpm", "mdi:heart-pulse", None, SensorStateClass.TOTAL, True],
"restingHeartRate": ["Resting Heart Rate", "bpm", "mdi:heart-pulse", None, SensorStateClass.TOTAL, True],
"minAvgHeartRate": ["Min Avg Heart Rate", "bpm", "mdi:heart-pulse", None, SensorStateClass.TOTAL, False],
"maxAvgHeartRate": ["Max Avg Heart Rate", "bpm", "mdi:heart-pulse", None, SensorStateClass.TOTAL, False],
"abnormalHeartRateAlertsCount": [
"Abnormal HR Counts",
None,
"mdi:heart-pulse",
None,
SensorStateClass.TOTAL,
False,
],
"lastSevenDaysAvgRestingHeartRate": [
"Last 7 Days Avg Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.TOTAL,
False,
],
"averageStressLevel": ["Avg Stress Level", "lvl", "mdi:flash-alert", None, SensorStateClass.TOTAL, True],
"maxStressLevel": ["Max Stress Level", "lvl", "mdi:flash-alert", None, SensorStateClass.TOTAL, True],
"stressQualifier": ["Stress Qualifier", None, "mdi:flash-alert", None, None, False],
"stressDuration": ["Stress Duration", TIME_MINUTES, "mdi:flash-alert", None, SensorStateClass.TOTAL, False],
"restStressDuration": [
"Rest Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"activityStressDuration": [
"Activity Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"uncategorizedStressDuration": [
"Uncat. Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"totalStressDuration": [
"Total Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"lowStressDuration": [
"Low Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"mediumStressDuration": [
"Medium Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"highStressDuration": [
"High Stress Duration",
TIME_MINUTES,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
True,
],
"stressPercentage": [
"Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"restStressPercentage": [
"Rest Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"activityStressPercentage": [
"Activity Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"uncategorizedStressPercentage": [
"Uncat. Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"lowStressPercentage": [
"Low Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"mediumStressPercentage": [
"Medium Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"highStressPercentage": [
"High Stress Percentage",
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
False,
],
"moderateIntensityMinutes": [
"Moderate Intensity",
TIME_MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
False,
],
"vigorousIntensityMinutes": [
"Vigorous Intensity",
TIME_MINUTES,
"mdi:run-fast",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
False,
],
"intensityMinutesGoal": [
"Intensity Goal",
TIME_MINUTES,
"mdi:run-fast",
None,
SensorStateClass.TOTAL,
False,
],
"bodyBatteryChargedValue": [
"Body Battery Charged",
PERCENTAGE,
"mdi:battery-charging-100",
None,
SensorStateClass.TOTAL,
True,
],
"bodyBatteryDrainedValue": [
"Body Battery Drained",
PERCENTAGE,
"mdi:battery-alert-variant-outline",
None,
SensorStateClass.TOTAL,
True,
],
"bodyBatteryHighestValue": [
"Body Battery Highest",
PERCENTAGE,
"mdi:battery-heart",
None,
SensorStateClass.TOTAL,
True,
],
"bodyBatteryLowestValue": [
"Body Battery Lowest",
PERCENTAGE,
"mdi:battery-heart-outline",
None,
SensorStateClass.TOTAL,
True,
],
"bodyBatteryMostRecentValue": [
"Body Battery Most Recent",
PERCENTAGE,
"mdi:battery-positive",
None,
SensorStateClass.TOTAL,
True,
],
"averageSpo2": ["Average SPO2", PERCENTAGE, "mdi:diabetes", None, SensorStateClass.TOTAL, True],
"lowestSpo2": ["Lowest SPO2", PERCENTAGE, "mdi:diabetes", None, SensorStateClass.TOTAL, True],
"latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, SensorStateClass.TOTAL, True],
"latestSpo2ReadingTimeLocal": [
"Latest SPO2 Time",
None,
"mdi:diabetes",
SensorDeviceClass.TIMESTAMP,
None,
False,
],
"averageMonitoringEnvironmentAltitude": [
"Average Altitude",
PERCENTAGE,
"mdi:image-filter-hdr",
None,
SensorStateClass.TOTAL,
False,
],
"highestRespirationValue": [
"Highest Respiration",
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
False,
],
"lowestRespirationValue": [
"Lowest Respiration",
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
False,
],
"latestRespirationValue": [
"Latest Respiration",
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
False,
],
"latestRespirationTimeGMT": [
"Latest Respiration Update",
None,
"mdi:progress-clock",
SensorDeviceClass.TIMESTAMP,
None,
False,
],
"weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"bmi": ["BMI", "bmi", "mdi:food", None, SensorStateClass.TOTAL, False],
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
"bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, SensorStateClass.TOTAL, False],
"boneMass": ["Bone Mass", MASS_KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"physiqueRating": ["Physique Rating", None, "mdi:numeric", None, SensorStateClass.TOTAL, False],
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
"metabolicAge": ["Metabolic Age", TIME_YEARS, "mdi:calendar-heart", None, SensorStateClass.TOTAL, False],
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", SensorDeviceClass.TIMESTAMP, None, True],
DAY_TO_NUMBER = {
"Mo": 1,
"M": 1,
"Tu": 2,
"We": 3,
"W": 3,
"Th": 4,
"Fr": 5,
"F": 5,
"Sa": 6,
"Su": 7,
}
LEVEL_POINTS = {
1: 0,
2: 20,
3: 60,
4: 140,
5: 300,
6: 620,
7: 1260,
8: 2540,
9: 5100,
10: 10220,
}
GEAR_ICONS = {
"Shoes": "mdi:shoe-sneaker",
"Bike": "mdi:bike",
"Other": "mdi:basketball",
"Golf Clubs": "mdi:golf",
}
class ServiceSetting:
"""Options for the service settings."""
ONLY_THIS_AS_DEFAULT = "set this as default, unset others"
DEFAULT = "set as default"
UNSET_DEFAULT = "unset default"
class Gear:
"""Gear attribute keys."""
UUID = "uuid"
TYPE_KEY = "typeKey"
TYPE_ID = "typeId"
USERPROFILE_ID = "userProfileId"
ACTIVITY_TYPE_PK = "activityTypePk"

View File

@@ -0,0 +1,527 @@
"""DataUpdateCoordinator for Garmin Connect integration."""
import asyncio
import logging
from collections.abc import Awaitable
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo
import requests
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
DAY_TO_NUMBER,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
LEVEL_POINTS,
Gear,
)
_LOGGER = logging.getLogger(__name__)
class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
"""Garmin Connect Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
self.entry = entry
self.hass = hass
self._in_china = hass.config.country == "CN"
self.time_zone = hass.config.time_zone
self.api = Garmin(is_cn=self._in_china)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=DEFAULT_UPDATE_INTERVAL,
config_entry=entry,
)
async def async_login(self) -> bool:
"""Authenticate with Garmin Connect using stored token."""
try:
if CONF_TOKEN not in self.entry.data:
raise ConfigEntryAuthFailed(
"Token not found in config entry. Please reauthenticate."
)
await self.hass.async_add_executor_job(self.api.login, self.entry.data[CONF_TOKEN])
except ConfigEntryAuthFailed:
raise
except GarminConnectAuthenticationError as err:
_LOGGER.error("Authentication error: %s", err.response.text)
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError as err:
_LOGGER.error("Too many requests during login: %s", err)
return False
except GarminConnectConnectionError as err:
_LOGGER.error("Connection error during login: %s", err)
raise ConfigEntryNotReady from err
except requests.exceptions.HTTPError as err:
if err.response.status_code == 401:
raise ConfigEntryAuthFailed from err
if err.response.status_code == 429:
_LOGGER.error("Too many requests: %s", err.response.text)
return False
_LOGGER.error("HTTP error during login: %s", err)
return False
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unknown error during login: %s", err)
return False
return True
async def _async_update_data(self) -> dict:
"""Fetch data from Garmin Connect."""
summary = {}
body = {}
alarms = {}
gear = {}
gear_stats: list[Any] = []
gear_defaults = {}
activity_types = {}
last_activities = []
daily_steps: list[dict[str, Any]] = []
yesterday_steps = None
yesterday_distance = None
weekly_step_avg = None
weekly_distance_avg = None
sleep_data = {}
sleep_score = None
sleep_time_seconds = None
deep_sleep_seconds = None
light_sleep_seconds = None
rem_sleep_seconds = None
awake_sleep_seconds = None
hrv_data = {}
hrv_status = {"status": "unknown"}
endurance_data = {}
endurance_status = {"overallScore": None}
hill_data = {}
hill_status = {"overallScore": None}
menstrual_data = {}
next_alarms: list[str] | None = []
today = datetime.now(ZoneInfo(self.time_zone)).date()
current_hour = datetime.now(ZoneInfo(self.time_zone)).hour
yesterday_date = (today - timedelta(days=1)).isoformat()
_LOGGER.debug(
"Fetching data for date: %s (timezone: %s, hour: %s)",
today.isoformat(),
self.time_zone,
current_hour,
)
try:
summary = await self.hass.async_add_executor_job(
self.api.get_user_summary, today.isoformat()
)
# Smart fallback: detect when Garmin servers haven't populated today's data yet
# Key signal: dailyStepGoal is None means the day data structure doesn't exist
# This works regardless of timezone - no fixed hour window needed
today_data_not_ready = not summary or summary.get("dailyStepGoal") is None
if today_data_not_ready:
_LOGGER.debug(
"Today's data not ready (dailyStepGoal=%s), fetching yesterday's data",
summary.get("dailyStepGoal") if summary else None,
)
yesterday_summary = await self.hass.async_add_executor_job(
self.api.get_user_summary, yesterday_date
)
if yesterday_summary and yesterday_summary.get("dailyStepGoal") is not None:
summary = yesterday_summary
_LOGGER.debug("Using yesterday's summary data as fallback")
_LOGGER.debug(
"Summary data for %s: totalSteps=%s, dailyStepGoal=%s, lastSync=%s",
today.isoformat(),
summary.get("totalSteps"),
summary.get("dailyStepGoal"),
summary.get("lastSyncTimestampGMT"),
)
# Fetch last 7 days steps for weekly average and yesterday's final count
week_ago = (today - timedelta(days=7)).isoformat()
yesterday = (today - timedelta(days=1)).isoformat()
daily_steps = await self.hass.async_add_executor_job(
self.api.get_daily_steps, week_ago, yesterday
)
# Process daily steps for yesterday values and weekly averages
if daily_steps:
# Yesterday is the last item in the list
if daily_steps:
yesterday_data = daily_steps[-1]
yesterday_steps = yesterday_data.get("totalSteps")
yesterday_distance = yesterday_data.get("totalDistance")
# Calculate weekly averages
total_steps = sum(d.get("totalSteps", 0) for d in daily_steps)
total_distance = sum(d.get("totalDistance", 0) for d in daily_steps)
days_count = len(daily_steps)
if days_count > 0:
weekly_step_avg = round(total_steps / days_count)
weekly_distance_avg = round(total_distance / days_count)
body = await self.hass.async_add_executor_job(
self.api.get_body_composition, today.isoformat()
)
last_activities = await self.hass.async_add_executor_job(
self.api.get_activities_by_date,
(today - timedelta(days=7)).isoformat(),
(today + timedelta(days=1)).isoformat(),
)
summary["lastActivities"] = last_activities
last_activity = last_activities[0] if last_activities else {}
# Fetch polyline for last activity if it has GPS data
if last_activity and last_activity.get("hasPolyline"):
try:
activity_id = last_activity.get("activityId")
activity_details = await self.hass.async_add_executor_job(
self.api.get_activity_details, activity_id, 100, 4000
)
if activity_details:
polyline_data = activity_details.get("geoPolylineDTO", {})
raw_polyline = polyline_data.get("polyline", [])
# Simplify polyline to just lat/lon to reduce attribute size
# Full polyline with all fields can be 350+ bytes per point
# Simplified: ~50 bytes per point, fits HA 16KB limit
last_activity["polyline"] = [
{"lat": p.get("lat"), "lon": p.get("lon")}
for p in raw_polyline
if p.get("lat") is not None and p.get("lon") is not None
]
except Exception as err:
_LOGGER.debug("Failed to fetch polyline for activity: %s", err)
summary["lastActivity"] = last_activity
# Fetch workouts (scheduled/planned training sessions)
try:
workouts = await self.hass.async_add_executor_job(self.api.get_workouts, 0, 10)
summary["workouts"] = (
workouts.get("workouts", []) if isinstance(workouts, dict) else workouts
)
summary["lastWorkout"] = summary["workouts"][0] if summary["workouts"] else {}
except Exception:
summary["workouts"] = []
summary["lastWorkout"] = {}
badges = await self.hass.async_add_executor_job(self.api.get_earned_badges)
summary["badges"] = badges
# Fetch training readiness
try:
training_readiness = await self.hass.async_add_executor_job(
self.api.get_training_readiness, today.isoformat()
)
summary["trainingReadiness"] = training_readiness
except Exception:
summary["trainingReadiness"] = {}
# Fetch morning training readiness (AFTER_WAKEUP_RESET context)
try:
morning_training_readiness = await self.hass.async_add_executor_job(
self.api.get_morning_training_readiness, today.isoformat()
)
summary["morningTrainingReadiness"] = morning_training_readiness or {}
except Exception:
summary["morningTrainingReadiness"] = {}
# Fetch training status
try:
training_status = await self.hass.async_add_executor_job(
self.api.get_training_status, today.isoformat()
)
summary["trainingStatus"] = training_status
except Exception:
summary["trainingStatus"] = {}
# Fetch lactate threshold
try:
lactate_threshold = await self.hass.async_add_executor_job(
self.api.get_lactate_threshold
)
summary["lactateThreshold"] = lactate_threshold
except Exception:
summary["lactateThreshold"] = {}
# Fetch goals (active, future, past)
try:
active_goals = await self.hass.async_add_executor_job(self.api.get_goals, "active")
summary["activeGoals"] = active_goals or []
except Exception:
summary["activeGoals"] = []
try:
future_goals = await self.hass.async_add_executor_job(self.api.get_goals, "future")
summary["futureGoals"] = future_goals or []
except Exception:
summary["futureGoals"] = []
try:
past_goals = await self.hass.async_add_executor_job(self.api.get_goals, "past")
# Limit to last 10 completed goals
summary["goalsHistory"] = (past_goals or [])[:10]
except Exception:
summary["goalsHistory"] = []
user_points = sum(badge["badgePoints"] * badge["badgeEarnedNumber"] for badge in badges)
summary["userPoints"] = user_points
user_level = 0
for level, points in LEVEL_POINTS.items():
if user_points >= points:
user_level = level
summary["userLevel"] = user_level
alarms = await self.hass.async_add_executor_job(self.api.get_device_alarms)
next_alarms = calculate_next_active_alarms(alarms, self.time_zone)
activity_types = await self.hass.async_add_executor_job(self.api.get_activity_types)
sleep_data = await self.hass.async_add_executor_job(
self.api.get_sleep_data, today.isoformat()
)
hrv_data = await self.hass.async_add_executor_job(
self.api.get_hrv_data, today.isoformat()
)
endurance_data = await self.hass.async_add_executor_job(
self.api.get_endurance_score, today.isoformat()
)
hill_data = await self.hass.async_add_executor_job(
self.api.get_hill_score, today.isoformat()
)
try:
menstrual_data = await self.hass.async_add_executor_job(
self.api.get_menstrual_data_for_date, today.isoformat()
)
# API returns None when not enabled - convert to empty dict
if menstrual_data is None:
menstrual_data = {}
except Exception: # pylint: disable=broad-except
# Menstrual data not available for this user
menstrual_data = {}
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
try:
if Gear.USERPROFILE_ID in summary:
gear = await self.hass.async_add_executor_job(
self.api.get_gear, summary[Gear.USERPROFILE_ID]
)
fitnessage_data = await self.hass.async_add_executor_job(
self.api.get_fitnessage_data, today.isoformat()
)
hydration_data = await self.hass.async_add_executor_job(
self.api.get_hydration_data, today.isoformat()
)
# Fetch blood pressure data (last 30 days for latest reading)
blood_pressure_data = {}
try:
bp_response = await self.hass.async_add_executor_job(
self.api.get_blood_pressure,
(today - timedelta(days=30)).isoformat(),
today.isoformat(),
)
# API returns dict with measurementSummaries containing measurements
if bp_response and isinstance(bp_response, dict):
summaries = bp_response.get("measurementSummaries", [])
if summaries:
# Get measurements from the most recent day
latest_summary = summaries[-1]
measurements = latest_summary.get("measurements", [])
if measurements:
latest_bp = measurements[-1]
blood_pressure_data = {
"bpSystolic": latest_bp.get("systolic"),
"bpDiastolic": latest_bp.get("diastolic"),
"bpPulse": latest_bp.get("pulse"),
"bpMeasurementTime": latest_bp.get("measurementTimestampLocal"),
}
except Exception as err:
_LOGGER.debug("Blood pressure data not available: %s", err)
except GarminConnectAuthenticationError as err:
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError:
return {}
except GarminConnectConnectionError as err:
raise ConfigEntryNotReady from err
except requests.exceptions.HTTPError as err:
if err.response.status_code == 401:
raise ConfigEntryAuthFailed from err
if err.response.status_code == 429:
return {}
_LOGGER.error("HTTP error during update: %s", err)
return {}
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Unknown error during update: %s", err)
return {}
try:
if gear:
tasks: list[Awaitable] = [
self.hass.async_add_executor_job(self.api.get_gear_stats, gear_item[Gear.UUID])
for gear_item in gear
]
gear_stats = await asyncio.gather(*tasks)
if Gear.USERPROFILE_ID in summary:
gear_defaults = await self.hass.async_add_executor_job(
self.api.get_gear_defaults, summary[Gear.USERPROFILE_ID]
)
except GarminConnectAuthenticationError as err:
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError as err:
raise ConfigEntryNotReady from err
except GarminConnectConnectionError as err:
raise ConfigEntryNotReady from err
except requests.exceptions.HTTPError:
pass
except (KeyError, TypeError, ValueError, ConnectionError) as err:
_LOGGER.debug("Error fetching gear data: %s", err)
try:
sleep_score = sleep_data["dailySleepDTO"]["sleepScores"]["overall"]["value"]
except KeyError:
pass
try:
sleep_time_seconds = sleep_data["dailySleepDTO"]["sleepTimeSeconds"]
except KeyError:
pass
try:
deep_sleep_seconds = sleep_data["dailySleepDTO"]["deepSleepSeconds"]
except KeyError:
pass
try:
light_sleep_seconds = sleep_data["dailySleepDTO"]["lightSleepSeconds"]
except KeyError:
pass
try:
rem_sleep_seconds = sleep_data["dailySleepDTO"]["remSleepSeconds"]
except KeyError:
pass
try:
awake_sleep_seconds = sleep_data["dailySleepDTO"]["awakeSleepSeconds"]
except KeyError:
pass
try:
if hrv_data and "hrvSummary" in hrv_data:
hrv_status = hrv_data["hrvSummary"]
except KeyError:
pass
try:
if endurance_data and "overallScore" in endurance_data:
endurance_status = endurance_data
except KeyError:
pass
try:
if hill_data and "overallScore" in hill_data:
hill_status = hill_data
except KeyError:
pass
return {
**summary,
**body["totalAverage"],
"nextAlarm": next_alarms,
"gear": gear,
"gearStats": gear_stats,
"activityTypes": activity_types,
"gearDefaults": gear_defaults,
"sleepScore": sleep_score,
"sleepTimeSeconds": sleep_time_seconds,
"deepSleepSeconds": deep_sleep_seconds,
"lightSleepSeconds": light_sleep_seconds,
"remSleepSeconds": rem_sleep_seconds,
"awakeSleepSeconds": awake_sleep_seconds,
"yesterdaySteps": yesterday_steps,
"yesterdayDistance": yesterday_distance,
"weeklyStepAvg": weekly_step_avg,
"weeklyDistanceAvg": weekly_distance_avg,
"hrvStatus": hrv_status,
"enduranceScore": endurance_status,
"hillScore": hill_status,
**fitnessage_data,
**hydration_data,
**menstrual_data,
**blood_pressure_data,
}
def calculate_next_active_alarms(alarms: Any, time_zone: str) -> list[str] | None:
"""Calculate the next scheduled active alarms."""
active_alarms: list[str] = []
if not alarms:
return active_alarms
now = datetime.now(ZoneInfo(time_zone))
for alarm_setting in alarms:
if alarm_setting["alarmMode"] != "ON":
continue
for day in alarm_setting["alarmDays"]:
alarm_time = alarm_setting["alarmTime"]
if day == "ONCE":
midnight = datetime.combine(
now.date(), datetime.min.time(), tzinfo=ZoneInfo(time_zone)
)
alarm = midnight + timedelta(minutes=alarm_time)
if alarm < now:
alarm += timedelta(days=1)
else:
start_of_week = datetime.combine(
now.date() - timedelta(days=now.date().isoweekday() - 1),
datetime.min.time(),
tzinfo=ZoneInfo(time_zone),
)
alarm = start_of_week + timedelta(days=DAY_TO_NUMBER[day] - 1, minutes=alarm_time)
if alarm < now:
alarm += timedelta(days=7)
active_alarms.append(alarm.isoformat())
return sorted(active_alarms) if active_alarms else None

View File

@@ -0,0 +1,24 @@
"""Base entity for Garmin Connect integration."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
class GarminConnectEntity(CoordinatorEntity):
"""Base entity for Garmin Connect."""
_attr_has_entity_name = True
def __init__(self, coordinator, unique_id: str) -> None:
"""Initialize Garmin Connect entity."""
super().__init__(coordinator)
self._unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name="Garmin Connect",
manufacturer="Garmin",
model="Garmin Connect",
entry_type=None,
)

View File

@@ -0,0 +1,60 @@
{
"entity": {
"sensor": {
"steps": {
"default": "mdi:shoe-print"
},
"daily_step_goal": {
"default": "mdi:flag-checkered"
},
"yesterday_steps": {
"default": "mdi:shoe-print"
},
"weekly_step_average": {
"default": "mdi:chart-line"
},
"floors_ascended": {
"default": "mdi:stairs-up"
},
"floors_descended": {
"default": "mdi:stairs-down"
},
"floors_goal": {
"default": "mdi:flag-checkered"
},
"total_calories": {
"default": "mdi:food"
},
"active_calories": {
"default": "mdi:fire"
},
"resting_heart_rate": {
"default": "mdi:heart-pulse"
},
"max_heart_rate": {
"default": "mdi:heart-flash"
},
"min_heart_rate": {
"default": "mdi:heart-minus"
},
"stress_level": {
"default": "mdi:head-snowflake"
},
"body_battery": {
"default": "mdi:battery-heart"
},
"sleep_duration": {
"default": "mdi:sleep"
},
"weight": {
"default": "mdi:scale-bathroom"
},
"bmi": {
"default": "mdi:human"
},
"device_last_synced": {
"default": "mdi:sync"
}
}
}
}

View File

@@ -1,11 +1,16 @@
{
"domain": "garmin_connect",
"name": "Garmin Connect",
"codeowners": ["@cyberjunky"],
"codeowners": [
"@cyberjunky"
],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/cyberjunky/home-assistant-garmin_connect",
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"iot_class": "cloud_polling",
"requirements": ["garminconnect==0.1.55"],
"version": "0.2.15"
}
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"requirements": [
"garminconnect>=0.2.38"
],
"version": "1.0.0-beta-01"
}

View File

@@ -0,0 +1,38 @@
# Garmin Connect Quality Scale
# This file tracks the integration quality scale for Home Assistant Core.
# See: https://developers.home-assistant.io/docs/core/integration-quality-scale/
rules:
# Bronze tier (minimum for Core)
config-flow: done
unique-config-entry: done
entity-unique-id: done
has-entity-name: done
appropriate-polling: done
action-exceptions: done
test-before-configure: done
runtime-data: done
# Silver tier
reauthentication-flow: done
entity-translations: done
parallel-updates: done
config-entry-unloading: done
log-when-unavailable: done
entity-unavailable: done
# Gold tier
entity-device-class: done
reconfiguration-flow: exempt # Credentials handled by reauthentication flow
dynamic-devices: exempt # Single cloud account, not device-based
discovery: exempt # Cloud service, not discoverable
diagnostics: done
exception-translations: done
icon-translations: done
stale-devices: exempt # Single device per config entry
# Platinum tier
async-dependency: todo
inject-websession: todo
strict-typing: todo

View File

@@ -1,79 +1,214 @@
"""Platform for Garmin Connect integration."""
from __future__ import annotations
import logging
import datetime
import pytz
from tzlocal import get_localzone
import logging
import voluptuous as vol
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID
from homeassistant.const import CONF_ID, UnitOfLength
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import IntegrationError
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .alarm_util import calculate_next_active_alarms
from .const import (
DATA_COORDINATOR,
DOMAIN as GARMIN_DOMAIN,
GARMIN_ENTITY_LIST,
DOMAIN,
GEAR_ICONS,
Gear,
ServiceSetting,
)
from .entity import GarminConnectEntity
from .sensor_descriptions import (
ALL_SENSOR_DESCRIPTIONS,
)
_LOGGER = logging.getLogger(__name__)
# Limit parallel updates to prevent API rate limiting
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities) -> None:
"""Set up Garmin Connect sensor based on a config entry."""
coordinator: DataUpdateCoordinator = hass.data[GARMIN_DOMAIN][entry.entry_id][
DATA_COORDINATOR
]
coordinator: DataUpdateCoordinator = entry.runtime_data
unique_id = entry.data[CONF_ID]
entities = []
for (
sensor_type,
(name, unit, icon, device_class, state_class, enabled_by_default),
) in GARMIN_ENTITY_LIST.items():
# Add main sensors using entity descriptions
for description in ALL_SENSOR_DESCRIPTIONS:
_LOGGER.debug(
"Registering entity: %s, %s, %s, %s, %s, %s, %s",
sensor_type,
name,
unit,
icon,
device_class,
state_class,
enabled_by_default,
"Registering entity: %s (%s)",
description.key,
description.translation_key,
)
entities.append(
GarminConnectSensor(
coordinator,
unique_id,
sensor_type,
name,
unit,
icon,
device_class,
state_class,
enabled_by_default,
description,
)
)
# Add gear sensors
if "gear" in coordinator.data:
for gear_item in coordinator.data["gear"]:
name = gear_item["displayName"]
sensor_type = gear_item["gearTypeName"]
uuid = gear_item[Gear.UUID]
unit = UnitOfLength.KILOMETERS
icon = GEAR_ICONS.get(sensor_type, "mdi:shoe-print")
device_class = SensorDeviceClass.DISTANCE
state_class = SensorStateClass.TOTAL
enabled_by_default = True
_LOGGER.debug(
"Registering gear entity: %s, %s, %s",
sensor_type,
name,
uuid,
)
entities.append(
GarminConnectGearSensor(
coordinator,
unique_id,
sensor_type,
name,
unit,
icon,
uuid,
device_class,
state_class,
enabled_by_default,
)
)
async_add_entities(entities)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"set_active_gear",
{
vol.Required("activity_type"): str,
vol.Required("setting"): str,
},
"set_active_gear",
)
class GarminConnectSensor(CoordinatorEntity, SensorEntity):
class GarminConnectSensor(GarminConnectEntity, SensorEntity, RestoreEntity):
"""Representation of a Garmin Connect Sensor."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
unique_id: str,
description,
):
"""Initialize a Garmin Connect sensor."""
super().__init__(coordinator, unique_id)
self.entity_description = description
self._attr_unique_id = f"{unique_id}_{description.key}"
self._last_known_value: str | int | float | None = None
async def async_added_to_hass(self) -> None:
"""Restore last known value when added to hass."""
await super().async_added_to_hass()
if (last_state := await self.async_get_last_state()) is not None:
if last_state.state not in ("unknown", "unavailable"):
self._last_known_value = last_state.state
@property
def native_value(self):
"""Return the state of the sensor."""
if not self.coordinator.data:
# Only return last known value if preserve_value is enabled
if self.entity_description.preserve_value:
return self._last_known_value
return None
# Use custom value function if provided in description
if self.entity_description.value_fn:
value = self.entity_description.value_fn(self.coordinator.data)
else:
value = self.coordinator.data.get(self.entity_description.key)
if value is None:
# Return last known value if preserve_value enabled (e.g., weight at midnight)
if self.entity_description.preserve_value:
return self._last_known_value
return None
# Handle timestamp device class
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
if value:
try:
# Parse ISO format timestamp and set to UTC (GMT)
parsed = datetime.datetime.fromisoformat(value)
# If naive, assume UTC since Garmin returns GMT timestamps
if parsed.tzinfo is None:
value = parsed.replace(tzinfo=datetime.UTC)
else:
value = parsed
except (ValueError, TypeError):
_LOGGER.debug("Could not parse timestamp: %s", value)
value = None
# Preserve int types, only round floats
if isinstance(value, int):
self._last_known_value = value
return value
if isinstance(value, float):
# Round floats to 1 decimal place, but return int if it's a whole number
rounded = round(value, 1)
if rounded == int(rounded):
self._last_known_value = int(rounded)
return int(rounded)
self._last_known_value = rounded
return rounded
self._last_known_value = value
return value
@property
def extra_state_attributes(self):
"""Return additional state attributes."""
if not self.coordinator.data:
return {}
# Use custom attributes function if provided in description
if self.entity_description.attributes_fn:
return self.entity_description.attributes_fn(self.coordinator.data)
# Default: just return last_synced
return {
"last_synced": self.coordinator.data.get("lastSyncTimestampGMT"),
}
@property
def available(self) -> bool:
"""Return True if entity is available."""
# Entity is available if coordinator has data
# Individual sensors will show "Unknown" if their value is None
return bool(super().available and self.coordinator.data)
class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Garmin Connect Gear Sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator,
@@ -82,11 +217,12 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
name,
unit,
icon,
device_class,
state_class,
uuid,
device_class: None,
state_class: None,
enabled_default: bool = True,
):
"""Initialize a Garmin Connect sensor."""
"""Initialize a Garmin Connect Gear sensor."""
super().__init__(coordinator)
self._unique_id = unique_id
@@ -94,73 +230,78 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
self._device_class = device_class
self._state_class = state_class
self._enabled_default = enabled_default
self._uuid = uuid
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 = state_class
self._attr_unique_id = f"{self._unique_id}_{self._uuid}"
self._attr_state_class = self._state_class
@property
def uuid(self):
"""Return the entity uuid"""
return self._uuid
@property
def native_value(self):
"""Return the state of the sensor."""
if not self.coordinator.data or not self.coordinator.data[self._type]:
if not self.coordinator.data or not self._stats():
return None
value = self.coordinator.data[self._type]
if "Duration" in self._type or "Seconds" in self._type:
value = value // 60
elif "Mass" in self._type or self._type == "weight":
value = value / 1000
elif self._type == "nextAlarm":
active_alarms = calculate_next_active_alarms(
self.coordinator.data[self._type]
)
if active_alarms:
date_time_obj = datetime.datetime.strptime(active_alarms[0], "%Y-%m-%dT%H:%M:%S")
tz = get_localzone()
timezone = pytz.timezone(tz.zone)
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 == SensorDeviceClass.TIMESTAMP:
date_time_obj = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
tz = get_localzone()
timezone = pytz.timezone(tz.zone)
timezone_date_time_obj = timezone.localize(date_time_obj)
return timezone_date_time_obj
return round(value, 2)
value = self._stats()["totalDistance"]
return round(value / 1000, 2)
@property
def extra_state_attributes(self):
"""Return attributes for sensor."""
if not self.coordinator.data:
"""Return additional state attributes."""
gear = self._gear()
stats = self._stats()
gear_defaults = self._gear_defaults()
activity_types = self.coordinator.data["activityTypes"]
default_for_activity = self._activity_names_for_gear_defaults(
gear_defaults, activity_types)
if not self.coordinator.data or not gear or not stats:
return {}
attributes = {
"gear_uuid": self._uuid,
"last_synced": self.coordinator.data["lastSyncTimestampGMT"],
"total_activities": stats["totalActivities"],
"create_date": stats["createDate"],
"update_date": stats["updateDate"],
"date_begin": gear["dateBegin"],
"date_end": gear["dateEnd"],
"gear_make_name": gear["gearMakeName"],
"gear_model_name": gear["gearModelName"],
"gear_status_name": gear["gearStatusName"],
"custom_make_model": gear["customMakeModel"],
"maximum_meters": gear["maximumMeters"],
}
if self._type == "nextAlarm":
attributes["next_alarms"] = calculate_next_active_alarms(
self.coordinator.data[self._type]
)
attributes["default_for_activity"] = (
", ".join(default_for_activity) if default_for_activity else "None"
)
return attributes
def _activity_names_for_gear_defaults(self, gear_defaults, activity_types):
"""Get activity names for gear defaults."""
activity_type_ids = [d["activityTypePk"] for d in gear_defaults]
return [a["typeKey"] for a in activity_types if a["typeId"] in activity_type_ids]
@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
return {
"identifiers": {(GARMIN_DOMAIN, self._unique_id)},
"name": "Garmin Connect",
"manufacturer": "Garmin Connect",
}
return DeviceInfo(
identifiers={(DOMAIN, self._unique_id)},
name="Garmin Connect",
manufacturer="Garmin",
model="Garmin Connect",
entry_type=None,
)
@property
def entity_registry_enabled_default(self) -> bool:
@@ -170,8 +311,79 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.coordinator.data
and self._type in self.coordinator.data
return bool(super().available and self.coordinator.data and self._gear())
def _stats(self):
"""Get gear statistics from garmin"""
for gear_stats_item in self.coordinator.data["gearStats"]:
if gear_stats_item[Gear.UUID] == self._uuid:
return gear_stats_item
def _gear(self):
"""Get gear from garmin"""
for gear_item in self.coordinator.data["gear"]:
if gear_item[Gear.UUID] == self._uuid:
return gear_item
def _gear_defaults(self):
"""Return gear defaults for this UUID."""
return list(
filter(
lambda d: d[Gear.UUID] == self.uuid and d["defaultGear"] is True,
self.coordinator.data["gearDefaults"],
)
)
async def set_active_gear(self, **kwargs):
"""Set this gear as active for an activity type."""
activity_type = kwargs.get("activity_type")
setting = kwargs.get("setting")
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update")
try:
activity_type_id = next(
filter(
lambda a: a[Gear.TYPE_KEY] == activity_type,
self.coordinator.data["activityTypes"],
)
)[Gear.TYPE_ID]
if setting != ServiceSetting.ONLY_THIS_AS_DEFAULT:
await self.hass.async_add_executor_job(
self.coordinator.api.set_gear_default,
activity_type_id,
self._uuid,
setting == ServiceSetting.DEFAULT,
)
else:
old_default_state = await self.hass.async_add_executor_job(
self.coordinator.api.get_gear_defaults,
self.coordinator.data[Gear.USERPROFILE_ID],
)
to_deactivate = list(
filter(
lambda o: o[Gear.ACTIVITY_TYPE_PK] == activity_type_id
and o[Gear.UUID] != self._uuid,
old_default_state,
)
)
for active_gear in to_deactivate:
await self.hass.async_add_executor_job(
self.coordinator.api.set_gear_default,
activity_type_id,
active_gear[Gear.UUID],
False,
)
await self.hass.async_add_executor_job(
self.coordinator.api.set_gear_default,
activity_type_id,
self._uuid,
True,
)
except Exception as err:
raise IntegrationError(
f"Failed to set active gear: {err}"
) from err

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
"""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
from homeassistant.exceptions import HomeAssistantError
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(
{
vol.Required("weight"): vol.Coerce(float),
vol.Optional("timestamp"): cv.string,
vol.Optional("percent_fat"): vol.Coerce(float),
vol.Optional("percent_hydration"): vol.Coerce(float),
vol.Optional("visceral_fat_mass"): vol.Coerce(float),
vol.Optional("bone_mass"): vol.Coerce(float),
vol.Optional("muscle_mass"): vol.Coerce(float),
vol.Optional("basal_met"): vol.Coerce(float),
vol.Optional("active_met"): vol.Coerce(float),
vol.Optional("physique_rating"): vol.Coerce(float),
vol.Optional("metabolic_age"): vol.Coerce(float),
vol.Optional("visceral_fat_rating"): vol.Coerce(float),
vol.Optional("bmi"): vol.Coerce(float),
}
)
ADD_BLOOD_PRESSURE_SCHEMA = vol.Schema(
{
vol.Required("systolic"): int,
vol.Required("diastolic"): int,
vol.Required("pulse"): int,
vol.Optional("timestamp"): cv.string,
vol.Optional("notes"): cv.string,
}
)
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,
}
)
SERVICE_UPLOAD_ACTIVITY = "upload_activity"
UPLOAD_ACTIVITY_SCHEMA = vol.Schema(
{
vol.Required("file_path"): cv.string,
}
)
SERVICE_ADD_GEAR_TO_ACTIVITY = "add_gear_to_activity"
ADD_GEAR_TO_ACTIVITY_SCHEMA = vol.Schema(
{
vol.Required("activity_id"): vol.Coerce(int),
vol.Required("gear_uuid"): 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:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="no_integration_configured",
)
# Use the first config entry's coordinator
entry = entries[0]
if not hasattr(entry, "runtime_data") or entry.runtime_data is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="integration_not_loaded",
)
return entry.runtime_data # type: ignore[no-any-return]
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up Garmin Connect services."""
async def handle_add_body_composition(call: ServiceCall) -> None:
"""Handle add_body_composition service call."""
coordinator = _get_coordinator(hass)
weight = call.data.get("weight")
timestamp = call.data.get("timestamp")
percent_fat = call.data.get("percent_fat")
percent_hydration = call.data.get("percent_hydration")
visceral_fat_mass = call.data.get("visceral_fat_mass")
bone_mass = call.data.get("bone_mass")
muscle_mass = call.data.get("muscle_mass")
basal_met = call.data.get("basal_met")
active_met = call.data.get("active_met")
physique_rating = call.data.get("physique_rating")
metabolic_age = call.data.get("metabolic_age")
visceral_fat_rating = call.data.get("visceral_fat_rating")
bmi = call.data.get("bmi")
if not await coordinator.async_login():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="login_failed",
)
try:
await hass.async_add_executor_job(
coordinator.api.add_body_composition,
timestamp,
weight,
percent_fat,
percent_hydration,
visceral_fat_mass,
bone_mass,
muscle_mass,
basal_met,
active_met,
physique_rating,
metabolic_age,
visceral_fat_rating,
bmi,
)
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="add_body_composition_failed",
translation_placeholders={"error": str(err)},
) from err
async def handle_add_blood_pressure(call: ServiceCall) -> None:
"""Handle add_blood_pressure service call."""
coordinator = _get_coordinator(hass)
systolic = call.data.get("systolic")
diastolic = call.data.get("diastolic")
pulse = call.data.get("pulse")
timestamp = call.data.get("timestamp")
notes = call.data.get("notes")
if not await coordinator.async_login():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="login_failed",
)
try:
await hass.async_add_executor_job(
coordinator.api.set_blood_pressure,
systolic,
diastolic,
pulse,
timestamp,
notes,
)
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="add_blood_pressure_failed",
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
async def handle_add_gear_to_activity(call: ServiceCall) -> None:
"""Handle add_gear_to_activity service call."""
coordinator = _get_coordinator(hass)
activity_id = call.data.get("activity_id")
gear_uuid = call.data.get("gear_uuid")
if not await coordinator.async_login():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="login_failed",
)
try:
await hass.async_add_executor_job(
coordinator.api.add_gear_to_activity,
gear_uuid,
activity_id,
)
except Exception as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="add_gear_to_activity_failed",
translation_placeholders={"error": str(err)},
) from err
# Register services
hass.services.async_register(
DOMAIN,
SERVICE_ADD_BODY_COMPOSITION,
handle_add_body_composition,
schema=ADD_BODY_COMPOSITION_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_ADD_BLOOD_PRESSURE,
handle_add_blood_pressure,
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,
)
hass.services.async_register(
DOMAIN,
SERVICE_ADD_GEAR_TO_ACTIVITY,
handle_add_gear_to_activity,
schema=ADD_GEAR_TO_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)
hass.services.async_remove(DOMAIN, SERVICE_ADD_GEAR_TO_ACTIVITY)

View File

@@ -0,0 +1,324 @@
set_active_gear:
name: Set active gear
description: Set gear as active/default for an activity type.
target:
entity:
integration: garmin_connect
domain: sensor
fields:
activity_type:
name: Activity type
description: The activity type for which to set the gear as default.
required: true
example: running
default: running
selector:
select:
options:
- running
- cycling
- hiking
- walking
- swimming
- other
setting:
name: Setting
description: The gear setting to apply.
required: true
default: set this as default, unset others
selector:
select:
options:
- set this as default, unset others
- set as default
- unset default
add_body_composition:
name: Add body composition
description: Add body composition metrics to Garmin Connect.
fields:
weight:
name: Weight
description: Weight in kilograms.
required: true
example: 82.3
selector:
number:
min: 0
max: 500
step: 0.1
unit_of_measurement: kg
timestamp:
name: Timestamp
description: When the measurement was recorded (defaults to now).
required: false
example: "2024-01-15T08:30:00"
selector:
text:
bmi:
name: BMI
description: Body Mass Index based on weight and height.
required: false
example: 24.7
selector:
number:
min: 0
max: 100
step: 0.1
percent_fat:
name: Body fat percentage
description: Percentage of body fat.
required: false
example: 23.6
selector:
number:
min: 0
max: 100
step: 0.1
unit_of_measurement: "%"
percent_hydration:
name: Hydration percentage
description: Percentage of body hydration.
required: false
example: 51.2
selector:
number:
min: 0
max: 100
step: 0.1
unit_of_measurement: "%"
visceral_fat_mass:
name: Visceral fat mass
description: Estimated mass of visceral fat in kilograms.
required: false
example: 2.5
selector:
number:
min: 0
max: 50
step: 0.1
unit_of_measurement: kg
bone_mass:
name: Bone mass
description: Estimated mass of bones in kilograms.
required: false
example: 3.2
selector:
number:
min: 0
max: 20
step: 0.1
unit_of_measurement: kg
muscle_mass:
name: Muscle mass
description: Estimated mass of muscle in kilograms.
required: false
example: 35.5
selector:
number:
min: 0
max: 100
step: 0.1
unit_of_measurement: kg
basal_met:
name: Basal metabolism
description: Basal metabolic rate in kcal.
required: false
example: 1900
selector:
number:
min: 0
max: 5000
step: 1
unit_of_measurement: kcal
active_met:
name: Active metabolism
description: Active metabolic rate in kcal.
required: false
example: 840
selector:
number:
min: 0
max: 5000
step: 1
unit_of_measurement: kcal
physique_rating:
name: Physique rating
description: Body physique rating (1-9 scale).
required: false
example: 5
selector:
number:
min: 1
max: 9
step: 1
metabolic_age:
name: Metabolic age
description: Estimated metabolic age in years.
required: false
example: 37
selector:
number:
min: 1
max: 120
step: 1
unit_of_measurement: years
visceral_fat_rating:
name: Visceral fat rating
description: Visceral fat rating (1-59 scale).
required: false
example: 10
selector:
number:
min: 1
max: 59
step: 1
add_blood_pressure:
name: Add blood pressure
description: Add blood pressure measurement to Garmin Connect.
fields:
systolic:
name: Systolic
description: Systolic blood pressure value.
required: true
example: 120
selector:
number:
min: 60
max: 250
step: 1
unit_of_measurement: mmHg
diastolic:
name: Diastolic
description: Diastolic blood pressure value.
required: true
example: 80
selector:
number:
min: 40
max: 150
step: 1
unit_of_measurement: mmHg
pulse:
name: Pulse
description: Pulse rate.
required: true
example: 60
selector:
number:
min: 30
max: 220
step: 1
unit_of_measurement: bpm
timestamp:
name: Timestamp
description: When the measurement was recorded (defaults to now).
required: false
example: "2024-01-15T08:30:00"
selector:
text:
notes:
name: Notes
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:
add_gear_to_activity:
name: Add gear to activity
description: Associate gear (shoes, bike, etc.) with a specific activity.
fields:
activity_id:
name: Activity ID
description: The ID of the activity to add the gear to.
required: true
example: 12345678901
selector:
number:
min: 1
mode: box
gear_uuid:
name: Gear UUID
description: The UUID of the gear to add. Find this in the gear sensor attributes.
required: true
example: "abc12345-def6-7890-ghij-klmn12345678"
selector:
text:

View File

@@ -1,22 +1,69 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"step": {
"user": {
"title": "Garmin Connect login",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"mfa": {
"title": "Garmin Connect MFA code",
"data": {
"mfa_code": "MFA code (6-digits)"
}
},
"reauth_confirm": {
"title": "Garmin Connect reauthentication",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"too_many_requests": "Too many requests, retry later.",
"unknown": "[%key:common::config_flow::error::unknown%]"
"too_many_requests": "Too many requests, retry later",
"unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_mfa_code": "Invalid MFA code"
},
"step": {
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "Enter your credentials."
}
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"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}"
},
"add_gear_to_activity_failed": {
"message": "Failed to add gear to activity: {error}"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key::common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
"too_many_requests": "Massa sol\u00b7licituds, torna-ho a intentar m\u00e9s tard.",
"unknown": "Error inesperat"
},
"step": {
"user": {
"data": {
"password": "Contrasenya",
"username": "Nom d'usuari"
},
"description": "Introdueix les teves credencials.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\u00da\u010det je ji\u017e nastaven"
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
"too_many_requests": "P\u0159\u00edli\u0161 mnoho po\u017eadavk\u016f, opakujte to pozd\u011bji.",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
},
"step": {
"user": {
"data": {
"password": "Heslo",
"username": "U\u017eivatelsk\u00e9 jm\u00e9no"
},
"description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Denne konto er allerede konfigureret."
},
"error": {
"cannot_connect": "Kunne ikke oprette forbindelse - pr\u00f8v igen.",
"invalid_auth": "Ugyldig godkendelse.",
"too_many_requests": "For mange anmodninger - pr\u00f8v igen senere.",
"unknown": "Uventet fejl."
},
"step": {
"user": {
"data": {
"password": "Adgangskode",
"username": "Brugernavn"
},
"description": "Indtast dine legitimationsoplysninger.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Dieses Konto ist bereits konfiguriert."
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"password": "Passwort",
"username": "Benutzername"
},
"description": "Geben Sie Ihre Zugangsdaten ein.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,22 +1,461 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"too_many_requests": "Too many requests, retry later.",
"unknown": "Unexpected error"
},
"step": {
"user": {
"title": "Garmin Connect login",
"data": {
"password": "Password",
"username": "Username"
},
"description": "Enter your credentials."
},
"mfa": {
"title": "Garmin Connect MFA code",
"data": {
"mfa_code": "MFA code (6-digits)"
}
},
"reauth_confirm": {
"title": "Garmin Connect reauthentication",
"data": {
"password": "Password",
"username": "Username"
}
}
},
"abort": {
"already_configured": "Account is already configured",
"reauth_successful": "Reauthentication successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"too_many_requests": "Too many requests, retry later",
"unknown": "Unexpected error",
"invalid_mfa_code": "Invalid MFA code"
}
},
"entity": {
"sensor": {
"total_steps": {
"name": "Total steps"
},
"daily_step_goal": {
"name": "Daily step goal"
},
"yesterday_steps": {
"name": "Yesterday steps"
},
"weekly_step_avg": {
"name": "Weekly step average"
},
"yesterday_distance": {
"name": "Yesterday distance"
},
"weekly_distance_avg": {
"name": "Weekly distance average"
},
"total_distance": {
"name": "Total distance"
},
"floors_ascended": {
"name": "Floors ascended"
},
"floors_descended": {
"name": "Floors descended"
},
"floors_ascended_goal": {
"name": "Floors ascended goal"
},
"floors_ascended_meters": {
"name": "Floors ascended (meters)"
},
"floors_descended_meters": {
"name": "Floors descended (meters)"
},
"total_calories": {
"name": "Total calories"
},
"active_calories": {
"name": "Active calories"
},
"bmr_calories": {
"name": "BMR calories"
},
"burned_calories": {
"name": "Burned calories"
},
"consumed_calories": {
"name": "Consumed calories"
},
"remaining_calories": {
"name": "Remaining calories"
},
"resting_heart_rate": {
"name": "Resting heart rate"
},
"max_heart_rate": {
"name": "Max heart rate"
},
"min_heart_rate": {
"name": "Min heart rate"
},
"last_7_days_avg_resting_heart_rate": {
"name": "Last 7 days avg resting heart rate"
},
"min_avg_heart_rate": {
"name": "Min avg heart rate"
},
"max_avg_heart_rate": {
"name": "Max avg heart rate"
},
"abnormal_hr_alerts": {
"name": "Abnormal HR alerts"
},
"hrv_status": {
"name": "HRV status"
},
"hrv_weekly_avg": {
"name": "HRV weekly average"
},
"hrv_last_night_avg": {
"name": "HRV last night average"
},
"hrv_last_night_5min_high": {
"name": "HRV last night 5-min high"
},
"hrv_baseline": {
"name": "HRV baseline"
},
"avg_stress_level": {
"name": "Avg stress level"
},
"max_stress_level": {
"name": "Max stress level"
},
"stress_qualifier": {
"name": "Stress qualifier"
},
"total_stress_duration": {
"name": "Total stress duration"
},
"rest_stress_duration": {
"name": "Rest stress duration"
},
"activity_stress_duration": {
"name": "Activity stress duration"
},
"low_stress_duration": {
"name": "Low stress duration"
},
"medium_stress_duration": {
"name": "Medium stress duration"
},
"high_stress_duration": {
"name": "High stress duration"
},
"uncategorized_stress_duration": {
"name": "Uncategorized stress duration"
},
"stress_duration": {
"name": "Stress duration"
},
"stress_percentage": {
"name": "Stress percentage"
},
"rest_stress_percentage": {
"name": "Rest stress percentage"
},
"activity_stress_percentage": {
"name": "Activity stress percentage"
},
"uncategorized_stress_percentage": {
"name": "Uncategorized stress percentage"
},
"low_stress_percentage": {
"name": "Low stress percentage"
},
"medium_stress_percentage": {
"name": "Medium stress percentage"
},
"high_stress_percentage": {
"name": "High stress percentage"
},
"sleeping_time": {
"name": "Sleeping time"
},
"total_sleep_duration": {
"name": "Total sleep duration"
},
"sleep_duration": {
"name": "Sleep duration"
},
"awake_duration": {
"name": "Awake duration"
},
"sleep_score": {
"name": "Sleep score"
},
"deep_sleep": {
"name": "Deep sleep"
},
"light_sleep": {
"name": "Light sleep"
},
"rem_sleep": {
"name": "REM sleep"
},
"awake_sleep": {
"name": "Awake during sleep"
},
"body_battery_most_recent": {
"name": "Body battery (most recent)"
},
"body_battery_highest": {
"name": "Body battery (highest)"
},
"body_battery_lowest": {
"name": "Body battery (lowest)"
},
"body_battery_charged": {
"name": "Body battery charged"
},
"body_battery_drained": {
"name": "Body battery drained"
},
"weight": {
"name": "Weight"
},
"bmi": {
"name": "BMI"
},
"body_fat": {
"name": "Body fat"
},
"body_water": {
"name": "Body water"
},
"bone_mass": {
"name": "Bone mass"
},
"muscle_mass": {
"name": "Muscle mass"
},
"physique_rating": {
"name": "Physique rating"
},
"visceral_fat": {
"name": "Visceral fat"
},
"metabolic_age": {
"name": "Metabolic age"
},
"hydration": {
"name": "Hydration"
},
"hydration_goal": {
"name": "Hydration goal"
},
"hydration_daily_average": {
"name": "Hydration daily average"
},
"hydration_sweat_loss": {
"name": "Hydration sweat loss"
},
"hydration_activity_intake": {
"name": "Hydration activity intake"
},
"active_time": {
"name": "Active time"
},
"highly_active_time": {
"name": "Highly active time"
},
"sedentary_time": {
"name": "Sedentary time"
},
"moderate_intensity": {
"name": "Moderate intensity"
},
"vigorous_intensity": {
"name": "Vigorous intensity"
},
"intensity_goal": {
"name": "Intensity goal"
},
"avg_spo2": {
"name": "Avg SpO2"
},
"lowest_spo2": {
"name": "Lowest SpO2"
},
"latest_spo2": {
"name": "Latest SpO2"
},
"latest_spo2_time": {
"name": "Latest SpO2 time"
},
"highest_respiration": {
"name": "Highest respiration"
},
"lowest_respiration": {
"name": "Lowest respiration"
},
"latest_respiration": {
"name": "Latest respiration"
},
"latest_respiration_time": {
"name": "Latest respiration time"
},
"avg_altitude": {
"name": "Avg altitude"
},
"chronological_age": {
"name": "Chronological age"
},
"fitness_age": {
"name": "Fitness age"
},
"achievable_fitness_age": {
"name": "Achievable fitness age"
},
"previous_fitness_age": {
"name": "Previous fitness age"
},
"endurance_score": {
"name": "Endurance score"
},
"hill_score": {
"name": "Hill score"
},
"next_alarm": {
"name": "Next alarm"
},
"last_activity": {
"name": "Last activity"
},
"last_activities": {
"name": "Last activities"
},
"last_workout": {
"name": "Last workout"
},
"last_workouts": {
"name": "Last workouts"
},
"training_readiness": {
"name": "Training readiness"
},
"training_status": {
"name": "Training status"
},
"morning_training_readiness": {
"name": "Morning training readiness"
},
"lactate_threshold_hr": {
"name": "Lactate threshold heart rate"
},
"lactate_threshold_speed": {
"name": "Lactate threshold speed"
},
"badges": {
"name": "Badges"
},
"user_points": {
"name": "User points"
},
"user_level": {
"name": "User level"
},
"active_goals": {
"name": "Active goals"
},
"future_goals": {
"name": "Future goals"
},
"goals_history": {
"name": "Goals history"
},
"wellness_start_time": {
"name": "Wellness start time"
},
"wellness_end_time": {
"name": "Wellness end time"
},
"wellness_description": {
"name": "Wellness description"
},
"wellness_distance": {
"name": "Wellness distance"
},
"wellness_active_calories": {
"name": "Wellness active calories"
},
"wellness_calories": {
"name": "Wellness calories"
},
"menstrual_cycle_phase": {
"name": "Menstrual cycle phase"
},
"menstrual_cycle_day": {
"name": "Menstrual cycle day"
},
"menstrual_period_day": {
"name": "Menstrual period day"
},
"menstrual_cycle_length": {
"name": "Menstrual cycle length"
},
"menstrual_period_length": {
"name": "Menstrual period length"
},
"bp_systolic": {
"name": "Blood pressure systolic"
},
"bp_diastolic": {
"name": "Blood pressure diastolic"
},
"bp_pulse": {
"name": "Blood pressure pulse"
},
"bp_measurement_time": {
"name": "Blood pressure measurement time"
},
"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}"
},
"add_gear_to_activity_failed": {
"message": "Failed to add gear to activity: {error}"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Esta cuenta ya est\u00e1 configurada."
},
"error": {
"cannot_connect": "No se pudo conectar, intente nuevamente.",
"invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
"too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.",
"unknown": "Error inesperado."
},
"step": {
"user": {
"data": {
"password": "Contrase\u00f1a",
"username": "Nombre de usuario"
},
"description": "Ingrese sus credenciales.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "La cuenta ya ha sido configurada"
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.",
"unknown": "Error inesperado"
},
"step": {
"user": {
"data": {
"password": "Contrase\u00f1a",
"username": "Usuario"
},
"description": "Introduzca sus credenciales.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Konto on juba seadistatud"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamine nurjus",
"too_many_requests": "Liiga palju taotlusi, proovi hiljem uuesti.",
"unknown": "Tundmatu viga"
},
"step": {
"user": {
"data": {
"password": "Salas\u00f5na",
"username": "Kasutajanimi"
},
"description": "Sisesta oma mandaat.",
"title": ""
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9."
},
"error": {
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer.",
"invalid_auth": "Authentification non valide.",
"too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.",
"unknown": "Erreur inattendue."
},
"step": {
"user": {
"data": {
"password": "Mot de passe",
"username": "Nom d'utilisateur"
},
"description": "Entrez vos informations d'identification.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,21 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"too_many_requests": "\u05d1\u05e7\u05e9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05d3\u05d9, \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05e0\u05d9\u05ea \u05de\u05d0\u05d5\u05d7\u05e8 \u05d9\u05d5\u05ea\u05e8.",
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
},
"step": {
"user": {
"data": {
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.",
"unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
},
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
},
"description": "Adja meg a hiteles\u00edt\u0151 adatait.",
"title": "Garmin Csatlakoz\u00e1s"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Akun sudah dikonfigurasi"
},
"error": {
"cannot_connect": "Gagal terhubung",
"invalid_auth": "Autentikasi tidak valid",
"too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.",
"unknown": "Kesalahan yang tidak diharapkan"
},
"step": {
"user": {
"data": {
"password": "Kata Sandi",
"username": "Nama Pengguna"
},
"description": "Masukkan kredensial Anda.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
"too_many_requests": "Troppe richieste, riprovare pi\u00f9 tardi.",
"unknown": "Errore imprevisto"
},
"step": {
"user": {
"data": {
"password": "Password",
"username": "Nome utente"
},
"description": "Inserisci le tue credenziali",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"too_many_requests": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
"password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc0ac\uc6a9\uc790 \uc774\ub984"
},
"description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Kont ass scho konfigur\u00e9iert"
},
"error": {
"cannot_connect": "Feeler beim verbannen",
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
"too_many_requests": "Ze vill Ufroen, prob\u00e9iert sp\u00e9ider nach emol.",
"unknown": "Onerwaarte Feeler"
},
"step": {
"user": {
"data": {
"password": "Passwuert",
"username": "Benotzernumm"
},
"description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,12 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parole",
"username": "Lietot\u0101jv\u0101rds"
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Account is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie",
"too_many_requests": "Te veel aanvragen, probeer het later opnieuw.",
"unknown": "Onverwachte fout"
},
"step": {
"user": {
"data": {
"password": "Wachtwoord",
"username": "Gebruikersnaam"
},
"description": "Voer uw gegevens in",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Kontoen er allerede konfigurert"
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning",
"too_many_requests": "For mange foresp\u00f8rsler, pr\u00f8v p\u00e5 nytt senere.",
"unknown": "Uventet feil"
},
"step": {
"user": {
"data": {
"password": "Passord",
"username": "Brukernavn"
},
"description": "Fyll inn legitimasjonen din.",
"title": ""
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Konto jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie",
"too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"step": {
"user": {
"data": {
"password": "Has\u0142o",
"username": "Nazwa u\u017cytkownika"
},
"description": "Wprowad\u017a dane uwierzytelniaj\u0105ce",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"config": {
"step": {
"user": {
"description": "Digite suas credenciais.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,22 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Conta j\u00e1 configurada"
},
"error": {
"cannot_connect": "Falha na liga\u00e7\u00e3o",
"invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
"unknown": "Erro inesperado"
},
"step": {
"user": {
"data": {
"password": "Palavra-passe",
"username": "Nome de Utilizador"
},
"description": "Introduza as suas credenciais.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.",
"too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Účet je už nakonfigurovaný"
},
"error": {
"cannot_connect": "Nepodarilo sa pripojiť",
"invalid_auth": "Neplatné overenie",
"too_many_requests": "Príliš veľa žiadostí, skúste to neskôr.",
"unknown": "Neočakávaná chyba"
},
"step": {
"user": {
"data": {
"password": "Heslo",
"username": "Užívateľské meno"
},
"description": "Zadajte svoje poverenia.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ta ra\u010dun je \u017ee konfiguriran."
},
"error": {
"cannot_connect": "Povezava ni uspela, poskusite znova.",
"invalid_auth": "Neveljavna avtentikacija.",
"too_many_requests": "Preve\u010d zahtev, poskusite pozneje.",
"unknown": "Nepri\u010dakovana napaka."
},
"step": {
"user": {
"data": {
"password": "Geslo",
"username": "Uporabni\u0161ko ime"
},
"description": "Vnesite svoje poverilnice.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Det h\u00e4r kontot har redan konfigurerats."
},
"error": {
"cannot_connect": "Kunde inte ansluta, var god f\u00f6rs\u00f6k igen.",
"invalid_auth": "Ogiltig autentisering.",
"too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare.",
"unknown": "Ov\u00e4ntat fel."
},
"step": {
"user": {
"data": {
"password": "L\u00f6senord",
"username": "Anv\u00e4ndarnamn"
},
"description": "Ange dina anv\u00e4ndaruppgifter.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,20 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
},
"error": {
"cannot_connect": "Ba\u011flanma hatas\u0131",
"invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama",
"unknown": "Beklenmeyen hata"
},
"step": {
"user": {
"data": {
"password": "Parola",
"username": "Kullan\u0131c\u0131 Ad\u0131"
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f",
"invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.",
"too_many_requests": "\u0417\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.",
"unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430"
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430"
},
"description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -1,11 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "\u7528\u6237\u540d"
}
}
}
}
}

View File

@@ -1,23 +0,0 @@
{
"config": {
"abort": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
"too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"step": {
"user": {
"data": {
"password": "\u5bc6\u78bc",
"username": "\u4f7f\u7528\u8005\u540d\u7a31"
},
"description": "\u8f38\u5165\u6191\u8b49\u3002",
"title": "Garmin Connect"
}
}
}
}

View File

@@ -0,0 +1,198 @@
---
title: Garmin Connect
description: Instructions on how to integrate Garmin Connect health data within Home Assistant.
ha_category:
- Health
- Sensor
ha_iot_class: Cloud Polling
ha_release: "2025.2"
ha_domain: garmin_connect
ha_platforms:
- sensor
ha_integration_type: integration
ha_codeowners:
- '@cyberjunky'
ha_config_flow: true
---
The **Garmin Connect** {% term integration %} allows you to expose health and fitness data from [Garmin Connect](https://connect.garmin.com/) to Home Assistant.
## Prerequisites
You need a Garmin Connect account with at least one Garmin device that syncs data to Garmin Connect.
{% include integrations/config_flow.md %}
## Sensors
This integration provides **110+ sensors** covering various health and fitness metrics. Sensors are grouped into the following categories:
### Activity & Steps
- **Total Steps** - Daily step count
- **Daily Step Goal** - Your configured step target
- **Yesterday Steps/Distance** - Previous day's complete totals
- **Weekly Step/Distance Avg** - 7-day averages
- **Total Distance** - Distance walked/run
- **Floors Ascended/Descended** - Floors climbed
### Calories
- **Total Calories** - Total daily calorie burn
- **Active Calories** - Calories burned through activity
- **BMR Calories** - Basal metabolic rate calories
### Heart Rate
- **Resting Heart Rate** - Daily resting HR
- **Min/Max Heart Rate** - Daily HR range
- **Last 7 Days Avg HR** - Weekly average
- **HRV Weekly Average** - 7-day HRV average (ms)
- **HRV Last Night Average** - Last night's HRV (ms)
- **HRV Baseline** - Personal HRV baseline with range
### Stress & Recovery
- **Avg/Max Stress Level** - Stress measurements (0-100)
- **Stress Durations** - Time in rest/activity/low/medium/high stress
### Sleep
- **Sleep Score** - Overall sleep quality score
- **Sleep Duration** - Time asleep
- **Awake Duration** - Time awake during sleep
- **Deep Sleep** - Time in deep sleep
- **Light Sleep** - Time in light sleep
- **REM Sleep** - Time in REM sleep
### Body Battery
- **Body Battery** - Current energy level (0-100)
- **Charged/Drained** - Energy gained/spent
### Body Composition
- **Weight** - Body weight
- **BMI** - Body Mass Index
- **Body Fat/Water** - Percentage measurements
- **Muscle/Bone Mass** - Mass measurements
### Hydration
- **Hydration** - Daily water intake
- **Hydration Goal** - Target intake
- **Sweat Loss** - Estimated fluid loss
### Blood Pressure
- **Systolic** - Systolic blood pressure (mmHg)
- **Diastolic** - Diastolic blood pressure (mmHg)
- **Pulse** - Pulse from blood pressure reading (bpm)
- **Measurement Time** - When the BP was measured
### Health Monitoring
- **SpO2** - Blood oxygen levels (average, lowest, latest)
- **HRV Status** - Heart rate variability
- **Respiration Rate** - Breathing measurements
### Fitness & Performance
- **Fitness Age** - Estimated fitness age
- **Endurance Score** - Overall endurance rating
- **Training Readiness** - Training readiness score (%)
- **Training Status** - Current training status phrase
- **Lactate Threshold HR** - Lactate threshold heart rate (bpm)
- **Lactate Threshold Speed** - Lactate threshold running pace (m/s)
### Workouts
- **Last Workout** - Name of most recent scheduled workout
- **Last Workouts** - Count of recent workouts (details in attributes)
### Menstrual Cycle Tracking
- **Cycle Phase** - Current menstrual phase
- **Cycle Day** - Day of the current cycle
- **Period Day** - Day of the period
- **Cycle/Period Length** - Cycle and period lengths in days
> **Note:** Menstrual cycle sensors are only available if tracking is enabled in your Garmin Connect account.
### Gear Tracking
Gear sensors are dynamically created for each piece of equipment registered in Garmin Connect (shoes, bikes, etc.). They track total distance and usage statistics.
## Actions
### Add body composition
Add body composition metrics to Garmin Connect.
| Data attribute | Required | Description |
| ---------------------- | -------- | ----------- |
| `weight` | Yes | Weight in kilograms |
| `timestamp` | No | ISO format timestamp |
| `bmi` | No | Body Mass Index |
| `percent_fat` | No | Body fat percentage |
| `muscle_mass` | No | Muscle mass in kg |
| `bone_mass` | No | Bone mass in kg |
| `body_water` | No | Body water percentage |
| `physique_rating` | No | Physique rating (1-9) |
| `visceral_fat` | No | Visceral fat rating |
| `metabolic_age` | No | Metabolic age |
### Add blood pressure
Add blood pressure measurements to Garmin Connect.
| Data attribute | Required | Description |
| ---------------------- | -------- | ----------- |
| `systolic` | Yes | Systolic pressure (mmHg) |
| `diastolic` | Yes | Diastolic pressure (mmHg) |
| `pulse` | Yes | Pulse rate (bpm) |
| `timestamp` | No | ISO format timestamp |
| `notes` | No | Notes about the measurement |
### Set active gear
Set a gear item as the default for an activity type.
| Data attribute | Required | Description |
| ---------------------- | -------- | ----------- |
| `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.
## MFA Support
If your Garmin account has Multi-Factor Authentication (MFA) enabled, you will be prompted to enter your MFA code during setup.
## Known limitations
- Not all sensors will have data depending on your Garmin devices and connected apps.
- API rate limits may cause temporary unavailability during high-traffic periods.

View File

@@ -1,5 +1,6 @@
{
"name": "Garmin Connect",
"render_readme": true,
"domains": ["sensor"]
}
"homeassistant": "2025.2.4",
"hacs": "2.0.5",
"render_readme": true
}

31
pyproject.toml Normal file
View File

@@ -0,0 +1,31 @@
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
[tool.mypy]
python_version = "3.13"
ignore_missing_imports = true
check_untyped_defs = true
warn_return_any = true
warn_unused_ignores = true
show_error_codes = true
[tool.ruff]
target-version = "py313"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "C4"]
ignore = ["E501"]
[tool.coverage.run]
source = ["custom_components"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
homeassistant>=2025.12.4
colorlog==6.10.1
setuptools==80.9.0

15
requirements_lint.txt Normal file
View File

@@ -0,0 +1,15 @@
codespell==2.4.1
fit-tool==0.9.13
mypy==1.19.1
pre-commit==4.5.1
pre-commit-hooks==6.0.0
pylint==4.0.4
pytest
pytest-asyncio
pytest-cov
pytest-homeassistant-custom-component
pyupgrade==3.21.2
ruff==0.14.10
types-pytz==2025.2.0.20251108
types-requests
vulture==2.14

BIN
screenshots/enabledebug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

29
scripts/develop Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Determine the Home Assistant command to use
if command -v hass &> /dev/null; then
HASS_CMD="hass"
else
HASS_CMD="python3 -m homeassistant"
fi
if [ ! -f "${PWD}/config/configuration.yaml" ]; then
mkdir -p "${PWD}/config"
${HASS_CMD} --config "${PWD}/config" --script ensure_config
echo "Updating default configuration."
echo "
logger:
default: info
logs:
custom_components.garmin_connect: debug"
fi
# Set the python path to include our custom_components directory
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
${HASS_CMD} --config "${PWD}/config" --debug

7
scripts/install/core Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/../.."
bash scripts/install/pip_packages --requirement requirements.txt

7
scripts/install/core_dev Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/../.."
bash scripts/install/pip_packages "git+https://github.com/home-assistant/core.git@dev"

9
scripts/install/pip_packages Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
python3 -m pip \
install \
--upgrade \
--disable-pip-version-check \
"${@}"

39
scripts/lint Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Activate local virtual environment if present (so hooks use .venv executables)
if [ -f ".venv/bin/activate" ]; then
# shellcheck disable=SC1091
. .venv/bin/activate
fi
pre-commit install-hooks --config .github/pre-commit-config.yaml;
# Run pre-commit; if hooks modify files they return non-zero — retry a few times
set +e
max_attempts=3
attempt=1
rc=0
while [ $attempt -le $max_attempts ]; do
pre-commit run --hook-stage manual --all-files --config .github/pre-commit-config.yaml
rc=$?
if [ $rc -eq 0 ]; then
break
fi
if [ $attempt -lt $max_attempts ]; then
echo "pre-commit modified files or reported issues; re-running (attempt $attempt/$max_attempts)"
git add -A || true
attempt=$((attempt+1))
continue
fi
break
done
set -e
if [ $rc -ne 0 ]; then
echo "pre-commit failed after $max_attempts attempts (exit code: $rc)"
exit $rc
fi
vulture . --min-confidence 75 --ignore-names policy --exclude .venv

17
scripts/setup Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
scripts/install/pip_packages "pip<23.2,>=21.3.1"
scripts/install/pip_packages setuptools wheel
scripts/install/pip_packages --requirement requirements_lint.txt
scripts/install/core
# Fix pycares compatibility issue with aiodns 3.5.0 on Python 3.13
# Home Assistant requires aiodns==3.5.0, but pycares 5.0.0 is incompatible
# Downgrade to pycares 4.11.0 which is compatible
python3 -m pip install --upgrade "pycares<5,>=4.9.0" 2>/dev/null || true
pre-commit install --config .github/pre-commit-config.yaml

19
scripts/test Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Run pytest with coverage
set -e
cd "$(dirname "$0")/.."
# Activate virtual environment if present
if [ -f ".venv/bin/activate" ]; then
# shellcheck disable=SC1091
. .venv/bin/activate
fi
pytest tests/ \
--cov=custom_components.garmin_connect \
--cov-report=term-missing \
--cov-report=html \
-v \
"$@"

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Tests for Garmin Connect integration."""

35
tests/conftest.py Normal file
View File

@@ -0,0 +1,35 @@
"""Test fixtures for Garmin Connect integration."""
from unittest.mock import patch
import pytest
@pytest.fixture
def mock_garmin_client():
"""Mock Garmin Connect client."""
with patch("custom_components.garmin_connect.coordinator.Garmin") as mock:
mock_instance = mock.return_value
mock_instance.login.return_value = None
mock_instance.get_user_summary.return_value = {
"totalSteps": 5000,
"dailyStepGoal": 10000,
"totalKilocalories": 2000,
"lastSyncTimestampGMT": "2024-01-01T12:00:00",
"userProfileId": "12345",
}
mock_instance.get_body_composition.return_value = {
"totalAverage": {"weight": 75.0, "bmi": 24.5}
}
mock_instance.get_activities_by_date.return_value = []
mock_instance.get_earned_badges.return_value = []
mock_instance.get_device_alarms.return_value = []
mock_instance.get_activity_types.return_value = []
mock_instance.get_sleep_data.return_value = {}
mock_instance.get_hrv_data.return_value = {}
mock_instance.get_endurance_score.return_value = {}
mock_instance.get_gear.return_value = []
mock_instance.get_fitnessage_data.return_value = {}
mock_instance.get_hydration_data.return_value = {}
mock_instance.garth.dumps.return_value = "mock_token"
yield mock_instance

39
tests/test_config_flow.py Normal file
View File

@@ -0,0 +1,39 @@
"""Tests for Garmin Connect config flow."""
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from custom_components.garmin_connect.const import DOMAIN
async def test_form_user(hass: HomeAssistant, _mock_garmin_client) -> None:
"""Test user config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
"""Test invalid authentication."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"custom_components.garmin_connect.config_flow.Garmin"
) as mock_garmin:
from garminconnect import GarminConnectAuthenticationError
mock_garmin.return_value.login.side_effect = GarminConnectAuthenticationError("Invalid")
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"username": "test@test.com", "password": "wrong"},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_auth"}

73
tests/test_const.py Normal file
View File

@@ -0,0 +1,73 @@
"""Tests for Garmin Connect constants.
These tests use mocking to avoid requiring the full Home Assistant stack.
"""
import sys
from unittest.mock import MagicMock
# Mock homeassistant modules before importing const
sys.modules["homeassistant"] = MagicMock()
sys.modules["homeassistant.config_entries"] = MagicMock()
sys.modules["homeassistant.core"] = MagicMock()
sys.modules["homeassistant.const"] = MagicMock()
sys.modules["homeassistant.helpers"] = MagicMock()
sys.modules["homeassistant.helpers.device_registry"] = MagicMock()
sys.modules["homeassistant.helpers.update_coordinator"] = MagicMock()
sys.modules["homeassistant.exceptions"] = MagicMock()
sys.modules["garminconnect"] = MagicMock()
sys.modules["garth"] = MagicMock()
sys.modules["garth.exc"] = MagicMock()
from custom_components.garmin_connect.const import ( # noqa: E402
DAY_TO_NUMBER,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
GEAR_ICONS,
LEVEL_POINTS,
Gear,
ServiceSetting,
)
def test_domain():
"""Test domain constant."""
assert DOMAIN == "garmin_connect"
def test_default_update_interval():
"""Test default update interval."""
assert DEFAULT_UPDATE_INTERVAL.total_seconds() == 300 # 5 minutes
def test_day_to_number():
"""Test day to number mapping."""
assert DAY_TO_NUMBER["Mo"] == 1
assert DAY_TO_NUMBER["Su"] == 7
assert len(DAY_TO_NUMBER) >= 7 # Has aliases for days
def test_level_points():
"""Test level points mapping."""
assert 1 in LEVEL_POINTS
assert LEVEL_POINTS[1] == 0
assert len(LEVEL_POINTS) > 0
def test_gear_icons():
"""Test gear icons mapping."""
assert "Shoes" in GEAR_ICONS
assert "Bike" in GEAR_ICONS
assert "Other" in GEAR_ICONS
def test_service_setting():
"""Test ServiceSetting class."""
assert ServiceSetting.DEFAULT == "set as default"
assert ServiceSetting.UNSET_DEFAULT == "unset default"
def test_gear_class():
"""Test Gear class."""
assert Gear.UUID == "uuid"
assert Gear.USERPROFILE_ID == "userProfileId"

34
tests/test_coordinator.py Normal file
View File

@@ -0,0 +1,34 @@
"""Tests for Garmin Connect coordinator."""
from custom_components.garmin_connect.coordinator import (
calculate_next_active_alarms,
)
async def test_calculate_next_active_alarms_empty():
"""Test calculate_next_active_alarms with empty alarms."""
result = calculate_next_active_alarms([], "UTC")
assert result == []
async def test_calculate_next_active_alarms_none():
"""Test calculate_next_active_alarms with None."""
result = calculate_next_active_alarms(None, "UTC")
assert result == []
async def test_calculate_next_active_alarms_off():
"""Test calculate_next_active_alarms with alarm mode OFF."""
alarms = [{"alarmMode": "OFF", "alarmDays": ["MONDAY"], "alarmTime": 480}]
result = calculate_next_active_alarms(alarms, "UTC")
assert result == []
async def test_calculate_next_active_alarms_once():
"""Test calculate_next_active_alarms with ONCE alarm."""
alarms = [{"alarmMode": "ON", "alarmDays": ["ONCE"], "alarmTime": 480}]
result = calculate_next_active_alarms(alarms, "UTC")
assert result is not None
assert len(result) == 1

View File

@@ -0,0 +1,47 @@
"""Tests for Garmin Connect sensor descriptions."""
from custom_components.garmin_connect.sensor_descriptions import (
ACTIVITY_SENSORS,
ALL_SENSOR_DESCRIPTIONS,
CALORIES_SENSORS,
HEART_RATE_SENSORS,
)
def test_all_sensor_descriptions_not_empty():
"""Test that ALL_SENSOR_DESCRIPTIONS is not empty."""
assert len(ALL_SENSOR_DESCRIPTIONS) > 0
def test_all_sensors_have_key():
"""Test that all sensors have a key."""
for sensor in ALL_SENSOR_DESCRIPTIONS:
assert sensor.key is not None
assert len(sensor.key) > 0
def test_all_sensors_have_translation_key():
"""Test that all sensors have a translation_key."""
for sensor in ALL_SENSOR_DESCRIPTIONS:
assert sensor.translation_key is not None
def test_activity_sensors_exist():
"""Test that activity sensors are defined."""
assert len(ACTIVITY_SENSORS) > 0
def test_calories_sensors_exist():
"""Test that calories sensors are defined."""
assert len(CALORIES_SENSORS) > 0
def test_heart_rate_sensors_exist():
"""Test that heart rate sensors are defined."""
assert len(HEART_RATE_SENSORS) > 0
def test_sensor_count():
"""Test that we have the expected number of sensors."""
# Should have at least 90+ sensors
assert len(ALL_SENSOR_DESCRIPTIONS) >= 90

176
www/garmin-polyline-card.js Normal file
View File

@@ -0,0 +1,176 @@
/**
* Garmin Activity Polyline Map Card
* A simple custom Lovelace card to display activity routes from sensor attributes
*/
class GarminPolylineCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._hass = null;
this._config = null;
this._map = null;
this._polyline = null;
}
setConfig(config) {
if (!config.entity) {
throw new Error('You need to define an entity');
}
this._config = {
entity: config.entity,
attribute: config.attribute || 'polyline',
title: config.title || 'Activity Route',
height: config.height || '300px',
color: config.color || '#FF5722',
weight: config.weight || 4,
...config
};
}
set hass(hass) {
this._hass = hass;
this._updateMap();
}
_updateMap() {
if (!this._hass || !this._config) return;
const stateObj = this._hass.states[this._config.entity];
if (!stateObj) return;
const polylineData = stateObj.attributes[this._config.attribute];
if (!polylineData || !Array.isArray(polylineData) || polylineData.length === 0) {
this._renderNoData();
return;
}
// Convert to Leaflet format [[lat, lon], ...]
const coordinates = polylineData
.filter(p => p.lat != null && p.lon != null)
.map(p => [p.lat, p.lon]);
if (coordinates.length === 0) {
this._renderNoData();
return;
}
this._renderMap(coordinates, stateObj);
}
_renderNoData() {
this.shadowRoot.innerHTML = `
<ha-card header="${this._config.title}">
<div style="padding: 16px; text-align: center; color: var(--secondary-text-color);">
No route data available
</div>
</ha-card>
`;
this._map = null;
}
_renderMap(coordinates, stateObj) {
const activityName = stateObj.state || 'Activity';
// Check if we already have a map container
if (!this._map) {
this.shadowRoot.innerHTML = `
<ha-card header="${this._config.title}">
<div id="map" style="height: ${this._config.height}; width: 100%;"></div>
<div style="padding: 8px 16px; font-size: 12px; color: var(--secondary-text-color);">
${activityName}${coordinates.length} points
</div>
</ha-card>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
`;
// Load Leaflet if not already loaded
if (!window.L) {
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = () => this._initMap(coordinates);
document.head.appendChild(script);
} else {
setTimeout(() => this._initMap(coordinates), 100);
}
} else {
// Update existing polyline
if (this._polyline) {
this._polyline.setLatLngs(coordinates);
this._map.fitBounds(this._polyline.getBounds(), { padding: [20, 20] });
}
}
}
_initMap(coordinates) {
const mapContainer = this.shadowRoot.getElementById('map');
if (!mapContainer || !window.L) return;
// If map already exists, remove it first
if (this._map) {
this._map.remove();
this._map = null;
}
// Create map
this._map = L.map(mapContainer, {
zoomControl: true,
scrollWheelZoom: false
});
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this._map);
// Add polyline
this._polyline = L.polyline(coordinates, {
color: this._config.color,
weight: this._config.weight,
opacity: 0.8
}).addTo(this._map);
// Fit map to polyline bounds
this._map.fitBounds(this._polyline.getBounds(), { padding: [20, 20] });
// Add start/end markers
if (coordinates.length > 0) {
L.circleMarker(coordinates[0], {
radius: 8,
color: '#4CAF50',
fillColor: '#4CAF50',
fillOpacity: 1
}).addTo(this._map).bindPopup('Start');
L.circleMarker(coordinates[coordinates.length - 1], {
radius: 8,
color: '#F44336',
fillColor: '#F44336',
fillOpacity: 1
}).addTo(this._map).bindPopup('End');
}
}
getCardSize() {
return 4;
}
static getStubConfig() {
return {
entity: 'sensor.garmin_connect_last_activity',
attribute: 'polyline',
title: 'Activity Route'
};
}
}
customElements.define('garmin-polyline-card', GarminPolylineCard);
// Register with Home Assistant
window.customCards = window.customCards || [];
window.customCards.push({
type: 'garmin-polyline-card',
name: 'Garmin Polyline Card',
description: 'Display Garmin activity routes on a map'
});
console.info('%c GARMIN-POLYLINE-CARD %c loaded ', 'background: #FF5722; color: white;', '');