mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-09 12:57:58 -05:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e8e52d72 | ||
|
|
c94104443d | ||
|
|
13b2bb64b0 | ||
|
|
d9b99cd9ea | ||
|
|
00a15e077a | ||
|
|
4a5564bc45 | ||
|
|
b35de63aae | ||
|
|
c52166ded0 | ||
|
|
d6bc6f7f69 | ||
|
|
a41b847e45 | ||
|
|
9c42b634e7 | ||
|
|
28a95b2ab2 | ||
|
|
d1609b5c5b | ||
|
|
6b5d8fc0b6 | ||
|
|
be907979ba | ||
|
|
d884162c8f | ||
|
|
194f377016 | ||
|
|
dfc4b5c723 | ||
|
|
06ee9e15b7 | ||
|
|
164fc53b6d | ||
|
|
2eeb42b4da | ||
|
|
0cb06c7cc4 | ||
|
|
99fe655919 | ||
|
|
36d6f7720d | ||
|
|
34d88ff534 | ||
|
|
4a3ac50097 | ||
|
|
16a179ab9b | ||
|
|
8269a44345 | ||
|
|
3be17f04c4 | ||
|
|
46b11708bb | ||
|
|
115db56262 | ||
|
|
792c50e291 | ||
|
|
0f0d9ad8f5 | ||
|
|
2a16ea4727 | ||
|
|
bd567dc0b2 | ||
|
|
1620038867 | ||
|
|
9cc0b4ee43 | ||
|
|
a2ce110dcf | ||
|
|
a2c73638d4 | ||
|
|
585242e77c | ||
|
|
dbcb5d7103 | ||
|
|
3a14a7a108 | ||
|
|
5f9aeb4914 | ||
|
|
5934224198 | ||
|
|
fbbbf92728 | ||
|
|
cd3190a799 | ||
|
|
4015141d23 | ||
|
|
91cdd4ad77 | ||
|
|
cdff89f2ca | ||
|
|
689df3de94 | ||
|
|
8c16842247 | ||
|
|
fa0e60648f | ||
|
|
5a8fd38741 | ||
|
|
f16f96e5de | ||
|
|
25944ba0fe | ||
|
|
d3fa73c5a1 | ||
|
|
252f67ff7f | ||
|
|
b366807571 | ||
|
|
437aae6d34 | ||
|
|
51ccb196c0 | ||
|
|
8de123eff2 | ||
|
|
d4e9a4edc6 | ||
|
|
5a5b42a560 | ||
|
|
e2deaed42b | ||
|
|
78b6641506 | ||
|
|
29f0832e8b | ||
|
|
07473ef701 | ||
|
|
b8156a6a7d | ||
|
|
7546a40012 | ||
|
|
e6f7947e2c | ||
|
|
dda115539f | ||
|
|
304cd1f227 | ||
|
|
6dc64df4d9 | ||
|
|
c3af1cc392 | ||
|
|
4fa9f446de | ||
|
|
9431374a71 | ||
|
|
684acf436c | ||
|
|
3b54e25db5 | ||
|
|
73eb0cea73 | ||
|
|
8099a99077 | ||
|
|
8647e95e30 | ||
|
|
270327d7d8 | ||
|
|
14c6694239 | ||
|
|
d42edcabc6 | ||
|
|
b827597dab | ||
|
|
cb16c0198c | ||
|
|
d8478aea37 | ||
|
|
3392a1458a | ||
|
|
09e10f0da2 | ||
|
|
265c0fbb21 | ||
|
|
9c9cf6609c | ||
|
|
91c35eb486 | ||
|
|
5334378493 | ||
|
|
3d5200581d | ||
|
|
e4331ecf8c | ||
|
|
47096eabeb | ||
|
|
09e80e4255 | ||
|
|
62377b5d55 | ||
|
|
9136466115 | ||
|
|
3bf77a9056 | ||
|
|
83ceb35666 | ||
|
|
003a4c57ef | ||
|
|
e99ec5c770 | ||
|
|
38fd1fb631 | ||
|
|
9b8f536601 | ||
|
|
261f41940f | ||
|
|
27551067ba | ||
|
|
d5dddd5f73 | ||
|
|
4e033b91a5 | ||
|
|
a12306d79d | ||
|
|
6abe6ace3c | ||
|
|
5f31a7492d | ||
|
|
b8abef2954 | ||
|
|
a3096b4c80 | ||
|
|
23e7ab94c0 | ||
|
|
7fb469121c | ||
|
|
d5c88528b1 | ||
|
|
7ab5141a6c | ||
|
|
7ad8d099e1 | ||
|
|
9198f469c2 | ||
|
|
7b227663dc | ||
|
|
afc9baa4a2 | ||
|
|
f15658548d | ||
|
|
e64573c6aa | ||
|
|
71f24148a2 | ||
|
|
891230ce7a | ||
|
|
c72e7a1879 | ||
|
|
65720791c2 | ||
|
|
1029e21e23 | ||
|
|
ac923b7517 | ||
|
|
cf5d39fb56 | ||
|
|
77dbbc9f47 | ||
|
|
2326be7455 | ||
|
|
bfb720ac5d | ||
|
|
d20c9bedb2 | ||
|
|
5e6f7ff6e1 | ||
|
|
9d90c366d9 | ||
|
|
891ddbce27 | ||
|
|
dc345c4d53 | ||
|
|
89222f0299 | ||
|
|
18304be84a | ||
|
|
ffd478cc60 | ||
|
|
4e6b9310a1 | ||
|
|
e9fba7abbe |
22
.coderabbit.yaml
Normal file
22
.coderabbit.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # Schema for CodeRabbit configurations
|
||||
language: "en-US"
|
||||
early_access: true
|
||||
reviews:
|
||||
profile: "assertive"
|
||||
request_changes_workflow: false
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
path_filters:
|
||||
- "!tests/**/cassettes/**"
|
||||
path_instructions:
|
||||
- path: "tests/**"
|
||||
instructions: |
|
||||
- test functions shouldn't have a return type hint
|
||||
- it's ok to use `assert` instead of `pytest.assume()`
|
||||
chat:
|
||||
auto_reply: true
|
||||
62
.devcontainer.json
Normal file
62
.devcontainer.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "cyberjunky/home-assistant-garmin_connect",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.12",
|
||||
"postCreateCommand": "scripts/setup",
|
||||
"forwardPorts": [
|
||||
8123
|
||||
],
|
||||
"portsAttributes": {
|
||||
"8123": {
|
||||
"label": "Home Assistant"
|
||||
},
|
||||
"0-8122": {
|
||||
"label": "Auto-Forwarded - Other",
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"8124-999999": {
|
||||
"label": "Auto-Forwarded - Other",
|
||||
"onAutoForward": "ignore"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance",
|
||||
"GitHub.copilot"
|
||||
],
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.formatting.provider": "ruff",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.rulers": [
|
||||
100
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "always",
|
||||
"source.organizeImports": "always"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true
|
||||
},
|
||||
"extensions": [
|
||||
"GitHub.copilot",
|
||||
"github.vscode-pull-request-github",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-vscode.makefile-tools",
|
||||
"ryanluker.vscode-coverage-gutters"
|
||||
]
|
||||
}
|
||||
},
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/rust:1": {}
|
||||
}
|
||||
}
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [cyberjunky] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
26
.github/dependabot.yml
vendored
Normal file
26
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
labels:
|
||||
- "pr: dependency-update"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
labels:
|
||||
- "pr: dependency-update"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
labels:
|
||||
- "pr: dependency-update"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
88
.github/pre-commit-config.yaml
vendored
Normal file
88
.github/pre-commit-config.yaml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: codespell
|
||||
name: Check code for common misspellings
|
||||
language: system
|
||||
types: [text]
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
entry: codespell
|
||||
args:
|
||||
- --quiet-level=2
|
||||
- --ignore-words-list=hass,ba,fo
|
||||
- --skip=tests/fixtures/*,custom_components/garmin_connect/translations/*
|
||||
|
||||
- id: 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$
|
||||
|
||||
- id: check-executables-have-shebangs
|
||||
name: Check that executables have shebangs
|
||||
language: system
|
||||
types: [text, executable]
|
||||
entry: check-executables-have-shebangs
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
|
||||
- id: check-json
|
||||
name: Check JSON files
|
||||
language: system
|
||||
types: [json]
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
entry: check-json
|
||||
|
||||
- id: requirements-txt-fixer
|
||||
name: Check requirements files
|
||||
language: system
|
||||
types: [text]
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
entry: requirements-txt-fixer
|
||||
files: ^requirements_.*.txt$
|
||||
|
||||
- id: check-ast
|
||||
name: Check Python AST
|
||||
language: system
|
||||
types: [python]
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
entry: check-ast
|
||||
|
||||
- id: mixed-line-ending
|
||||
name: Check line nedings
|
||||
language: system
|
||||
types: [text]
|
||||
stages: [commit-msg, post-commit, manual]
|
||||
entry: mixed-line-ending
|
||||
args:
|
||||
- --fix=lf
|
||||
25
.github/release.yml
vendored
Normal file
25
.github/release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: '💥 Breaking changes'
|
||||
labels:
|
||||
- 'Breaking Change'
|
||||
|
||||
- title: '🛎️ Experimental'
|
||||
labels:
|
||||
- 'Experimental'
|
||||
|
||||
- title: '✨ New features'
|
||||
labels:
|
||||
- 'pr: new-feature'
|
||||
|
||||
- title: '⚡ Enhancements'
|
||||
labels:
|
||||
- 'pr: enhancement'
|
||||
|
||||
- title: '♻️ Refactor'
|
||||
labels:
|
||||
- 'pr: refactor'
|
||||
|
||||
- title: '🐛 Bug Fixes'
|
||||
labels:
|
||||
- 'pr: bugfix'
|
||||
14
.github/workflows/hassfest.yaml
vendored
Normal file
14
.github/workflows/hassfest.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Validate with hassfest
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,10 @@
|
||||
# Translation files
|
||||
custom_components/garmin_connect/translations
|
||||
!custom_components/garmin_connect/translations/en.json
|
||||
|
||||
# Home Assistant configuration
|
||||
config
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
89
.pre-commit-config.yaml
Normal file
89
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
ci:
|
||||
skip:
|
||||
- mypy
|
||||
- pylint
|
||||
|
||||
default_language_version:
|
||||
python: python3.11
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --safe
|
||||
- --quiet
|
||||
<<: &python-files-with-tests
|
||||
files: ^((custom_components|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.6.0
|
||||
- pydocstyle==6.1.1
|
||||
<<: &python-files
|
||||
files: ^(custom_components/.+)?[^/]+\.py$
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.6
|
||||
hooks:
|
||||
- id: bandit
|
||||
args:
|
||||
- --quiet
|
||||
- --format=custom
|
||||
- --configfile=bandit.yaml
|
||||
<<: *python-files-with-tests
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
- id: no-commit-to-branch
|
||||
- id: requirements-txt-fixer
|
||||
- id: mixed-line-ending
|
||||
args:
|
||||
- --fix=lf
|
||||
stages: [manual]
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
additional_dependencies:
|
||||
- prettier@2.7.1
|
||||
- prettier-plugin-sort-json@0.0.3
|
||||
exclude_types:
|
||||
- python
|
||||
exclude: manifest\.json$
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.33.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: local
|
||||
hooks:
|
||||
# Run mypy through our wrapper script in order to get the possible
|
||||
# pyenv and/or virtualenv activated; it may not have been e.g. if
|
||||
# committing from a GUI tool that was not launched from an activated
|
||||
# shell.
|
||||
- id: mypy
|
||||
name: Check with mypy
|
||||
entry: scripts/run-in-env.sh mypy
|
||||
language: script
|
||||
types: [python]
|
||||
<<: *python-files
|
||||
- id: pylint
|
||||
name: Check with pylint
|
||||
entry: scripts/run-in-env.sh pylint
|
||||
language: script
|
||||
types: [python]
|
||||
<<: *python-files
|
||||
28
.vscode/launch.json
vendored
Normal file
28
.vscode/launch.json
vendored
Normal 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
29
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 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
|
||||
|
||||
90
README.md
90
README.md
@@ -3,7 +3,7 @@
|
||||
# Garmin Connect
|
||||
The Garmin Connect integration allows you to expose data from Garmin Connect to Home Assistant.
|
||||
|
||||
NOTE: This integration doesn't support 2FA on Garmin Connect, so if you have enabled it -and want to keep it- this integration doesn't work, it will try to login repeatedly and generate lots of 2FA codes via email.
|
||||
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.
|
||||
|
||||
## Installation
|
||||
@@ -49,7 +49,6 @@ Daily Step Goal
|
||||
Total KiloCalories
|
||||
Active KiloCalories
|
||||
BMR KiloCalories
|
||||
Consumed KiloCalories
|
||||
Burned KiloCalories
|
||||
Total Distance Mtr
|
||||
Active Time
|
||||
@@ -57,6 +56,7 @@ Sedentary Time
|
||||
Sleeping Time
|
||||
Awake Duration
|
||||
Sleep Duration
|
||||
Total Sleep Duration
|
||||
Floors Ascended
|
||||
Floors Descended
|
||||
Floors Ascended Goal
|
||||
@@ -80,11 +80,19 @@ Body Battery Most Recent
|
||||
Average SPO2
|
||||
Lowest SPO2
|
||||
Latest SPO2
|
||||
Next Alarm Time
|
||||
Total Sleep Duration
|
||||
HRV Status
|
||||
Gear Sensors
|
||||
```
|
||||
|
||||
Disabled by default:
|
||||
|
||||
```text
|
||||
Badges
|
||||
User Points
|
||||
User Level
|
||||
Consumed KiloCalories
|
||||
Remaining KiloCalories
|
||||
Net Remaining KiloCalories
|
||||
Net Calorie Goal
|
||||
@@ -119,22 +127,96 @@ 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
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
alias: uiSendWeightToGarminConnect
|
||||
description: ""
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- sensor.weight
|
||||
condition:
|
||||
- condition: and
|
||||
conditions:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.weight
|
||||
above: 75
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.weight
|
||||
below: 88
|
||||
action:
|
||||
- service: garmin_connect.add_body_composition
|
||||
data:
|
||||
entity_id: sensor.weight
|
||||
weight: "{{trigger.to_state.state}}"
|
||||
timestamp: "{{ as_timestamp(now()) | timestamp_local}}"
|
||||
bmi: >-
|
||||
{{ (trigger.to_state.state | float(0) / 1.86**2 )| round(1, default=0)
|
||||
}}
|
||||
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`:
|
||||
|
||||
0
constraints.txt
Normal file
0
constraints.txt
Normal file
@@ -1,9 +1,10 @@
|
||||
"""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
|
||||
|
||||
from garminconnect import (
|
||||
Garmin,
|
||||
@@ -11,19 +12,20 @@ from garminconnect import (
|
||||
GarminConnectConnectionError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, IntegrationError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
import requests
|
||||
|
||||
from .const import (
|
||||
DATA_COORDINATOR,
|
||||
DAY_TO_NUMBER,
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
GEAR,
|
||||
SERVICE_SETTING,
|
||||
LEVEL_POINTS,
|
||||
Gear,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -66,74 +68,119 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.hass = hass
|
||||
self.in_china = False
|
||||
|
||||
country = self.hass.config.country
|
||||
if country == "CN":
|
||||
self.country = self.hass.config.country
|
||||
if self.country == "CN":
|
||||
self.in_china = True
|
||||
_LOGGER.debug("Country: %s", self.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(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], self.in_china)
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL)
|
||||
|
||||
async def async_login(self) -> bool:
|
||||
"""Login to Garmin Connect."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self._api.login)
|
||||
await self.hass.async_add_executor_job(self.api.login)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
) as err:
|
||||
_LOGGER.error("Error occurred during Garmin Connect login request: %s", err)
|
||||
return False
|
||||
except (GarminConnectConnectionError) as err:
|
||||
_LOGGER.error(
|
||||
"Connection error occurred during Garmin Connect login request: %s", err
|
||||
)
|
||||
except GarminConnectConnectionError as err:
|
||||
_LOGGER.error("Connection error occurred during Garmin Connect login request: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Unknown error occurred during Garmin Connect login request"
|
||||
)
|
||||
_LOGGER.exception("Unknown error occurred during Garmin Connect login request")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch data from Garmin Connect."""
|
||||
summary = {}
|
||||
body = {}
|
||||
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"}
|
||||
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(summary)
|
||||
_LOGGER.debug("Summary data fetched: %s", summary)
|
||||
|
||||
# Body composition
|
||||
body = await self.hass.async_add_executor_job(
|
||||
self._api.get_body_composition, date.today().isoformat()
|
||||
)
|
||||
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()
|
||||
self.api.get_body_composition, today.isoformat()
|
||||
)
|
||||
_LOGGER.debug("Body data fetched: %s", body)
|
||||
|
||||
# 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("Activities data fetched: %s", last_activities)
|
||||
summary["lastActivities"] = last_activities
|
||||
summary["lastActivity"] = last_activities[0] if last_activities else {}
|
||||
|
||||
_LOGGER.debug(body)
|
||||
alarms = await self.hass.async_add_executor_job(self._api.get_device_alarms)
|
||||
_LOGGER.debug(alarms)
|
||||
gear = await self.hass.async_add_executor_job(
|
||||
self._api.get_gear, summary[GEAR.USERPROFILE_ID]
|
||||
)
|
||||
tasks: list[Awaitable] = [
|
||||
self.hass.async_add_executor_job(
|
||||
self._api.get_gear_stats, gear_item[GEAR.UUID]
|
||||
)
|
||||
for gear_item in gear
|
||||
]
|
||||
gear_stats = await asyncio.gather(*tasks)
|
||||
activity_types = await self.hass.async_add_executor_job(
|
||||
self._api.get_activity_types
|
||||
)
|
||||
gear_defaults = await self.hass.async_add_executor_job(
|
||||
self._api.get_gear_defaults, summary[GEAR.USERPROFILE_ID]
|
||||
)
|
||||
# Badges
|
||||
badges = await self.hass.async_add_executor_job(self.api.get_earned_badges)
|
||||
_LOGGER.debug("Badges data fetched: %s", badges)
|
||||
summary["badges"] = badges
|
||||
|
||||
# Calculate user points and user level
|
||||
user_points = 0
|
||||
for badge in badges:
|
||||
user_points += badge["badgePoints"] * badge["badgeEarnedNumber"]
|
||||
|
||||
summary["userPoints"] = user_points
|
||||
|
||||
user_level = 0
|
||||
for level, points in LEVEL_POINTS.items():
|
||||
if user_points >= points:
|
||||
user_level = level
|
||||
|
||||
summary["userLevel"] = user_level
|
||||
|
||||
# Alarms
|
||||
alarms = await self.hass.async_add_executor_job(self.api.get_device_alarms)
|
||||
_LOGGER.debug("Alarms data fetched: %s", alarms)
|
||||
|
||||
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)
|
||||
_LOGGER.debug("Activity types data fetched: %s", activity_types)
|
||||
|
||||
# Sleep data
|
||||
sleep_data = await self.hass.async_add_executor_job(
|
||||
self.api.get_sleep_data, today.isoformat()
|
||||
)
|
||||
_LOGGER.debug("Sleep data fetched: %s", sleep_data)
|
||||
|
||||
# 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)
|
||||
except (
|
||||
GarminConnectAuthenticationError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
@@ -142,60 +189,119 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER.debug("Trying to relogin to Garmin Connect")
|
||||
if not await self.async_login():
|
||||
raise UpdateFailed(error) from error
|
||||
return {}
|
||||
|
||||
summary['lastActivities'] = activities
|
||||
# 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)
|
||||
|
||||
gear_defaults = await self.hass.async_add_executor_job(
|
||||
self.api.get_gear_defaults, summary[Gear.USERPROFILE_ID]
|
||||
)
|
||||
_LOGGER.debug("Gear defaults data fetched: %s", gear_defaults)
|
||||
except (KeyError, TypeError, ValueError, ConnectionError) as err:
|
||||
_LOGGER.debug("Error while fetching Gear data: %s", err)
|
||||
|
||||
# Gear stats data
|
||||
try:
|
||||
tasks: list[Awaitable] = [
|
||||
self.hass.async_add_executor_job(self.api.get_gear_stats, gear_item[Gear.UUID])
|
||||
for gear_item in gear
|
||||
]
|
||||
gear_stats = await asyncio.gather(*tasks)
|
||||
_LOGGER.debug("Gear stats data fetched: %s", gear_stats)
|
||||
except (
|
||||
KeyError,
|
||||
TypeError,
|
||||
ValueError,
|
||||
ConnectionError,
|
||||
requests.exceptions.HTTPError,
|
||||
) as err:
|
||||
_LOGGER.debug("Error while fetching Gear stats data: %s", err)
|
||||
|
||||
# Sleep score data
|
||||
try:
|
||||
sleep_score = sleep_data["dailySleepDTO"]["sleepScores"]["overall"]["value"]
|
||||
_LOGGER.debug("Sleep score data: %s", sleep_score)
|
||||
except KeyError:
|
||||
_LOGGER.debug("Sleep score data is not available")
|
||||
|
||||
# Sleep time seconds data
|
||||
try:
|
||||
sleep_time_seconds = sleep_data["dailySleepDTO"]["sleepTimeSeconds"]
|
||||
_LOGGER.debug("Sleep time seconds data: %s", sleep_time_seconds)
|
||||
except KeyError:
|
||||
_LOGGER.debug("Sleep time seconds data is not available")
|
||||
|
||||
# HRV data
|
||||
try:
|
||||
if hrv_data and "hrvSummary" in hrv_data:
|
||||
hrv_status = hrv_data["hrvSummary"]
|
||||
_LOGGER.debug("HRV summary: %s", hrv_status)
|
||||
except KeyError:
|
||||
_LOGGER.debug("HRV 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,
|
||||
}
|
||||
|
||||
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 garmin next active alarms from settings.
|
||||
Alarms are sorted by time.
|
||||
|
||||
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,
|
||||
Example of alarms data:
|
||||
Alarms data fetched: [{'alarmMode': 'OFF', 'alarmTime': 1233, 'alarmDays': ['ONCE'], 'alarmSound': 'TONE_AND_VIBRATION', 'alarmId': 1737308355, 'changeState': 'UNCHANGED', 'backlight': 'ON', 'enabled': None, 'alarmMessage': None, 'alarmImageId': None, 'alarmIcon': None, 'alarmType': None}]
|
||||
"""
|
||||
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)
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self._api.set_gear_default, activity_type_id, entity.uuid, True
|
||||
)
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Utility method for converting Garmin Connect alarms to python datetime."""
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DAY_TO_NUMBER = {
|
||||
"Mo": 1,
|
||||
"M": 1,
|
||||
"Tu": 2,
|
||||
"We": 3,
|
||||
"W": 3,
|
||||
"Th": 4,
|
||||
"Fr": 5,
|
||||
"F": 5,
|
||||
"Sa": 6,
|
||||
"Su": 7,
|
||||
}
|
||||
|
||||
|
||||
def calculate_next_active_alarms(alarms):
|
||||
"""
|
||||
Calculate garmin next active alarms from settings.
|
||||
|
||||
Alarms are sorted by time
|
||||
"""
|
||||
active_alarms = []
|
||||
|
||||
for alarm_setting in alarms:
|
||||
if alarm_setting["alarmMode"] != "ON":
|
||||
continue
|
||||
for day in alarm_setting["alarmDays"]:
|
||||
alarm_time = alarm_setting["alarmTime"]
|
||||
if day == "ONCE":
|
||||
midnight = datetime.combine(date.today(), datetime.min.time())
|
||||
alarm = midnight + timedelta(minutes=alarm_time)
|
||||
if alarm < datetime.now():
|
||||
alarm += timedelta(days=1)
|
||||
else:
|
||||
start_of_week = datetime.combine(
|
||||
date.today() - timedelta(days=datetime.today().isoweekday() % 7),
|
||||
datetime.min.time(),
|
||||
)
|
||||
days_to_add = DAY_TO_NUMBER[day] % 7
|
||||
alarm = start_of_week + timedelta(minutes=alarm_time, days=days_to_add)
|
||||
if alarm < datetime.now():
|
||||
alarm += timedelta(days=7)
|
||||
active_alarms.append(alarm.isoformat())
|
||||
return sorted(active_alarms) if active_alarms else None
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Config flow for Garmin Connect integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from garminconnect import (
|
||||
@@ -7,10 +8,9 @@ from garminconnect import (
|
||||
GarminConnectConnectionError,
|
||||
GarminConnectTooManyRequestsError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@@ -1,32 +1,77 @@
|
||||
"""Constants for the Garmin Connect integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import NamedTuple
|
||||
|
||||
from homeassistant.const import (
|
||||
LENGTH_METERS,
|
||||
MASS_KILOGRAMS,
|
||||
PERCENTAGE,
|
||||
TIME_MINUTES,
|
||||
TIME_YEARS,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfMass, UnitOfTime
|
||||
|
||||
DOMAIN = "garmin_connect"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
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",
|
||||
@@ -46,7 +91,7 @@ GARMIN_ENTITY_LIST = {
|
||||
"netCalorieGoal": ["Net Calorie Goal", "kcal", "mdi:food", None, SensorStateClass.TOTAL, False],
|
||||
"totalDistanceMeters": [
|
||||
"Total Distance Mtr",
|
||||
LENGTH_METERS,
|
||||
UnitOfLength.METERS,
|
||||
"mdi:walk",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -68,10 +113,17 @@ 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",
|
||||
LENGTH_METERS,
|
||||
UnitOfLength.METERS,
|
||||
"mdi:walk",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -85,21 +137,49 @@ 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",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:fire",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
False,
|
||||
],
|
||||
"activeSeconds": ["Active Time", TIME_MINUTES, "mdi:fire", None, SensorStateClass.TOTAL, True],
|
||||
"sedentarySeconds": ["Sedentary Time", TIME_MINUTES, "mdi:seat", None, SensorStateClass.TOTAL, True],
|
||||
"sleepingSeconds": ["Sleeping Time", TIME_MINUTES, "mdi:sleep", None, SensorStateClass.TOTAL, True],
|
||||
"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",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:sleep",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -107,7 +187,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"measurableAsleepDuration": [
|
||||
"Sleep Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:sleep",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -115,7 +195,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"floorsAscendedInMeters": [
|
||||
"Floors Ascended Mtr",
|
||||
LENGTH_METERS,
|
||||
UnitOfLength.METERS,
|
||||
"mdi:stairs",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -123,14 +203,28 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"floorsDescendedInMeters": [
|
||||
"Floors Descended Mtr",
|
||||
LENGTH_METERS,
|
||||
UnitOfLength.METERS,
|
||||
"mdi:stairs",
|
||||
SensorDeviceClass.DISTANCE,
|
||||
SensorStateClass.TOTAL,
|
||||
False,
|
||||
],
|
||||
"floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
|
||||
"floorsDescended": ["Floors Descended", "floors", "mdi:stairs", None, SensorStateClass.TOTAL, True],
|
||||
"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",
|
||||
@@ -139,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,
|
||||
@@ -157,16 +286,37 @@ 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", TIME_MINUTES, "mdi:flash-alert", None, SensorStateClass.TOTAL, False],
|
||||
"restStressDuration": [
|
||||
"Rest Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -174,7 +324,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"activityStressDuration": [
|
||||
"Activity Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -182,7 +332,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"uncategorizedStressDuration": [
|
||||
"Uncat. Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -190,7 +340,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"totalStressDuration": [
|
||||
"Total Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -198,7 +348,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"lowStressDuration": [
|
||||
"Low Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -206,7 +356,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"mediumStressDuration": [
|
||||
"Medium Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -214,7 +364,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"highStressDuration": [
|
||||
"High Stress Duration",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -225,7 +375,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"restStressPercentage": [
|
||||
@@ -233,7 +383,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"activityStressPercentage": [
|
||||
@@ -241,7 +391,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"uncategorizedStressPercentage": [
|
||||
@@ -249,7 +399,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"lowStressPercentage": [
|
||||
@@ -257,7 +407,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"mediumStressPercentage": [
|
||||
@@ -265,7 +415,7 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"highStressPercentage": [
|
||||
@@ -273,12 +423,12 @@ GARMIN_ENTITY_LIST = {
|
||||
PERCENTAGE,
|
||||
"mdi:flash-alert",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"moderateIntensityMinutes": [
|
||||
"Moderate Intensity",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:flash-alert",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -286,7 +436,7 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"vigorousIntensityMinutes": [
|
||||
"Vigorous Intensity",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:run-fast",
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
@@ -294,9 +444,9 @@ GARMIN_ENTITY_LIST = {
|
||||
],
|
||||
"intensityMinutesGoal": [
|
||||
"Intensity Goal",
|
||||
TIME_MINUTES,
|
||||
UnitOfTime.MINUTES,
|
||||
"mdi:run-fast",
|
||||
None,
|
||||
SensorDeviceClass.DURATION,
|
||||
SensorStateClass.TOTAL,
|
||||
False,
|
||||
],
|
||||
@@ -340,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,
|
||||
@@ -364,7 +535,7 @@ GARMIN_ENTITY_LIST = {
|
||||
"brpm",
|
||||
"mdi:progress-clock",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"lowestRespirationValue": [
|
||||
@@ -372,7 +543,7 @@ GARMIN_ENTITY_LIST = {
|
||||
"brpm",
|
||||
"mdi:progress-clock",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"latestRespirationValue": [
|
||||
@@ -380,7 +551,7 @@ GARMIN_ENTITY_LIST = {
|
||||
"brpm",
|
||||
"mdi:progress-clock",
|
||||
None,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.MEASUREMENT,
|
||||
False,
|
||||
],
|
||||
"latestRespirationTimeGMT": [
|
||||
@@ -391,18 +562,101 @@ GARMIN_ENTITY_LIST = {
|
||||
None,
|
||||
False,
|
||||
],
|
||||
"weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
|
||||
"bmi": ["BMI", "bmi", "mdi:food", None, SensorStateClass.TOTAL, False],
|
||||
"bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
|
||||
"bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, SensorStateClass.TOTAL, False],
|
||||
"boneMass": ["Bone Mass", MASS_KILOGRAMS, "mdi:bone", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
|
||||
"muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, False],
|
||||
"physiqueRating": ["Physique Rating", None, "mdi:numeric", None, SensorStateClass.TOTAL, False],
|
||||
"visceralFat": ["Visceral Fat", PERCENTAGE, "mdi:food", None, SensorStateClass.TOTAL, False],
|
||||
"metabolicAge": ["Metabolic Age", TIME_YEARS, "mdi:calendar-heart", None, SensorStateClass.TOTAL, False],
|
||||
"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]
|
||||
|
||||
"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",
|
||||
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,
|
||||
],
|
||||
}
|
||||
|
||||
GEAR_ICONS = {
|
||||
@@ -413,7 +667,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"
|
||||
@@ -421,7 +675,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"
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
"name": "Garmin Connect",
|
||||
"codeowners": ["@cyberjunky"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://github.com/cyberjunky/home-assistant-garmin_connect",
|
||||
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["garminconnect==0.1.55", "tzlocal"],
|
||||
"version": "0.2.16"
|
||||
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
|
||||
"requirements": ["garminconnect>=0.2.24"],
|
||||
"version": "0.2.30"
|
||||
}
|
||||
|
||||
@@ -1,54 +1,44 @@
|
||||
"""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
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
LENGTH_KILOMETERS,
|
||||
)
|
||||
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 (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities) -> None:
|
||||
"""Set up Garmin Connect sensor based on a config entry."""
|
||||
coordinator: DataUpdateCoordinator = hass.data[GARMIN_DOMAIN][entry.entry_id][
|
||||
DATA_COORDINATOR
|
||||
]
|
||||
coordinator: DataUpdateCoordinator = hass.data[GARMIN_DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
unique_id = entry.data[CONF_ID]
|
||||
|
||||
entities = []
|
||||
@@ -56,7 +46,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,
|
||||
@@ -82,15 +71,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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -98,17 +110,48 @@ 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",
|
||||
{
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
@@ -145,38 +188,45 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
if self._type == "lastActivities":
|
||||
return len(self.coordinator.data[self._type])
|
||||
|
||||
if not self.coordinator.data or not self.coordinator.data[self._type]:
|
||||
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 "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]
|
||||
_LOGGER.debug("Active alarms: %s", active_alarms)
|
||||
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("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
|
||||
@@ -189,13 +239,27 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
"last_synced": self.coordinator.data["lastSyncTimestampGMT"],
|
||||
}
|
||||
|
||||
# Only add 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 add the last 10 badges for performance reasons
|
||||
if self._type == "badges":
|
||||
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"]
|
||||
|
||||
return attributes
|
||||
|
||||
@@ -216,42 +280,96 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.coordinator.data
|
||||
and self._type in self.coordinator.data
|
||||
return super().available and self.coordinator.data and self._type in self.coordinator.data
|
||||
|
||||
async def add_body_composition(self, **kwargs):
|
||||
"""Handle the service call to add body composition."""
|
||||
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):
|
||||
"""Handle the service call to add blood pressure."""
|
||||
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."""
|
||||
|
||||
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 = LENGTH_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):
|
||||
@@ -273,10 +391,8 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
|
||||
gear = self._gear()
|
||||
stats = self._stats()
|
||||
gear_defaults = self._gear_defaults()
|
||||
activity_types = self.coordinator.data["activity_types"]
|
||||
default_for_activity = self._activity_names_for_gear_defaults(
|
||||
gear_defaults, activity_types
|
||||
)
|
||||
activity_types = self.coordinator.data["activityTypes"]
|
||||
default_for_activity = self._activity_names_for_gear_defaults(gear_defaults, activity_types)
|
||||
|
||||
if not self.coordinator.data or not gear or not stats:
|
||||
return {}
|
||||
@@ -302,16 +418,9 @@ 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:
|
||||
@@ -334,21 +443,67 @@ 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 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):
|
||||
"""Handle the service call to set active gear."""
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
set_active_gear:
|
||||
name: Set active gear for activity
|
||||
# target:
|
||||
# entity:
|
||||
# integration: "garmin_connect"
|
||||
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
|
||||
@@ -30,10 +36,119 @@ 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
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: garmin_connect
|
||||
device_class: garmin_gear
|
||||
device_class: weight
|
||||
weight:
|
||||
required: true
|
||||
name: Weight
|
||||
description: Weight in KG
|
||||
example: 82.3
|
||||
timestamp:
|
||||
required: false
|
||||
name: Timestamp
|
||||
description: Datetime string of when the measurements were recorded. Defaults to now.
|
||||
example: 2023-12-30T07:34:00
|
||||
bmi:
|
||||
required: false
|
||||
name: BMI (Body Mass Index)
|
||||
description: Body mass index is based on weight and height.
|
||||
example: 24.7
|
||||
percent_fat:
|
||||
required: false
|
||||
name: Percent Fat
|
||||
description: Percent body fat
|
||||
example: 23.6
|
||||
percent_hydration:
|
||||
required: false
|
||||
name: Percent Hydration
|
||||
description: Percent body hydration
|
||||
example: 51.2
|
||||
visceral_fat_mass:
|
||||
required: false
|
||||
name: Visceral Fat Mass
|
||||
description: Estimated mass of visceral fat in KG
|
||||
example: 45.3
|
||||
bone_mass:
|
||||
required: false
|
||||
name: Bone Mass
|
||||
description: Estimated mass of bones in KG
|
||||
example: 10.1
|
||||
muscle_mass:
|
||||
required: false
|
||||
name: Muscle Mass
|
||||
description: Estimated mass of muscle in KG
|
||||
example: 15.2
|
||||
basal_met:
|
||||
required: false
|
||||
name: Basel Metabolism
|
||||
description: Basel metabolism
|
||||
example: 1900
|
||||
active_met:
|
||||
required: false
|
||||
name: Active Metabolism
|
||||
description: Active metabolism
|
||||
example: 840
|
||||
physique_rating:
|
||||
required: false
|
||||
name: Physique Rating
|
||||
description: Physique Rating
|
||||
example: 28
|
||||
metabolic_age:
|
||||
required: false
|
||||
name: Metabolic Age
|
||||
description: Metabolic Age
|
||||
example: 37
|
||||
visceral_fat_rating:
|
||||
required: false
|
||||
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
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: garmin_connect
|
||||
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'
|
||||
@@ -15,7 +15,7 @@
|
||||
"password": "Heslo",
|
||||
"username": "Užívateľské meno"
|
||||
},
|
||||
"description": "Zadajte svoje poverenia.",
|
||||
"description": "Zadajte svoje poverovacie údaje.",
|
||||
"title": "Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "Garmin Connect",
|
||||
"render_readme": true,
|
||||
"domains": ["sensor"]
|
||||
}
|
||||
"homeassistant": "2024.4.1",
|
||||
"hacs": "1.34.0"
|
||||
}
|
||||
119
pyproject.toml
Normal file
119
pyproject.toml
Normal 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 = []
|
||||
2
requirements_base.txt
Normal file
2
requirements_base.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
colorlog==6.9.0
|
||||
setuptools==75.8.0
|
||||
1
requirements_core_min.txt
Normal file
1
requirements_core_min.txt
Normal file
@@ -0,0 +1 @@
|
||||
homeassistant==2025.1.4
|
||||
8
requirements_lint.txt
Normal file
8
requirements_lint.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
--requirement requirements_base.txt
|
||||
codespell==2.4.1
|
||||
isort==5.13.2
|
||||
pre-commit==4.1.0
|
||||
pre-commit-hooks==5.0.0
|
||||
pyupgrade==3.19.1
|
||||
ruff==0.9.5
|
||||
vulture==2.14
|
||||
23
scripts/develop
Executable file
23
scripts/develop
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
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 python path to include our custom_components directory
|
||||
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
|
||||
|
||||
# Start Home Assistant
|
||||
hass --config "${PWD}/config" --debug
|
||||
7
scripts/install/core
Executable file
7
scripts/install/core
Executable 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
7
scripts/install/core_dev
Executable 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"
|
||||
10
scripts/install/pip_packages
Executable file
10
scripts/install/pip_packages
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
python3 -m pip \
|
||||
install \
|
||||
--upgrade \
|
||||
--disable-pip-version-check \
|
||||
--constraint constraints.txt \
|
||||
"${@}"
|
||||
10
scripts/lint
Executable file
10
scripts/lint
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
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 75 --ignore-names policy
|
||||
12
scripts/setup
Executable file
12
scripts/setup
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
scripts/install/pip_packages "pip<23.2,>=21.3.1"
|
||||
scripts/install/pip_packages setuptools wheel
|
||||
scripts/install/pip_packages --requirement requirements_lint.txt
|
||||
scripts/install/core
|
||||
|
||||
pre-commit install --config .github/pre-commit-config.yaml
|
||||
Reference in New Issue
Block a user