mirror of
https://github.com/cyberjunky/home-assistant-garmin_connect.git
synced 2026-01-07 20:13:57 -05:00
Removed lastupdated attributes, created diag sensor, added endurance score, fixed midnight timegap (test)
This commit is contained in:
@@ -112,16 +112,40 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
hrv_status = {"status": "unknown"}
|
||||
endurance_data = {}
|
||||
endurance_status = {"overallScore": None}
|
||||
hill_data = {}
|
||||
hill_status = {"overallScore": None}
|
||||
menstrual_data = {}
|
||||
next_alarms: list[str] | None = []
|
||||
|
||||
today = datetime.now(ZoneInfo(self.time_zone)).date()
|
||||
current_hour = datetime.now(ZoneInfo(self.time_zone)).hour
|
||||
yesterday_date = (today - timedelta(days=1)).isoformat()
|
||||
_LOGGER.debug("Fetching data for date: %s (timezone: %s, hour: %s)", today.isoformat(), self.time_zone, current_hour)
|
||||
|
||||
try:
|
||||
summary = await self.hass.async_add_executor_job(
|
||||
self.api.get_user_summary, today.isoformat()
|
||||
)
|
||||
|
||||
# Midnight fallback: During 0:00-2:00 window, if today's data is empty/None,
|
||||
# fallback to yesterday's data to avoid gaps due to UTC/GMT timezone differences
|
||||
if current_hour < 2 and (not summary or summary.get("totalSteps") in (None, 0)):
|
||||
_LOGGER.debug("Midnight fallback: Today's data empty, fetching yesterday's data")
|
||||
yesterday_summary = await self.hass.async_add_executor_job(
|
||||
self.api.get_user_summary, yesterday_date
|
||||
)
|
||||
if yesterday_summary and yesterday_summary.get("totalSteps"):
|
||||
summary = yesterday_summary
|
||||
_LOGGER.debug("Using yesterday's summary data as fallback")
|
||||
|
||||
_LOGGER.debug(
|
||||
"Summary data for %s: totalSteps=%s, dailyStepGoal=%s, lastSync=%s",
|
||||
today.isoformat(),
|
||||
summary.get("totalSteps"),
|
||||
summary.get("dailyStepGoal"),
|
||||
summary.get("lastSyncTimestampGMT"),
|
||||
)
|
||||
|
||||
# Fetch last 7 days steps for weekly average and yesterday's final count
|
||||
week_ago = (today - timedelta(days=7)).isoformat()
|
||||
yesterday = (today - timedelta(days=1)).isoformat()
|
||||
@@ -191,6 +215,10 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.api.get_endurance_score, today.isoformat()
|
||||
)
|
||||
|
||||
hill_data = await self.hass.async_add_executor_job(
|
||||
self.api.get_hill_score, today.isoformat()
|
||||
)
|
||||
|
||||
try:
|
||||
menstrual_data = await self.hass.async_add_executor_job(
|
||||
self.api.get_menstrual_data_for_date, today.isoformat()
|
||||
@@ -309,6 +337,12 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if hill_data and "overallScore" in hill_data:
|
||||
hill_status = hill_data
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return {
|
||||
**summary,
|
||||
**body["totalAverage"],
|
||||
@@ -329,6 +363,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"weeklyDistanceAvg": weekly_distance_avg,
|
||||
"hrvStatus": hrv_status,
|
||||
"enduranceScore": endurance_status,
|
||||
"hillScore": hill_status,
|
||||
**fitnessage_data,
|
||||
**hydration_data,
|
||||
**menstrual_data,
|
||||
|
||||
@@ -27,7 +27,7 @@ rules:
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices: exempt # Single cloud account, not device-based
|
||||
discovery: exempt # Cloud service, not discoverable
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
stale-devices: exempt # Single device per config entry
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -187,9 +186,17 @@ class GarminConnectSensor(GarminConnectEntity, SensorEntity, RestoreEntity):
|
||||
# Handle timestamp device class
|
||||
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
if value:
|
||||
value = datetime.datetime.fromisoformat(value).replace(
|
||||
tzinfo=ZoneInfo(self.coordinator.time_zone)
|
||||
)
|
||||
try:
|
||||
# Parse ISO format timestamp and set to UTC (GMT)
|
||||
parsed = datetime.datetime.fromisoformat(value)
|
||||
# If naive, assume UTC since Garmin returns GMT timestamps
|
||||
if parsed.tzinfo is None:
|
||||
value = parsed.replace(tzinfo=datetime.UTC)
|
||||
else:
|
||||
value = parsed
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER.debug("Could not parse timestamp: %s", value)
|
||||
value = None
|
||||
|
||||
# Preserve int types, only round floats
|
||||
if isinstance(value, int):
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfLength,
|
||||
UnitOfMass,
|
||||
UnitOfTime,
|
||||
@@ -728,10 +729,19 @@ FITNESS_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
|
||||
value_fn=lambda data: data.get("enduranceScore", {}).get("overallScore"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
**{k: v for k, v in data.get("enduranceScore", {}).items() if k != "overallScore"},
|
||||
},
|
||||
),
|
||||
GarminConnectSensorEntityDescription(
|
||||
key="hillScore",
|
||||
translation_key="hill_score",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:terrain",
|
||||
value_fn=lambda data: data.get("hillScore", {}).get("overallScore"),
|
||||
attributes_fn=lambda data: {
|
||||
**{k: v for k, v in data.get("hillScore", {}).items() if k != "overallScore"},
|
||||
},
|
||||
),
|
||||
GarminConnectSensorEntityDescription(
|
||||
key="physiqueRating",
|
||||
translation_key="physique_rating",
|
||||
@@ -758,7 +768,7 @@ ACTIVITY_TRACKING_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
icon="mdi:alarm",
|
||||
value_fn=lambda data: data.get("nextAlarm", [None])[0] if data.get("nextAlarm") else None,
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
"next_alarms": data.get("nextAlarm"),
|
||||
},
|
||||
),
|
||||
@@ -769,7 +779,7 @@ ACTIVITY_TRACKING_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
|
||||
value_fn=lambda data: data.get("lastActivity", {}).get("activityName"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
**data.get("lastActivity", {}),
|
||||
},
|
||||
),
|
||||
@@ -781,7 +791,7 @@ ACTIVITY_TRACKING_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
|
||||
value_fn=lambda data: len(data.get("lastActivities", [])),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
"last_activities": sorted(
|
||||
data.get("lastActivities", []),
|
||||
key=lambda x: x.get("activityId", 0),
|
||||
@@ -796,7 +806,7 @@ ACTIVITY_TRACKING_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
|
||||
value_fn=lambda data: len(data.get("badges", [])),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
"badges": sorted(
|
||||
data.get("badges", []),
|
||||
key=lambda x: x.get("badgeEarnedDate", ""),
|
||||
@@ -1012,7 +1022,7 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
icon="mdi:calendar-heart",
|
||||
value_fn=lambda data: data.get("menstrualData", {}).get("currentPhase"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
**{k: v for k, v in data.get("menstrualData", {}).items()
|
||||
if k not in ("currentPhase",)},
|
||||
},
|
||||
@@ -1024,7 +1034,7 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.get("menstrualData", {}).get("dayOfCycle"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
},
|
||||
),
|
||||
GarminConnectSensorEntityDescription(
|
||||
@@ -1034,7 +1044,7 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.get("menstrualData", {}).get("dayOfPeriod"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
},
|
||||
),
|
||||
GarminConnectSensorEntityDescription(
|
||||
@@ -1045,7 +1055,7 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.get("menstrualData", {}).get("cycleLength"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
},
|
||||
),
|
||||
GarminConnectSensorEntityDescription(
|
||||
@@ -1056,11 +1066,23 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.get("menstrualData", {}).get("periodLength"),
|
||||
attributes_fn=lambda data: {
|
||||
"last_synced": data.get("lastSyncTimestampGMT"),
|
||||
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Diagnostic Sensors
|
||||
DIAGNOSTIC_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
GarminConnectSensorEntityDescription(
|
||||
key="lastSyncTimestampGMT",
|
||||
translation_key="last_synced",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:sync",
|
||||
# Value is already an ISO timestamp string, just pass it through
|
||||
value_fn=lambda data: data.get("lastSyncTimestampGMT"),
|
||||
),
|
||||
)
|
||||
|
||||
ALL_SENSOR_DESCRIPTIONS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
*ACTIVITY_SENSORS,
|
||||
@@ -1081,6 +1103,7 @@ ALL_SENSOR_DESCRIPTIONS: tuple[GarminConnectSensorEntityDescription, ...] = (
|
||||
*ADDITIONAL_DISTANCE_SENSORS,
|
||||
*WELLNESS_SENSORS,
|
||||
*MENSTRUAL_CYCLE_SENSORS,
|
||||
*DIAGNOSTIC_SENSORS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,24 +33,5 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"menstrual_cycle_phase": {
|
||||
"name": "Menstrual cycle phase"
|
||||
},
|
||||
"menstrual_cycle_day": {
|
||||
"name": "Menstrual cycle day"
|
||||
},
|
||||
"menstrual_period_day": {
|
||||
"name": "Menstrual period day"
|
||||
},
|
||||
"menstrual_cycle_length": {
|
||||
"name": "Menstrual cycle length"
|
||||
},
|
||||
"menstrual_period_length": {
|
||||
"name": "Menstrual period length"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,6 +325,9 @@
|
||||
"endurance_score": {
|
||||
"name": "Endurance score"
|
||||
},
|
||||
"hill_score": {
|
||||
"name": "Hill score"
|
||||
},
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
@@ -375,6 +378,9 @@
|
||||
},
|
||||
"menstrual_period_length": {
|
||||
"name": "Menstrual period length"
|
||||
},
|
||||
"last_synced": {
|
||||
"name": "Last synced"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
python_version = "3.13"
|
||||
ignore_missing_imports = true
|
||||
check_untyped_defs = true
|
||||
warn_return_any = true
|
||||
@@ -12,7 +12,7 @@ warn_unused_ignores = true
|
||||
show_error_codes = true
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
target-version = "py313"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
@@ -12,5 +12,3 @@ ruff==0.14.10
|
||||
types-pytz==2025.2.0.20251108
|
||||
types-requests
|
||||
vulture==2.14
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user