Compare commits

...

18 Commits

Author SHA1 Message Date
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
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
11 changed files with 176 additions and 38 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

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

@@ -90,6 +90,8 @@ Disabled by default:
```text
Badges
User Points
User Level
Consumed KiloCalories
Remaining KiloCalories
Net Remaining KiloCalories
@@ -134,6 +136,8 @@ Muscle Mass
Physique Rating
Visceral Fat
Metabolic Age
Last Activities
Last Activity
```
## Screenshots
@@ -174,7 +178,7 @@ action:
mode: single
```
### Examples on how to test services from HA GUI
### Examples on how to test actions from HA GUI
#### Add Body Composition
@@ -185,10 +189,10 @@ data:
weight: 87
bmi: 25.5
bone_mass: 4.8
...
```
See the action template for other available values to add
NOTE: You need to enable Weight entity
NOTE: You need to enable the Weight entity
#### Set Active Gear

View File

@@ -17,12 +17,14 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
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,
LEVEL_POINTS,
Gear,
)
@@ -106,6 +108,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
gear_stats = {}
gear_defaults = {}
activity_types = {}
last_activities = []
sleep_data = {}
sleep_score = None
sleep_time_seconds = None
@@ -116,41 +119,64 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
today = datetime.now(ZoneInfo(self.time_zone)).date()
try:
# User summary
summary = await self.hass.async_add_executor_job(
self.api.get_user_summary, today.isoformat()
)
_LOGGER.debug("Summary data fetched: %s", summary)
# Body composition
body = await self.hass.async_add_executor_job(
self.api.get_body_composition, today.isoformat()
)
_LOGGER.debug("Body data fetched: %s", body)
activities = await self.hass.async_add_executor_job(
# 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", activities)
summary["lastActivities"] = activities
_LOGGER.debug("Activities data fetched: %s", last_activities)
summary["lastActivities"] = last_activities
summary["lastActivity"] = last_activities[0] if last_activities else {}
# 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()
)
@@ -164,38 +190,52 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
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)
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)
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("Gear data is not available: %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"]
@@ -208,9 +248,9 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
**body["totalAverage"],
"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,

View File

@@ -622,7 +622,17 @@ GARMIN_ENTITY_LIST = {
],
"nextAlarm": ["Next Alarm Time", None, "mdi:alarm", SensorDeviceClass.TIMESTAMP, None, True],
"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,

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/cyberjunky/home-assistant-garmin_connect/issues",
"requirements": ["garminconnect>=0.2.24"],
"version": "0.2.28"
"version": "0.2.30"
}

View File

@@ -198,6 +198,9 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
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()
@@ -236,13 +239,19 @@ 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:]
# Only show the last 10 badges for performance reasons
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'])
sorted_badges = sorted(badges, key=lambda x: x["badgeEarnedDate"])
attributes["badges"] = sorted_badges[-10:]
if self._type == "nextAlarm":
@@ -291,8 +300,7 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update")
raise IntegrationError("Failed to login to Garmin Connect, unable to update")
"""Record a weigh in/body composition."""
await self.hass.async_add_executor_job(
@@ -322,8 +330,7 @@ class GarminConnectSensor(CoordinatorEntity, SensorEntity):
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update")
raise IntegrationError("Failed to login to Garmin Connect, unable to update")
"""Record a blood pressure measurement."""
await self.hass.async_add_executor_job(
@@ -384,9 +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 {}
@@ -437,7 +443,7 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
def _stats(self):
"""Get gear statistics from garmin"""
for gear_stats_item in self.coordinator.data["gear_stats"]:
for gear_stats_item in self.coordinator.data["gearStats"]:
if gear_stats_item[Gear.UUID] == self._uuid:
return gear_stats_item
@@ -452,7 +458,7 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
return list(
filter(
lambda d: d[Gear.UUID] == self.uuid and d["defaultGear"] is True,
self.coordinator.data["gear_defaults"],
self.coordinator.data["gearDefaults"],
)
)
@@ -463,14 +469,13 @@ class GarminConnectGearSensor(CoordinatorEntity, SensorEntity):
"""Check for login."""
if not await self.coordinator.async_login():
raise IntegrationError(
"Failed to login to Garmin Connect, unable to update")
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["activity_types"],
self.coordinator.data["activityTypes"],
)
)[Gear.TYPE_ID]
if setting != ServiceSetting.ONLY_THIS_AS_DEFAULT:

View File

@@ -1,2 +1,2 @@
colorlog==6.9.0
setuptools==75.8.0
setuptools==75.8.0

View File

@@ -1,2 +1 @@
# homeassistant==2024.4.1
homeassistant==2025.1.2
homeassistant==2025.1.4

View File

@@ -1,8 +1,8 @@
--requirement requirements_base.txt
codespell==2.3.0
codespell==2.4.1
isort==5.13.2
pre-commit==4.0.1
pre-commit==4.1.0
pre-commit-hooks==5.0.0
pyupgrade==3.19.1
ruff==0.9.2
ruff==0.9.5
vulture==2.14