Compare commits

...

153 Commits

Author SHA1 Message Date
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
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
kruemelro
14c6694239 changed value datatype to int 2024-04-08 08:57:53 +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
69 changed files with 1892 additions and 1399 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

View File

@@ -1,6 +1,6 @@
{
"name": "cyberjunky/home-assistant-garmin_connect",
"image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
@@ -20,29 +20,32 @@
},
"customizations": {
"extensions": [
"charliermarsh.ruff",
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
"GitHub.copilot"
],
"vscode": {
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"files.eol": "\n",
"editor.tabSize": 4,
"python.pythonPath": "/usr/local/python/bin/python",
"python.analysis.autoSearchPaths": false,
"python.linting.pylintArgs": [
"--disable",
"import-error"
],
"python.formatting.provider": "black",
"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",
@@ -53,6 +56,7 @@
},
"remoteUser": "vscode",
"features": {
"rust": "latest"
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/rust:1": {}
}
}

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,15 +1,26 @@
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
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"
- package-ecosystem: "pip"
interval: weekly
time: "06:00"
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: "/"
labels:
- "pr: dependency-update"
schedule:
interval: "weekly"
ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"
interval: weekly
time: "06:00"
open-pull-requests-limit: 10

View File

@@ -1,50 +1,88 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
hooks:
- id: pyupgrade
stages: [manual]
args:
- "--py39-plus"
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
stages: [manual]
args:
- --safe
- --quiet
files: ^((custom_components|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.1.0
- repo: local
hooks:
- id: codespell
stages: [manual]
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
- --exclude-file=custom_components/hacs/utils/default.repositories
- --skip=tests/fixtures/*,custom_components/garmin_connect/translations/*
- id: isort
name: Sort imports
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: isort
- id: pyupgrade
name: Run pyupgrade
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: pyupgrade
files: ^.*.py$
args:
- "--py39-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: ruff-format
name: Run ruff format
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: ruff
args:
- format
files: ^((action|custom_components|script)/.+)?[^/]+\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-executables-have-shebangs
stages: [manual]
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
stages: [manual]
name: Check JSON files
language: system
types: [json]
stages: [commit-msg, post-commit, manual]
entry: check-json
- id: requirements-txt-fixer
stages: [manual]
name: Check requirements files
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: requirements-txt-fixer
files: ^requirements_.*.txt$
- id: check-ast
stages: [manual]
name: Check Python AST
language: system
types: [python]
stages: [commit-msg, post-commit, manual]
entry: check-ast
- id: mixed-line-ending
stages: [manual]
name: Check line nedings
language: system
types: [text]
stages: [commit-msg, post-commit, manual]
entry: mixed-line-ending
args:
- --fix=lf

View File

@@ -10,5 +10,5 @@ jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v5"
- uses: home-assistant/actions/hassfest@master

5
.gitignore vendored
View File

@@ -1,8 +1,3 @@
# misc
.vscode
outputdata
settings.json
# Translation files
custom_components/garmin_connect/translations
!custom_components/garmin_connect/translations/en.json

View File

@@ -1,3 +0,0 @@
{
"jsonRecursiveSort": true
}

View File

@@ -1,48 +0,0 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py310"
select = [
"B007", # Loop control variable {name} not used within loop body
"B014", # Exception handler with duplicate exception
"C", # complexity
"D", # docstrings
"E", # pycodestyle
"F", # pyflakes/autoflake
"ICN001", # import concentions; {name} should be imported as {asname}
"PGH004", # Use specific rule codes when using noqa
"PLC0414", # Useless import alias. Import alias does not rename original package.
"SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
"SIM117", # Merge with-statements that use the same scope
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
"SIM201", # Use {left} != {right} instead of not {left} == {right}
"SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
"SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
"SIM401", # Use get from dict with default instead of an if block
"T20", # flake8-print
"TRY004", # Prefer TypeError exception for invalid type
"RUF006", # Store a reference to the return value of asyncio.create_task
"UP", # pyupgrade
"W", # pycodestyle
]
ignore = [
"D202", # No blank lines allowed after function docstring
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"D404", # First word of the docstring should not be This
"D406", # Section name should end with a newline
"D407", # Section name underlining
"D411", # Missing blank line before section
"E501", # line too long
"E731", # do not assign a lambda expression, use a def
]
[flake8-pytest-style]
fixture-parentheses = false
[pyupgrade]
keep-runtime-typing = true
[mccabe]
max-complexity = 25

28
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"configurations": [
{
"justMyCode": false,
"name": "Python: Attach Local",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
],
"request": "attach",
"type": "debugpy"
},
{
"name": "Python: Attach Remote",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/homeassistant"
}
],
"request": "attach",
"type": "debugpy"
}
],
"version": "0.2.0"
}

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

@@ -0,0 +1,29 @@
{
"tasks": [
{
"command": "scripts/develop",
"label": "Run Home Assistant",
"problemMatcher": [],
"type": "shell"
},
{
"command": "scripts/setup",
"label": "Upgrade environment",
"problemMatcher": [],
"type": "shell"
},
{
"command": "scripts/test",
"label": "Run tests",
"problemMatcher": [],
"type": "shell"
},
{
"command": "scripts/lint",
"label": "Run lint checks",
"problemMatcher": [],
"type": "shell"
}
],
"version": "2.0.0"
}

View File

@@ -1,59 +0,0 @@
rules:
braces:
level: error
min-spaces-inside: 0
max-spaces-inside: 1
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
level: error
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
level: error
max-spaces-before: 0
max-spaces-after: 1
commas:
level: error
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
level: error
require-starting-space: true
min-spaces-from-content: 2
comments-indentation:
level: error
document-end:
level: error
present: false
document-start:
level: error
present: false
empty-lines:
level: error
max: 1
max-start: 0
max-end: 1
hyphens:
level: error
max-spaces-after: 1
indentation:
level: error
spaces: 2
indent-sequences: true
check-multi-line-strings: false
key-duplicates:
level: error
line-length: disable
new-line-at-end-of-file:
level: error
new-lines:
level: error
type: unix
trailing-spaces:
level: error
truthy:
disable

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-2024 Ron Klinkien
Copyright (c) 2021-2025 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

140
README.md
View File

@@ -1,41 +1,29 @@
[![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/)
[![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 yet (support is coming), 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.
## Install via HACS
## Installation
- The installation is done inside [HACS](https://hacs.xyz/) (Home Assistant Community Store).
- If you already have HACS installed click on the MyHomeAssistant button below, otherwise install HACS before adding this integration.
You can find installation instructions [here.](https://hacs.xyz/docs/setup/download)
- Once HACS is installed, search for `garmin connect` and click on "Download". Once downloaded, restart HomeAssistant.
### 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.
## 📦 Installation
### Manual
- Copy directory `custom_components/garmin_connect` to your `<config dir>/custom_components` directory.
- Restart Home-Assistant.
- Follow configuration steps below.
[![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)
## Configuration
Adding Garmin Connect to your Home Assistant instance can be done via the integrations user interface.
- In the sidebar, click 'Configuration', then 'Devices & Services'. Click the + icon to add "Garmin Connect" to your Home Assistant installation.
- Enter the credentials of the Garmin Connect account you want to add.
- Optionally -when MFA is enabled- it will ask for your MFA code.
- 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 Entities page under Devices and services. (Filter on disabled state)
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.
The integration will fetch new data every 5 minutes, make sure your devices sync to the Garmin Connect website.
## Available Sensors
@@ -49,7 +37,6 @@ Daily Step Goal
Total KiloCalories
Active KiloCalories
BMR KiloCalories
Consumed KiloCalories
Burned KiloCalories
Total Distance Mtr
Active Time
@@ -57,6 +44,7 @@ Sedentary Time
Sleeping Time
Awake Duration
Sleep Duration
Total Sleep Duration
Floors Ascended
Floors Descended
Floors Ascended Goal
@@ -80,11 +68,28 @@ Body Battery Most Recent
Average SPO2
Lowest SPO2
Latest SPO2
Next Alarm Time
Total Sleep Duration
HRV Status
Gear Sensors
Chronological Age
Fitness Age
Achievable Fitness Age
Previous Fitness Age
Hydration
Hydration Goal
Hydration Daily Average
Hydration Sweat Loss
Hydration Activity Intake
```
Disabled by default:
```text
Badges
User Points
User Level
Consumed KiloCalories
Remaining KiloCalories
Net Remaining KiloCalories
Net Calorie Goal
@@ -119,16 +124,18 @@ Latest Respiration Update
Highest Respiration
Lowest Respiration
Latest Respiration
Weight
BMI
Body Fat
Body Water
Body Mass
Bone Mass
Muscle Mass
Physique Rating
Visceral Fat
Metabolic Age
Last Activities
Last Activity
Endurance Score
```
## Screenshots
@@ -139,7 +146,7 @@ Metabolic Age
### Set up an automation using the garmin_connect.add_body_composition service
Useful if you want to pass your weight from another (incompatible) device to Garmin Connect. Garmin Connect does not calculate your BMI when you enter your weight manually so it needs to be passed along for now.
Useful if you want to pass your weight from another (incompatible) device to Garmin Connect. Garmin Connect does not calculate your BMI when you enter your weight manually so it needs to be passed along for now.
```
alias: uiSendWeightToGarminConnect
@@ -147,20 +154,20 @@ description: ""
trigger:
- platform: state
entity_id:
- sensor.my_weight
- sensor.weight
condition:
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.my_weight
entity_id: sensor.weight
above: 75
- condition: numeric_state
entity_id: sensor.my_weight
entity_id: sensor.weight
below: 88
action:
- service: garmin_connect.add_body_composition
data:
entity_id: sensor.garmin_connect_weight
entity_id: sensor.weight
weight: "{{trigger.to_state.state}}"
timestamp: "{{ as_timestamp(now()) | timestamp_local}}"
bmi: >-
@@ -168,6 +175,46 @@ action:
}}
mode: single
```
### Examples on how to test actions from HA GUI
#### Add Body Composition
```
action: garmin_connect.add_body_composition
data:
entity_id: sensor.weight
weight: 87
bmi: 25.5
bone_mass: 4.8
```
See the action template for other available values to add
NOTE: You need to enable the Weight entity
#### Set Active Gear
```
action: garmin_connect.set_active_gear
data:
entity_id: sensor.adidas
activity_type: running
setting: set as default
```
#### Add Blood Pressure
```
action: garmin_connect.add_blood_pressure
data:
entity_id: sensor.min_heart_rate
systolic: 120
diastolic: 80
pulse: 60
timestamp: 2025-1-21T07:34:00.000Z
notes: Measured with Beurer BC54
```
## Debugging
Add the relevant lines below to the `configuration.yaml`:
@@ -179,5 +226,26 @@ logger:
custom_components.garmin_connect: debug
```
## Donation
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/cyberjunkynl/)
## 💖 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! 🙏

View File

@@ -1,20 +0,0 @@
# https://bandit.readthedocs.io/en/latest/config.html
tests:
- B103
- B108
- B306
- B307
- B313
- B314
- B315
- B316
- B317
- B318
- B319
- B320
- B601
- B602
- B604
- B608
- B609

0
constraints.txt Normal file
View File

View File

@@ -1,29 +1,31 @@
"""The Garmin Connect integration."""
from datetime import date
from datetime import timedelta
import logging
import asyncio
from collections.abc import Awaitable
from datetime import datetime, timedelta
import logging
from zoneinfo import ZoneInfo
import requests
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
import requests
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, IntegrationError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
DATA_COORDINATOR,
DAY_TO_NUMBER,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
GEAR,
SERVICE_SETTING,
LEVEL_POINTS,
Gear,
)
_LOGGER = logging.getLogger(__name__)
@@ -61,32 +63,57 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
"""Garmin Connect Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the Garmin Connect hub."""
"""
Initialize the Garmin Connect data update coordinator for Home Assistant.
Configures the Garmin API client, determines if the user is located in China, sets the time zone, and establishes the data update interval for the integration.
"""
self.entry = entry
self.hass = hass
self.in_china = False
self._in_china = False
# Check if the user resides in China
country = self.hass.config.country
if country == "CN":
self.in_china = True
self._in_china = True
_LOGGER.debug("Country: %s", country)
self._api = Garmin(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], self.in_china)
self.time_zone = self.hass.config.time_zone
_LOGGER.debug("Time zone: %s", self.time_zone)
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
self.api = Garmin(is_cn=self._in_china)
super().__init__(hass, _LOGGER, name=DOMAIN,
update_interval=DEFAULT_UPDATE_INTERVAL)
async def async_login(self) -> bool:
"""Login to Garmin Connect."""
"""
Asynchronously authenticates with Garmin Connect using a stored token.
Attempts to log in with the token from the configuration entry, handling authentication failures, rate limiting, connection errors, and missing tokens by raising Home Assistant exceptions or returning False for recoverable errors.
Returns:
bool: True if login succeeds; False if rate limited or an unknown error occurs.
Raises:
ConfigEntryAuthFailed: If authentication fails or the token is missing.
ConfigEntryNotReady: If a connection error occurs.
"""
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)
# Check if the token exists in the entry data
if CONF_TOKEN not in self.entry.data:
raise KeyError("Token not found, migrating config entry")
await self.hass.async_add_executor_job(self.api.login, self.entry.data[CONF_TOKEN])
except GarminConnectAuthenticationError as err:
_LOGGER.error(
"Authentication error occurred during login: %s", err.response.text)
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError as err:
_LOGGER.error(
"Too many request error occurred during login: %s", err)
return False
except (GarminConnectConnectionError) as err:
except GarminConnectConnectionError as err:
_LOGGER.error(
"Connection error occurred during Garmin Connect login request: %s", err
)
@@ -95,58 +122,159 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER.exception(
"Unknown error occurred during Garmin Connect login request"
)
raise ConfigEntryNotReady
except requests.exceptions.HTTPError as err:
if err.response.status_code == 401:
_LOGGER.error(
"Authentication error occurred during login: %s", err.response.text)
raise ConfigEntryAuthFailed from err
if err.response.status_code == 429:
_LOGGER.error(
"Too many requests error occurred during login: %s", err.response.text)
return False
_LOGGER.error(
"Unknown HTTP error occurred during login: %s", err)
return False
except KeyError as err:
_LOGGER.error(
"Found old config during login: %s", err)
raise ConfigEntryAuthFailed from err
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception(
"Unknown error occurred during login: %s", err)
return False
return True
async def _async_update_data(self) -> dict:
"""Fetch data from Garmin Connect."""
"""
Fetches and aggregates comprehensive user data from Garmin Connect for the current day.
This asynchronous method retrieves and consolidates user summary, body composition, recent activities, badges, alarms, activity types, sleep metrics, HRV data, fitness age, hydration, and gear information. It calculates user points and level, determines the next scheduled alarms, and extracts key sleep and HRV metrics. Handles authentication, connection, and rate limiting errors by raising Home Assistant exceptions or returning empty results as appropriate.
Returns:
dict: A dictionary containing consolidated Garmin Connect data, including user summary, body composition, activities, badges, alarms, activity types, sleep metrics, HRV status, fitness age, hydration, gear details, user points, user level, next alarms, sleep score, and sleep time.
"""
summary = {}
body = {}
activites = {}
alarms = {}
gear = {}
gear_stats = {}
gear_defaults = {}
activity_types = {}
last_activities = []
sleep_data = {}
sleep_score = None
sleep_time_seconds = None
hrv_data = {}
hrv_status = {"status": "unknown"}
endurance_data = {}
endurance_status = {"overallScore": None}
next_alarms = []
today = datetime.now(ZoneInfo(self.time_zone)).date()
try:
# User summary
summary = await self.hass.async_add_executor_job(
self._api.get_user_summary, date.today().isoformat()
self.api.get_user_summary, today.isoformat()
)
_LOGGER.debug(f"Summary data: {summary}")
if summary:
_LOGGER.debug("User summary data fetched: %s", summary)
else:
_LOGGER.debug("No user summary data found")
# Body composition
body = await self.hass.async_add_executor_job(
self._api.get_body_composition, date.today().isoformat()
self.api.get_body_composition, today.isoformat()
)
_LOGGER.debug(f"Body data: {body}")
if body:
_LOGGER.debug("Body data fetched: %s", body)
else:
_LOGGER.debug("No body data found")
activities = await self.hass.async_add_executor_job(
self._api.get_activities_by_date, (date.today()-timedelta(days=7)).isoformat(), (date.today()+timedelta(days=1)).isoformat()
# Last activities
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(),
)
_LOGGER.debug(f"Activities data: {activities}")
summary['lastActivities'] = activities
if last_activities:
_LOGGER.debug("Last activities data fetched: %s",
last_activities)
else:
_LOGGER.debug("No last activities data found")
badges = await self.hass.async_add_executor_job(
self._api.get_earned_badges
)
_LOGGER.debug(f"Badges data: {badges}")
summary['badges'] = badges
# Add last activities to summary
summary["lastActivities"] = last_activities
summary["lastActivity"] = last_activities[0] if last_activities else {}
alarms = await self.hass.async_add_executor_job(self._api.get_device_alarms)
_LOGGER.debug(f"Alarms data: {alarms}")
# Badges
badges = await self.hass.async_add_executor_job(self.api.get_earned_badges)
if badges:
_LOGGER.debug("Badges data fetched: %s", badges)
else:
_LOGGER.debug("No badges data found")
activity_types = await self.hass.async_add_executor_job(
self._api.get_activity_types
)
_LOGGER.debug(f"Activity types data: {activity_types}")
# Add badges to summary
summary["badges"] = badges
# Calculate user points and user level
user_points = 0
for badge in badges:
user_points += badge["badgePoints"] * \
badge["badgeEarnedNumber"]
# Add user points to summary
summary["userPoints"] = user_points
user_level = 0
for level, points in LEVEL_POINTS.items():
if user_points >= points:
user_level = level
# Add user level to summary
summary["userLevel"] = user_level
# Alarms
alarms = await self.hass.async_add_executor_job(self.api.get_device_alarms)
if alarms:
_LOGGER.debug("Alarms data fetched: %s", alarms)
else:
_LOGGER.debug("No alarms data found")
# Add alarms to summary
next_alarms = calculate_next_active_alarms(alarms, self.time_zone)
# Activity types
activity_types = await self.hass.async_add_executor_job(self.api.get_activity_types)
if activity_types:
_LOGGER.debug("Activity types data fetched: %s",
activity_types)
else:
_LOGGER.debug("No activity types data found")
# Sleep data
sleep_data = await self.hass.async_add_executor_job(
self._api.get_sleep_data, date.today().isoformat())
_LOGGER.debug(f"Sleep data: {sleep_data}")
self.api.get_sleep_data, today.isoformat()
)
if sleep_data:
_LOGGER.debug("Sleep data fetched: %s", sleep_data)
else:
_LOGGER.debug("No sleep data found")
# HRV data
hrv_data = await self.hass.async_add_executor_job(
self.api.get_hrv_data, today.isoformat()
)
_LOGGER.debug("HRV data fetched: %s", hrv_data)
# Endurance data
endurance_data = await self.hass.async_add_executor_job(
self.api.get_endurance_score, today.isoformat()
)
_LOGGER.debug("Endurance data fetched: %s", endurance_data)
except (
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
@@ -155,111 +283,228 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER.debug("Trying to relogin to Garmin Connect")
if not await self.async_login():
raise UpdateFailed(error) from error
# Gear data
try:
gear = await self.hass.async_add_executor_job(
self.api.get_gear, summary[Gear.USERPROFILE_ID]
)
_LOGGER.debug("Gear data fetched: %s", gear)
# Fitness age data
fitnessage_data = await self.hass.async_add_executor_job(
self.api.get_fitnessage_data, today.isoformat()
)
if fitnessage_data:
_LOGGER.debug("Fitness age data fetched: %s", fitnessage_data)
else:
_LOGGER.debug("No fitness age data found")
# Hyrdation data
hydration_data = await self.hass.async_add_executor_job(
self.api.get_hydration_data, today.isoformat()
)
if hydration_data:
_LOGGER.debug("Hydration data fetched: %s", hydration_data)
else:
_LOGGER.debug("No hydration data found")
except GarminConnectAuthenticationError as err:
_LOGGER.error(
"Authentication error occurred during update: %s", err.response.text)
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError as err:
_LOGGER.error(
"Too many request error occurred during update: %s", err)
return {}
except GarminConnectConnectionError as err:
_LOGGER.error(
"Connection error occurred during update: %s", err)
raise ConfigEntryNotReady from err
except requests.exceptions.HTTPError as err:
if err.response.status_code == 401:
_LOGGER.error(
"Authentication error occurred during update: %s", err.response.text)
raise ConfigEntryAuthFailed from err
if err.response.status_code == 429:
_LOGGER.error(
"Too many requests error occurred during update: %s", err.response.text)
return {}
_LOGGER.error(
"Unknown HTTP error occurred during update: %s", err)
return False
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception(
"Unknown error occurred during update: %s", err)
return {}
try:
# Gear data like shoes, bike, etc.
gear = await self.hass.async_add_executor_job(
self._api.get_gear, summary[GEAR.USERPROFILE_ID]
self.api.get_gear, summary[Gear.USERPROFILE_ID]
)
_LOGGER.debug(f"Gear data: {gear}")
if gear:
_LOGGER.debug("Gear data fetched: %s", gear)
else:
_LOGGER.debug("No gear data found")
# Gear stats data like distance, time, etc.
tasks: list[Awaitable] = [
self.hass.async_add_executor_job(
self._api.get_gear_stats, gear_item[GEAR.UUID]
self.api.get_gear_stats, gear_item[Gear.UUID]
)
for gear_item in gear
]
gear_stats = await asyncio.gather(*tasks)
_LOGGER.debug(f"Gear stats data: {gear_stats}")
if gear_stats:
_LOGGER.debug("Gear statistics data fetched: %s", gear_stats)
else:
_LOGGER.debug("No gear statistics data found")
# Gear defaults data like shoe, bike, etc.
gear_defaults = await self.hass.async_add_executor_job(
self._api.get_gear_defaults, summary[GEAR.USERPROFILE_ID]
self.api.get_gear_defaults, summary[Gear.USERPROFILE_ID]
)
_LOGGER.debug(f"Gear defaults data: {gear_defaults}")
except:
_LOGGER.debug("Gear data is not available")
if gear_defaults:
_LOGGER.debug("Gear defaults data fetched: %s", gear_defaults)
else:
_LOGGER.debug("No gear defaults data found")
except GarminConnectAuthenticationError as err:
_LOGGER.error(
"Authentication error occurred while fetching Gear data: %s", err.response.text)
raise ConfigEntryAuthFailed from err
except GarminConnectTooManyRequestsError as err:
_LOGGER.error(
"Too many request error occurred while fetching Gear data: %s", err)
raise ConfigEntryNotReady from err
except GarminConnectConnectionError as err:
_LOGGER.error(
"Connection error occurred while fetching Gear data: %s", err)
raise ConfigEntryNotReady from err
except requests.exceptions.HTTPError as err:
if err.response.status_code == 401:
_LOGGER.error(
"Authentication error while fetching Gear data: %s", err.response.text)
elif err.response.status_code == 404:
_LOGGER.error(
"URL not found error while fetching Gear data: %s", err.response.text)
elif err.response.status_code == 429:
_LOGGER.error(
"Too many requests error while fetching Gear data: %s", err.response.text)
else:
_LOGGER.error(
"Unknown HTTP error occurred while fetching Gear data: %s", err)
except (KeyError, TypeError, ValueError, ConnectionError) as err:
_LOGGER.debug("Error occurred while fetching Gear data: %s", err)
# Sleep score data
try:
sleep_score = sleep_data["dailySleepDTO"]["sleepScores"]["overall"]["value"]
_LOGGER.debug(f"Sleep score data: {sleep_score}")
_LOGGER.debug("Sleep score data: %s", sleep_score)
except KeyError:
_LOGGER.debug("Sleep score data is not available")
_LOGGER.debug("No sleep score data found")
# Sleep time seconds data
try:
sleep_time_seconds = sleep_data["dailySleepDTO"]["sleepTimeSeconds"]
if sleep_time_seconds:
_LOGGER.debug("Sleep time seconds data: %s",
sleep_time_seconds)
else:
_LOGGER.debug("No sleep time seconds data found")
except KeyError:
_LOGGER.debug("No sleep time seconds data found")
# HRV data
try:
if hrv_data and "hrvSummary" in hrv_data:
hrv_status = hrv_data["hrvSummary"]
_LOGGER.debug("HRV summary status: %s", hrv_status)
except KeyError:
_LOGGER.debug(
"Error occurred while processing HRV summary status data")
# Endurance status
try:
if endurance_data and "overallScore" in endurance_data:
endurance_status = endurance_data
_LOGGER.debug("Endurance score: %s", endurance_status)
except KeyError:
_LOGGER.debug("Endurance data is not available")
return {
**summary,
**body["totalAverage"],
"nextAlarm": alarms,
"nextAlarm": next_alarms,
"gear": gear,
"gear_stats": gear_stats,
"activity_types": activity_types,
"gear_defaults": gear_defaults,
"gearStats": gear_stats,
"activityTypes": activity_types,
"gearDefaults": gear_defaults,
"sleepScore": sleep_score,
"sleepTimeSeconds": sleep_time_seconds,
"hrvStatus": hrv_status,
"enduranceScore": endurance_status,
**fitnessage_data,
**hydration_data,
}
async def set_active_gear(self, entity, service_data):
"""Update Garmin Gear settings"""
if not await self.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
setting = service_data.data["setting"]
activity_type_id = next(
filter(
lambda a: a[GEAR.TYPE_KEY] == service_data.data["activity_type"],
self.data["activity_types"],
)
)[GEAR.TYPE_ID]
if setting != SERVICE_SETTING.ONLY_THIS_AS_DEFAULT:
await self.hass.async_add_executor_job(
self._api.set_gear_default,
activity_type_id,
entity.uuid,
setting == SERVICE_SETTING.DEFAULT,
)
else:
old_default_state = await self.hass.async_add_executor_job(
self._api.get_gear_defaults, self.data[GEAR.USERPROFILE_ID]
)
to_deactivate = list(
filter(
lambda o: o[GEAR.ACTIVITY_TYPE_PK] == activity_type_id
and o[GEAR.UUID] != entity.uuid,
old_default_state,
def calculate_next_active_alarms(alarms, time_zone):
"""
Calculate the next scheduled active Garmin alarms based on alarm settings and the current time.
Filters alarms that are enabled and computes the next scheduled datetime for each alarm day, handling both one-time and recurring alarms. Returns a sorted list of ISO-formatted datetimes for upcoming alarms, or None if no active alarms are scheduled.
Parameters:
alarms: List of alarm setting dictionaries from Garmin devices.
time_zone: Time zone string used to localize alarm times.
Returns:
A sorted list of ISO-formatted datetimes for the next active alarms, or None if none are scheduled.
"""
active_alarms = []
if not alarms:
return active_alarms
now = datetime.now(ZoneInfo(time_zone))
_LOGGER.debug("Now: %s, Alarms: %s", now, alarms)
for alarm_setting in alarms:
if alarm_setting["alarmMode"] != "ON":
continue
for day in alarm_setting["alarmDays"]:
alarm_time = alarm_setting["alarmTime"]
_LOGGER.debug("Alarm time: %s, Alarm day: %s", alarm_time, day)
if day == "ONCE":
midnight = datetime.combine(
now.date(), datetime.min.time(), tzinfo=ZoneInfo(time_zone)
)
)
for active_gear in to_deactivate:
await self.hass.async_add_executor_job(
self._api.set_gear_default,
activity_type_id,
active_gear[GEAR.UUID],
False,
alarm = midnight + timedelta(minutes=alarm_time)
_LOGGER.debug("Midnight: %s, Alarm: %s", midnight, alarm_time)
# If the alarm time is in the past, move it to the next day
if alarm < now:
alarm += timedelta(days=1)
else:
start_of_week = datetime.combine(
now.date() - timedelta(days=now.date().isoweekday() % 7),
datetime.min.time(),
tzinfo=ZoneInfo(time_zone),
)
await self.hass.async_add_executor_job(
self._api.set_gear_default, activity_type_id, entity.uuid, True
)
async def add_body_composition(self, entity, service_data):
"""Record a weigh in/body composition"""
if not await self.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
days_to_add = DAY_TO_NUMBER[day] % 7
alarm = start_of_week + \
timedelta(minutes=alarm_time, days=days_to_add)
_LOGGER.debug("Start of week: %s, Alarm: %s",
start_of_week, alarm)
await self.hass.async_add_executor_job(
self._api.add_body_composition,
service_data.data.get("timestamp", None),
service_data.data.get("weight"),
service_data.data.get("percent_fat", None),
service_data.data.get("percent_hydration", None),
service_data.data.get("visceral_fat_mass", None),
service_data.data.get("bone_mass", None),
service_data.data.get("muscle_mass", None),
service_data.data.get("basal_met", None),
service_data.data.get("active_met", None),
service_data.data.get("physique_rating", None),
service_data.data.get("metabolic_age", None),
service_data.data.get("visceral_fat_rating", None),
service_data.data.get("bmi", None)
)
# If the alarm time is in the past, move it to the next week
if alarm < now:
alarm += timedelta(days=7)
active_alarms.append(alarm.isoformat())
return sorted(active_alarms) if active_alarms else None

View File

@@ -1,48 +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,211 @@
"""Config flow for Garmin Connect integration."""
import logging
import logging
from collections.abc import Mapping
from typing import Any, cast
import requests
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ID, CONF_TOKEN, CONF_PASSWORD, CONF_USERNAME
import voluptuous as vol
import garth
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):
"""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 schemas and internal state for the Garmin Connect configuration flow handler.
Sets up validation schemas for user credentials and MFA input, and initializes variables for API client, login results, MFA code, credentials, and region detection.
"""
self.data_schema = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
self.mfa_data_schema = {
vol.Required(CONF_MFA): str,
}
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)
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 the user with Garmin Connect and handle login errors or multi-factor authentication requirements.
If the user is located in China, configures the API client for the region. Initiates the login process and, if multi-factor authentication is needed, transitions to the MFA step. Handles specific authentication and connection errors, returning appropriate error messages to the user. On successful authentication, proceeds to create or update the configuration entry.
Parameters:
step_id (str): The current step identifier in the configuration flow.
Returns:
ConfigFlowResult: The result of the configuration flow step, which may be a form with errors, a transition to MFA, or entry creation.
"""
errors = {}
# Check if the user resides in China
country = self.hass.config.country
if country == "CN":
self._in_china = True
self._api = Garmin(email=self._username,
password=self._password, return_on_mfa=True, is_cn=self._in_china)
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)
if self._login_result1 == "needs_mfa": # MFA is required
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 await self._async_create_entry()
async def _async_garmin_connect_mfa_login(self) -> ConfigFlowResult:
"""
Complete the Garmin Connect authentication process using the stored multi-factor authentication (MFA) code.
If the MFA code is invalid or an error occurs, prompts the user to re-enter the code. On successful authentication, creates or updates the configuration entry.
"""
try:
await self.hass.async_add_executor_job(self._api.resume_login, self._login_result2, self._mfa_code)
except garth.exc.GarthException as err:
_LOGGER.error("Error during MFA login: %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 or update the configuration entry for the Garmin Connect integration using the current user's credentials and API token.
If an entry with the same username exists, its data is updated and the entry is reloaded; otherwise, a new entry is created with the username as the unique ID and the serialized API token.
"""
config_data = {
CONF_ID: self._username,
CONF_USERNAME: self._username,
CONF_TOKEN: self._api.garth.dumps(),
}
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
return self.async_update_reload_and_abort(existing_entry, data=config_data)
return self.async_create_entry(
title=username,
data={
CONF_ID: username,
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
title=cast(str, self._username), data=config_data
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""
Handle the initial user step of the configuration flow.
If no input is provided, displays a form to collect username and password. If credentials are submitted, stores them and attempts authentication with Garmin Connect.
"""
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 multi-factor authentication (MFA) step in the configuration flow.
If user input is not provided, displays a form to collect the MFA code. If input is provided, stores the MFA code and proceeds with MFA authentication.
"""
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]
_LOGGER.debug("MFA code received")
return await self._async_garmin_connect_mfa_login()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""
Start the reauthorization process using existing configuration entry data.
Extracts the username from the entry data and advances to the reauthorization confirmation step.
"""
self._username = entry_data[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""
Prompt the user to re-enter their username and password to confirm reauthorization of the Garmin Connect integration.
If credentials are provided, attempts to log in and complete the reauthorization process.
"""
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,30 +1,77 @@
"""Constants for the Garmin Connect integration."""
from datetime import timedelta
from typing import NamedTuple
from homeassistant.const import (
UnitOfMass,
UnitOfTime,
UnitOfLength,
PERCENTAGE,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfMass, UnitOfTime, UnitOfVolume
DOMAIN = "garmin_connect"
DATA_COORDINATOR = "coordinator"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
CONF_MFA = "mfa_code"
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,
}
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],
"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],
"consumedKilocalories": [
"Consumed KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
False,
],
"burnedKilocalories": [
"Burned KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
True,
],
"remainingKilocalories": [
"Remaining KiloCalories",
"kcal",
@@ -66,7 +113,14 @@ GARMIN_ENTITY_LIST = {
None,
False,
],
"wellnessDescription": ["Wellness Description", "", "mdi:clock", None, SensorStateClass.TOTAL, False],
"wellnessDescription": [
"Wellness Description",
"",
"mdi:clock",
None,
SensorStateClass.TOTAL,
False,
],
"wellnessDistanceMeters": [
"Wellness Distance Mtr",
UnitOfLength.METERS,
@@ -83,7 +137,14 @@ GARMIN_ENTITY_LIST = {
SensorStateClass.TOTAL,
False,
],
"wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
"wellnessKilocalories": [
"Wellness KiloCalories",
"kcal",
"mdi:food",
None,
SensorStateClass.TOTAL,
False,
],
"highlyActiveSeconds": [
"Highly Active Time",
UnitOfTime.MINUTES,
@@ -92,9 +153,30 @@ GARMIN_ENTITY_LIST = {
SensorStateClass.TOTAL,
False,
],
"activeSeconds": ["Active Time", UnitOfTime.MINUTES, "mdi:fire", None, SensorStateClass.TOTAL, True],
"sedentarySeconds": ["Sedentary Time", UnitOfTime.MINUTES, "mdi:seat", None, SensorStateClass.TOTAL, True],
"sleepingSeconds": ["Sleeping Time", UnitOfTime.MINUTES, "mdi:sleep", None, SensorStateClass.TOTAL, True],
"activeSeconds": [
"Active Time",
UnitOfTime.MINUTES,
"mdi:fire",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"sedentarySeconds": [
"Sedentary Time",
UnitOfTime.MINUTES,
"mdi:seat",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"sleepingSeconds": [
"Sleeping Time",
UnitOfTime.MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"measurableAwakeDuration": [
"Awake Duration",
UnitOfTime.MINUTES,
@@ -127,8 +209,22 @@ GARMIN_ENTITY_LIST = {
SensorStateClass.TOTAL,
False,
],
"floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
"floorsDescended": ["Floors Descended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
"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",
@@ -137,11 +233,46 @@ GARMIN_ENTITY_LIST = {
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],
"minHeartRate": [
"Min Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
True,
],
"maxHeartRate": [
"Max Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
True,
],
"restingHeartRate": [
"Resting Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
True,
],
"minAvgHeartRate": [
"Min Avg Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
False,
],
"maxAvgHeartRate": [
"Max Avg Heart Rate",
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
False,
],
"abnormalHeartRateAlertsCount": [
"Abnormal HR Counts",
None,
@@ -155,13 +286,34 @@ GARMIN_ENTITY_LIST = {
"bpm",
"mdi:heart-pulse",
None,
SensorStateClass.MEASUREMENT,
False,
],
"averageStressLevel": [
"Avg Stress Level",
"lvl",
"mdi:flash-alert",
None,
SensorStateClass.MEASUREMENT,
True,
],
"maxStressLevel": [
"Max Stress Level",
"lvl",
"mdi:flash-alert",
None,
SensorStateClass.MEASUREMENT,
True,
],
"stressQualifier": ["Stress Qualifier", None, "mdi:flash-alert", None, None, False],
"stressDuration": [
"Stress Duration",
UnitOfTime.MINUTES,
"mdi:flash-alert",
SensorDeviceClass.DURATION,
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", UnitOfTime.MINUTES, "mdi:flash-alert", None, SensorStateClass.TOTAL, False],
"restStressDuration": [
"Rest Stress Duration",
UnitOfTime.MINUTES,
@@ -223,7 +375,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"restStressPercentage": [
@@ -231,7 +383,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"activityStressPercentage": [
@@ -239,7 +391,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"uncategorizedStressPercentage": [
@@ -247,7 +399,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"lowStressPercentage": [
@@ -255,7 +407,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"mediumStressPercentage": [
@@ -263,7 +415,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"highStressPercentage": [
@@ -271,7 +423,7 @@ GARMIN_ENTITY_LIST = {
PERCENTAGE,
"mdi:flash-alert",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"moderateIntensityMinutes": [
@@ -294,7 +446,7 @@ GARMIN_ENTITY_LIST = {
"Intensity Goal",
UnitOfTime.MINUTES,
"mdi:run-fast",
None,
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
False,
],
@@ -338,9 +490,30 @@ GARMIN_ENTITY_LIST = {
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],
"averageSpo2": [
"Average SPO2",
PERCENTAGE,
"mdi:diabetes",
None,
SensorStateClass.MEASUREMENT,
True,
],
"lowestSpo2": [
"Lowest SPO2",
PERCENTAGE,
"mdi:diabetes",
None,
SensorStateClass.MEASUREMENT,
True,
],
"latestSpo2": [
"Latest SPO2",
PERCENTAGE,
"mdi:diabetes",
None,
SensorStateClass.MEASUREMENT,
True,
],
"latestSpo2ReadingTimeLocal": [
"Latest SPO2 Time",
None,
@@ -362,7 +535,7 @@ GARMIN_ENTITY_LIST = {
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"lowestRespirationValue": [
@@ -370,7 +543,7 @@ GARMIN_ENTITY_LIST = {
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"latestRespirationValue": [
@@ -378,7 +551,7 @@ GARMIN_ENTITY_LIST = {
"brpm",
"mdi:progress-clock",
None,
SensorStateClass.TOTAL,
SensorStateClass.MEASUREMENT,
False,
],
"latestRespirationTimeGMT": [
@@ -389,27 +562,181 @@ GARMIN_ENTITY_LIST = {
None,
False,
],
"weight": ["Weight", UnitOfMass.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", UnitOfMass.KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
"muscleMass": ["Muscle Mass", UnitOfMass.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", UnitOfTime.YEARS, "mdi:calendar-heart", None, SensorStateClass.TOTAL, False],
"weight": [
"Weight",
UnitOfMass.KILOGRAMS,
"mdi:weight-kilogram",
SensorDeviceClass.WEIGHT,
SensorStateClass.MEASUREMENT,
False,
],
"bmi": ["BMI", "bmi", "mdi:food", None, SensorStateClass.MEASUREMENT, False],
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.MEASUREMENT, False],
"bodyWater": [
"Body Water",
PERCENTAGE,
"mdi:water-percent",
None,
SensorStateClass.MEASUREMENT,
False,
],
"boneMass": [
"Bone Mass",
UnitOfMass.KILOGRAMS,
"mdi:bone",
SensorDeviceClass.WEIGHT,
SensorStateClass.MEASUREMENT,
False,
],
"muscleMass": [
"Muscle Mass",
UnitOfMass.KILOGRAMS,
"mdi:dumbbell",
SensorDeviceClass.WEIGHT,
SensorStateClass.MEASUREMENT,
False,
],
"physiqueRating": [
"Physique Rating",
None,
"mdi:numeric",
None,
SensorStateClass.MEASUREMENT,
False,
],
"visceralFat": [
"Visceral Fat",
PERCENTAGE,
"mdi:food",
None,
SensorStateClass.MEASUREMENT,
False,
],
"metabolicAge": [
"Metabolic Age",
UnitOfTime.YEARS,
"mdi:calendar-heart",
None,
SensorStateClass.MEASUREMENT,
False,
],
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", SensorDeviceClass.TIMESTAMP, None, True],
"lastActivities": ["Last Activities", None, "mdi:numeric", SensorStateClass.TOTAL, None, False],
"badges": ["Badges", None, "mdi:numeric", SensorStateClass.TOTAL, None, False],
"lastActivities": ["Last Activities", None, "mdi:numeric", None, SensorStateClass.TOTAL, False],
"lastActivity": ["Last Activity", None, "mdi:walk", None, None, False],
"badges": ["Badges", None, "mdi:medal", None, SensorStateClass.TOTAL, False],
"userPoints": ["User Points", None, "mdi:counter", None, SensorStateClass.TOTAL, False],
"userLevel": [
"User Level",
None,
"mdi:star-four-points-circle",
None,
SensorStateClass.TOTAL,
False,
],
"sleepScore": [
"Sleep Score",
None,
"mdi:sleep",
SensorStateClass.TOTAL,
None,
SensorStateClass.MEASUREMENT,
True,
]
],
"sleepTimeSeconds": [
"Total Sleep Duration",
UnitOfTime.MINUTES,
"mdi:sleep",
SensorDeviceClass.DURATION,
SensorStateClass.TOTAL,
True,
],
"hrvStatus": [
"HRV Status",
None,
"mdi:heart-pulse",
None,
None,
True,
],
"enduranceScore": [
"Endurance Score",
None,
"mdi:run",
None,
SensorStateClass.MEASUREMENT,
False,
],
"chronologicalAge": [
"Chronological Age",
UnitOfTime.YEARS,
"mdi:calendar-heart",
None,
SensorStateClass.MEASUREMENT,
True,
],
"fitnessAge": [
"Fitness Age",
UnitOfTime.YEARS,
"mdi:calendar-heart",
None,
SensorStateClass.MEASUREMENT,
True,
],
"achievableFitnessAge": [
"Achievable Fitness Age",
UnitOfTime.YEARS,
"mdi:calendar-heart",
None,
SensorStateClass.MEASUREMENT,
True,
],
"previousFitnessAge": [
"Previous Fitness Age",
UnitOfTime.YEARS,
"mdi:calendar-heart",
None,
SensorStateClass.MEASUREMENT,
True,
],
"valueInML": [
"Hydration",
UnitOfVolume.MILLILITERS,
"mdi:water",
None,
SensorStateClass.MEASUREMENT,
True,
],
"goalInML": [
"Hydration Goal",
UnitOfVolume.MILLILITERS,
"mdi:water",
None,
SensorStateClass.MEASUREMENT,
True,
],
"dailyAverageInML": [
"Hydration Daily Average",
UnitOfVolume.MILLILITERS,
"mdi:water",
None,
SensorStateClass.MEASUREMENT,
True,
],
"sweatLossInML": [
"Hydration Sweat Loss",
UnitOfVolume.MILLILITERS,
"mdi:water",
None,
SensorStateClass.MEASUREMENT,
True,
],
"activityIntakeInML": [
"Hydration Activity Intake",
UnitOfVolume.MILLILITERS,
"mdi:water",
None,
SensorStateClass.MEASUREMENT,
True,
],
}
GEAR_ICONS = {
@@ -420,7 +747,7 @@ GEAR_ICONS = {
}
class SERVICE_SETTING(NamedTuple):
class ServiceSetting(NamedTuple):
"""Options for the service settings, see services.yaml"""
ONLY_THIS_AS_DEFAULT = "set this as default, unset others"
@@ -428,7 +755,9 @@ class SERVICE_SETTING(NamedTuple):
UNSET_DEFAULT = "unset default"
class GEAR(NamedTuple):
class Gear(NamedTuple):
"""Options for the gear settings, see services.yaml"""
UUID = "uuid"
TYPE_KEY = "typeKey"
TYPE_ID = "typeId"

View File

@@ -7,6 +7,6 @@
"documentation": "https://github.com/cyberjunky/home-assistant-garmin_connect",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"requirements": ["garminconnect>=0.2.24", "tzlocal"],
"version": "0.2.23"
"requirements": ["garminconnect>=0.2.31"],
"version": "0.2.32"
}

View File

@@ -1,26 +1,23 @@
"""Platform for Garmin Connect integration."""
from __future__ import annotations
import logging
import voluptuous as vol
from numbers import Number
import datetime
from tzlocal import get_localzone
import logging
from numbers import Number
from zoneinfo import ZoneInfo
import voluptuous as vol
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
UnitOfLength,
ATTR_ENTITY_ID,
CONF_ID,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID
from homeassistant.const import ATTR_ENTITY_ID, 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.update_coordinator import (
@@ -28,14 +25,13 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from .alarm_util import calculate_next_active_alarms
from .const import (
DATA_COORDINATOR,
DOMAIN as GARMIN_DOMAIN,
GARMIN_ENTITY_LIST,
GEAR,
GEAR_ICONS,
Gear,
ServiceSetting,
)
_LOGGER = logging.getLogger(__name__)
@@ -55,7 +51,6 @@ async def async_setup_entry(
sensor_type,
(name, unit, icon, device_class, state_class, enabled_by_default),
) in GARMIN_ENTITY_LIST.items():
_LOGGER.debug(
"Registering entity: %s, %s, %s, %s, %s, %s, %s",
sensor_type,
@@ -81,15 +76,38 @@ async def async_setup_entry(
)
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 entity: %s, %s, %s, %s, %s, %s, %s, %s",
sensor_type,
name,
unit,
icon,
uuid,
device_class,
state_class,
enabled_by_default,
)
entities.append(
GarminConnectGearSensor(
coordinator,
unique_id,
gear_item[GEAR.UUID],
gear_item["gearTypeName"],
gear_item["displayName"],
None,
True,
sensor_type,
name,
unit,
icon,
uuid,
device_class,
state_class,
enabled_by_default,
)
)
@@ -97,43 +115,55 @@ async def async_setup_entry(
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"set_active_gear", ENTITY_SERVICE_SCHEMA, coordinator.set_active_gear
"set_active_gear",
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Required("activity_type"): str,
vol.Required("setting"): str,
},
"set_active_gear",
)
platform.async_register_entity_service(
"add_body_composition", BODY_COMPOSITION_SERVICE_SCHEMA, coordinator.add_body_composition
"add_body_composition",
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Optional("timestamp"): str,
vol.Required("weight"): vol.Coerce(float),
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_body_composition",
)
ENTITY_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Required("activity_type"): str,
vol.Required("setting"): str,
}
)
platform.async_register_entity_service(
"add_blood_pressure",
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Optional("timestamp"): str,
vol.Required("systolic"): int,
vol.Required("diastolic"): int,
vol.Required("pulse"): int,
vol.Optional("notes"): str,
},
"add_blood_pressure",
)
BODY_COMPOSITION_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): str,
vol.Optional("timestamp"): str,
vol.Required("weight"): float,
vol.Optional("percent_fat"): float,
vol.Optional("percent_hydration"): float,
vol.Optional("visceral_fat_mass"): float,
vol.Optional("bone_mass"): float,
vol.Optional("muscle_mass"): float,
vol.Optional("basal_met"): float,
vol.Optional("active_met"): float,
vol.Optional("physique_rating"): float,
vol.Optional("metabolic_age"): float,
vol.Optional("visceral_fat_rating"): float,
vol.Optional("bmi"): float
}
)
class GarminConnectSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Garmin Connect Sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator,
@@ -164,47 +194,62 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
@property
def native_value(self):
"""Return the state of the sensor."""
"""
Return the current value of the sensor, applying type-specific formatting and conversions.
if self._type == "lastActivities":
return len(self.coordinator.data[self._type])
if self._type == "badges":
return len(self.coordinator.data[self._type])
if not self.coordinator.data or not self.coordinator.data[self._type]:
For activity and badge sensors, returns the count. For last activity, returns the activity name. HRV status and stress qualifier values are capitalized. Duration and seconds values are converted from seconds to minutes, and mass values from grams to kilograms. For alarms, returns the next active alarm if available. Timestamp values are converted to timezone-aware datetime objects. Returns None if data is unavailable.
"""
if not self.coordinator.data:
return None
value = self.coordinator.data[self._type]
if "Duration" in self._type or "Seconds" in self._type:
value = value // 60
value = self.coordinator.data.get(self._type)
if value is None:
return None
if self._type == "lastActivities" or self._type == "badges":
value = len(self.coordinator.data[self._type])
if self._type == "lastActivity":
value = self.coordinator.data[self._type]["activityName"]
elif self._type == "hrvStatus":
value = self.coordinator.data[self._type]["status"].capitalize()
elif self._type == "enduranceScore":
value = self.coordinator.data[self._type]["overallScore"]
elif "Duration" in self._type or "Seconds" in self._type:
value = round(value // 60, 2)
elif "Mass" in self._type or self._type == "weight":
value = value / 1000
value = round(value / 1000, 2)
elif self._type == "nextAlarm":
active_alarms = calculate_next_active_alarms(
self.coordinator.data[self._type]
)
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_date_time_obj = date_time_obj.replace(tzinfo=tz)
return timezone_date_time_obj
_LOGGER.debug("Active alarms: %s", active_alarms)
_LOGGER.debug("Next alarm: %s", active_alarms[0])
value = active_alarms[0]
else:
return None
value = None
elif self._type == "stressQualifier":
return value
value = value.capitalize()
if self._device_class == SensorDeviceClass.TIMESTAMP:
date_time_obj = datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
tz = get_localzone()
timezone_date_time_obj = date_time_obj.replace(tzinfo=tz)
return timezone_date_time_obj
if value:
value = datetime.datetime.fromisoformat(value).replace(
tzinfo=ZoneInfo(self.coordinator.time_zone)
)
return round(value, 2) if isinstance(value, Number) else value
@property
def extra_state_attributes(self):
"""Return attributes for sensor."""
"""
Return additional state attributes for the sensor entity.
Includes the last sync timestamp and, depending on the sensor type, recent activities (up to 5), badges (up to 10), alarms, or HRV status details (excluding the status string). Returns an empty dictionary if no coordinator data is available.
"""
if not self.coordinator.data:
return {}
@@ -212,27 +257,45 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
"last_synced": self.coordinator.data["lastSyncTimestampGMT"],
}
# Only keep the last 5 activities for performance reasons
if self._type == "lastActivities":
attributes["last_Activities"] = self.coordinator.data[self._type]
activities = self.coordinator.data.get(self._type, [])
sorted_activities = sorted(
activities, key=lambda x: x["activityId"])
attributes["last_activities"] = sorted_activities[-5:]
if self._type == "lastActivity":
attributes = {**attributes, **self.coordinator.data[self._type]}
# Only keep the last 10 badges for performance reasons
if self._type == "badges":
attributes["badges"] = self.coordinator.data[self._type]
badges = self.coordinator.data.get(self._type, [])
sorted_badges = sorted(badges, key=lambda x: x["badgeEarnedDate"])
attributes["badges"] = sorted_badges[-10:]
if self._type == "nextAlarm":
attributes["next_alarms"] = calculate_next_active_alarms(
self.coordinator.data[self._type]
)
attributes["next_alarms"] = self.coordinator.data[self._type]
if self._type == "hrvStatus":
attributes = {**attributes, **self.coordinator.data[self._type]}
del attributes["status"]
if self._type == "enduranceScore":
attributes = {**attributes, **self.coordinator.data[self._type]}
del attributes["overallScore"]
return attributes
@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={(GARMIN_DOMAIN, self._unique_id)},
name="Garmin Connect",
manufacturer="Garmin",
model="Garmin Connect",
entry_type=None,
)
@property
def entity_registry_enabled_default(self) -> bool:
@@ -248,36 +311,124 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
and self._type in self.coordinator.data
)
async def add_body_composition(self, **kwargs):
"""
Add a new body composition measurement to Garmin Connect.
Extracts body composition metrics from keyword arguments and submits them to the Garmin Connect API. Ensures the user is logged in before attempting to add the record.
Raises:
IntegrationError: If login to Garmin Connect fails.
"""
weight = kwargs.get("weight")
timestamp = kwargs.get("timestamp")
percent_fat = kwargs.get("percent_fat")
percent_hydration = kwargs.get("percent_hydration")
visceral_fat_mass = kwargs.get("visceral_fat_mass")
bone_mass = kwargs.get("bone_mass")
muscle_mass = kwargs.get("muscle_mass")
basal_met = kwargs.get("basal_met")
active_met = kwargs.get("active_met")
physique_rating = kwargs.get("physique_rating")
metabolic_age = kwargs.get("metabolic_age")
visceral_fat_rating = kwargs.get("visceral_fat_rating")
bmi = kwargs.get("bmi")
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
"""Record a weigh in/body composition."""
await self.hass.async_add_executor_job(
self.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,
)
async def add_blood_pressure(self, **kwargs):
"""
Add a blood pressure measurement to Garmin Connect using the provided values.
Parameters:
systolic: Systolic blood pressure value.
diastolic: Diastolic blood pressure value.
pulse: Pulse rate.
timestamp: Optional timestamp for the measurement.
notes: Optional notes for the measurement.
Raises:
IntegrationError: If unable to log in to Garmin Connect.
"""
timestamp = kwargs.get("timestamp")
systolic = kwargs.get("systolic")
diastolic = kwargs.get("diastolic")
pulse = kwargs.get("pulse")
notes = kwargs.get("notes")
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
"""Record a blood pressure measurement."""
await self.hass.async_add_executor_job(
self.coordinator.api.set_blood_pressure,
systolic,
diastolic,
pulse,
timestamp,
notes,
)
class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
"""Representation of a Garmin Connect Sensor."""
"""Representation of a Garmin Connect Gear Sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator,
unique_id,
uuid,
sensor_type,
name,
unit,
icon,
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
self._type = sensor_type
self._uuid = uuid
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 = GEAR_ICONS[sensor_type]
self._attr_native_unit_of_measurement = UnitOfLength.KILOMETERS
self._attr_icon = icon
self._attr_native_unit_of_measurement = unit
self._attr_unique_id = f"{self._unique_id}_{self._uuid}"
self._attr_state_class = SensorStateClass.TOTAL
self._attr_device_class = "garmin_gear"
self._attr_state_class = self._state_class
@property
def uuid(self):
@@ -295,11 +446,15 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
@property
def extra_state_attributes(self):
"""Return attributes for sensor."""
"""
Return additional state attributes for the gear sensor entity.
Includes metadata such as last sync time, total activities, creation and update dates, gear make/model/status, custom model, maximum distance, and a comma-separated list of activity types for which this gear is set as default. Returns an empty dictionary if required data is missing.
"""
gear = self._gear()
stats = self._stats()
gear_defaults = self._gear_defaults()
activity_types = self.coordinator.data["activity_types"]
activity_types = self.coordinator.data["activityTypes"]
default_for_activity = self._activity_names_for_gear_defaults(
gear_defaults, activity_types
)
@@ -328,25 +483,22 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
return attributes
def _activity_names_for_gear_defaults(self, gear_defaults, activity_types):
return list(
map(
lambda b: b["typeKey"],
filter(
lambda a: a["typeId"]
in map(lambda d: d["activityTypePk"], 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={(GARMIN_DOMAIN, self._unique_id)},
name="Garmin Connect",
manufacturer="Garmin",
model="Garmin Connect",
entry_type=None,
)
@property
def entity_registry_enabled_default(self) -> bool:
@@ -360,21 +512,87 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
def _stats(self):
"""Get gear statistics from garmin"""
for gear_stats_item in self.coordinator.data["gear_stats"]:
if gear_stats_item[GEAR.UUID] == self._uuid:
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:
if gear_item[Gear.UUID] == self._uuid:
return gear_item
def _gear_defaults(self):
"""Get gear defaults"""
"""
Return a list of default gear settings for this gear UUID.
Returns:
List of gear default dictionaries where this gear is set as the default.
"""
return list(
filter(
lambda d: d[GEAR.UUID] == self.uuid and d["defaultGear"] is True,
self.coordinator.data["gear_defaults"],
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 or default for a specified activity type in Garmin Connect.
Parameters:
activity_type (str): The activity type key for which to update the gear setting.
setting (str): The desired gear setting, indicating whether to set as default or as the only default.
Raises:
IntegrationError: If unable to log in to Garmin Connect.
"""
activity_type = kwargs.get("activity_type")
setting = kwargs.get("setting")
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update"
)
"""Update Garmin Gear settings."""
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,
)
)

View File

@@ -2,6 +2,14 @@ set_active_gear:
name: Set active gear for activity
description: Set active gear for activity.
fields:
entity_id:
name: entity
description: entity
required: true
selector:
entity:
integration: garmin_connect
device_class: garmin_gear
activity_type:
required: true
name: activity type
@@ -28,6 +36,11 @@ set_active_gear:
- set this as default, unset others
- set as default
- unset default
add_body_composition:
name: Adds updated body composition metrics
description: Adds updated body composition metrics.
fields:
entity_id:
name: entity
description: entity
@@ -35,12 +48,7 @@ set_active_gear:
selector:
entity:
integration: garmin_connect
device_class: garmin_gear
add_body_composition:
name: Adds updated body composition metrics
description: Adds updated body composition metrics.
fields:
device_class: weight
weight:
required: true
name: Weight
@@ -106,6 +114,11 @@ add_body_composition:
name: Visceral Fat Rating
description: Visceral Fat Rating
example: 10
add_blood_pressure:
name: Adds updated blood pressure metrics
description: Adds updated blood pressure metrics.
fields:
entity_id:
name: entity
description: entity
@@ -113,4 +126,29 @@ add_body_composition:
selector:
entity:
integration: garmin_connect
device_class: weight
device_class: min_heart_rate
systolic:
required: true
name: Systolic
description: Systolic value
example: 120
diastolic:
required: true
name: Diastolic
description: Diastolic value
example: 80
pulse:
required: true
name: Pulse
description: Pulse
example: 60
timestamp:
required: false
name: Timestamp
description: Datetime string of when the measurements were recorded. Defaults to now.
example: 2023-12-30T07:34:00
notes:
required: false
name: Notes
description: Add notes to the measurement
example: 'Measured with Beurer BC54'

View File

@@ -1,22 +1,37 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"step": {
"user": {
"title": "Enter your Garmin Connect login information",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"mfa": {
"title": "Enter your Garmin Connect MFA code",
"data": {
"mfa_code": "MFA code (6-digits)"
}
},
"reauth_confirm": {
"title": "[%key:component::garmin_connect::config::step::user::title%]",
"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%]"
}
}
}

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,38 @@
{
"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": "Enter your Garmin Connect credentials",
"data": {
"password": "Password",
"username": "Username"
},
"description": "Enter your credentials."
},
"mfa": {
"title": "Enter your Garmin Connect MFA code",
"data": {
"mfa_code": "MFA code (6-digits)"
}
},
"reauth_confirm": {
"title": "Enter your Garmin Connect credentials",
"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"
}
}
}
}

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 desconhecido"
},
"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 poverovacie údaje.",
"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

@@ -1,4 +1,6 @@
{
"name": "Garmin Connect",
"render_readme": true
"render_readme": true,
"homeassistant": "2025.1.4",
"hacs": "1.34.0"
}

View File

@@ -1,19 +0,0 @@
[mypy]
python_version = 3.11
show_error_codes = true
follow_imports = silent
ignore_missing_imports = true
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true

View File

@@ -1,81 +0,0 @@
[MASTER]
ignore=tests
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
jobs=2
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
useless-suppression,
# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
# load-plugins=
# Pickle collected data for later comparisons.
persistent=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=ciso8601,
cv2
[BASIC]
good-names=i,j,k,ex,_,T,x,y,id
[MESSAGES CONTROL]
# Reasons disabled:
# format - handled by black
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# too-many-* - are not enforced for the sake of readability
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
# wrong-import-order - isort guards this
disable=
format,
abstract-class-little-used,
abstract-method,
cyclic-import,
duplicate-code,
inconsistent-return-statements,
too-many-instance-attributes,
wrong-import-order,
too-few-public-methods
# enable useless-suppression temporarily every now and then to clean them up
enable=
useless-suppression,
use-symbolic-message-instead,
[REPORTS]
score=no
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[FORMAT]
expected-line-ending-format=LF
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception

119
pyproject.toml Normal file
View File

@@ -0,0 +1,119 @@
[tool.isort]
# https://github.com/PyCQA/isort/wiki/isort-Settings
profile = "black"
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
known_first_party = ["custom_components"]
combine_as_imports = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
addopts = "-rxf -l --cov=./ --cov-report=xml"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore:It is recommended to use web.AppKey instances for keys",
]
log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
norecursedirs = [".git"]
[tool.ruff]
fix = true
line-length = 100
show-fixes = true
target-version = "py312"
[tool.lint]
select = ["ALL"]
ignore = [
"ANN001",
"ANN002",
"ANN003",
"ANN101",
"ANN201",
"ANN202",
"ANN204",
"ANN205",
"ANN401",
"ARG001",
"ARG002",
"ARG005",
"ASYNC110",
"BLE001",
"C901",
"COM812",
"D100",
"D101",
"D102",
"D103",
"D105",
"D107",
"D202",
"D203",
"D205",
"D213",
"D400",
"D401",
"D415",
"E501",
"E713",
"EM101",
"EM102",
"F401",
"FBT001",
"FBT002",
"FBT003",
"I001",
"INP001",
"ISC001",
"N812",
"N818",
"PERF401",
"PERF402",
"PIE804",
"PLR0912",
"PLR0913",
"PLR0915",
"PLR1714",
"PLR1722",
"PLR2004",
"PLR5501",
"PTH103",
"PTH107",
"PTH110",
"PTH113",
"PTH123",
"RET502",
"RET503",
"RET504",
"RET507",
"RSE102",
"RUF005",
"RUF012",
"RUF013",
"S105",
"S110",
"SIM102",
"SIM103",
"SIM105",
"SIM108",
"SIM110",
"SIM114",
"SIM117",
"SLF001",
"TCH001",
"TCH002",
"TCH003",
"TID252",
"TRY003",
"TRY201",
"TRY300",
"TRY301",
"TRY400",
"TRY401",
"UP040",
]
fixable = ["ALL"]
unfixable = []

View File

@@ -1,8 +0,0 @@
colorlog==6.8.2
homeassistant==2024.1.0
pip>=24.1.1,<24.2
ruff==0.5.0
mypy==1.10.1
pre-commit==3.7.1
pylint==3.2.5
types-cachetools

2
requirements_base.txt Normal file
View File

@@ -0,0 +1,2 @@
colorlog==6.10.1
setuptools==80.9.0

View File

@@ -0,0 +1 @@
homeassistant==2025.1.4

View File

@@ -1,3 +1,8 @@
-r requirements.txt
pre-commit==3.7.1
vulture==2.11
--requirement requirements_base.txt
codespell==2.4.1
isort==7.0.0
pre-commit==4.3.0
pre-commit-hooks==6.0.0
pyupgrade==3.21.0
ruff==0.14.3
vulture==2.14

19
scripts/develop Normal file → Executable file
View File

@@ -4,16 +4,19 @@ set -e
cd "$(dirname "$0")/.."
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
if [ ! -f "${PWD}/config/configuration.yaml" ]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
echo "Updating default configuration."
echo "
logger:
default: info
logs:
custom_components.garmin_connect: debug
" >> "${PWD}/config/configuration.yaml"
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
# Set the python path to include our custom_components directory
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant

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_core_min.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"

1
scripts/install/pip_packages Normal file → Executable file
View File

@@ -6,4 +6,5 @@ python3 -m pip \
install \
--upgrade \
--disable-pip-version-check \
--constraint constraints.txt \
"${@}"

5
scripts/lint Normal file → Executable file
View File

@@ -4,10 +4,7 @@ set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements_lint.txt
ruff check . --fix;
pre-commit install-hooks --config .github/pre-commit-config.yaml;
pre-commit run --hook-stage manual --all-files --config .github/pre-commit-config.yaml;
vulture . --min-confidence 55 --ignore-names policy
vulture . --min-confidence 75 --ignore-names policy

7
scripts/setup Normal file → Executable file
View File

@@ -4,4 +4,9 @@ set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements.txt
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
pre-commit install --config .github/pre-commit-config.yaml

View File

@@ -1,21 +0,0 @@
[flake8]
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
doctests = True
# To work with Black
max-line-length = 88
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# D107 Missing docstring in __init__
ignore =
E501,
W503,
E203,
D202,
D107
[isort]
# https://github.com/timothycrosley/isort
# https://github.com/timothycrosley/isort/wiki/isort-Settings
profile = black