diff --git a/README.md b/README.md index 88b2791..e027da4 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,15 @@ Sensor values depend on your Garmin devices and connected apps. | Hydration Goal | Target intake | | Sweat Loss | Estimated fluid loss | +### Blood Pressure + +| Sensor | Description | +|--------|-------------| +| Systolic | Systolic blood pressure (mmHg) | +| Diastolic | Diastolic blood pressure (mmHg) | +| Pulse | Pulse from blood pressure reading (bpm) | +| Measurement Time | When the BP was measured | + ### Health Monitoring | Sensor | Description | diff --git a/custom_components/garmin_connect/__init__.py b/custom_components/garmin_connect/__init__.py index 7c9f1f4..6b85fe4 100644 --- a/custom_components/garmin_connect/__init__.py +++ b/custom_components/garmin_connect/__init__.py @@ -99,8 +99,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - # Register integration-level services (only once for first entry) - if len(hass.config_entries.async_entries(DOMAIN)) == 1: + # Register integration-level services (only once) + if not hass.services.has_service(DOMAIN, "add_body_composition"): await async_setup_services(hass) return True @@ -112,8 +112,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - # Unload services only if this is the last entry - if unload_ok and len(hass.config_entries.async_entries(DOMAIN)) == 1: + # Unload services only if this is the last entry (no entries remaining after unload) + remaining_entries = len(hass.config_entries.async_entries(DOMAIN)) + if unload_ok and remaining_entries == 1: # This entry is being unloaded await async_unload_services(hass) return bool(unload_ok) diff --git a/custom_components/garmin_connect/coordinator.py b/custom_components/garmin_connect/coordinator.py index 8c929db..3879160 100644 --- a/custom_components/garmin_connect/coordinator.py +++ b/custom_components/garmin_connect/coordinator.py @@ -253,6 +253,41 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): self.api.get_hydration_data, today.isoformat() ) + # Fetch blood pressure data (last 30 days for latest reading) + blood_pressure_data = {} + try: + bp_response = await self.hass.async_add_executor_job( + self.api.get_blood_pressure, + (today - timedelta(days=30)).isoformat(), + today.isoformat(), + ) + _LOGGER.debug( + "Blood pressure API response: type=%s", + type(bp_response).__name__, + ) + # API returns dict with measurementSummaries containing measurements + if bp_response and isinstance(bp_response, dict): + summaries = bp_response.get("measurementSummaries", []) + if summaries: + # Get measurements from the most recent day + latest_summary = summaries[-1] + measurements = latest_summary.get("measurements", []) + if measurements: + latest_bp = measurements[-1] + blood_pressure_data = { + "bpSystolic": latest_bp.get("systolic"), + "bpDiastolic": latest_bp.get("diastolic"), + "bpPulse": latest_bp.get("pulse"), + "bpMeasurementTime": latest_bp.get( + "measurementTimestampLocal" + ), + } + _LOGGER.debug( + "Blood pressure data parsed: %s", blood_pressure_data + ) + except Exception as err: + _LOGGER.debug("Blood pressure data not available: %s", err) + except GarminConnectAuthenticationError as err: raise ConfigEntryAuthFailed from err except GarminConnectTooManyRequestsError: @@ -367,6 +402,7 @@ class GarminConnectDataUpdateCoordinator(DataUpdateCoordinator): **fitnessage_data, **hydration_data, **menstrual_data, + **blood_pressure_data, } diff --git a/custom_components/garmin_connect/sensor_descriptions.py b/custom_components/garmin_connect/sensor_descriptions.py index d8790ae..6479702 100644 --- a/custom_components/garmin_connect/sensor_descriptions.py +++ b/custom_components/garmin_connect/sensor_descriptions.py @@ -1071,6 +1071,38 @@ MENSTRUAL_CYCLE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( ), ) +# Blood Pressure Sensors +BLOOD_PRESSURE_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( + GarminConnectSensorEntityDescription( + key="bpSystolic", + translation_key="bp_systolic", + native_unit_of_measurement="mmHg", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:heart-pulse", + ), + GarminConnectSensorEntityDescription( + key="bpDiastolic", + translation_key="bp_diastolic", + native_unit_of_measurement="mmHg", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:heart-pulse", + ), + GarminConnectSensorEntityDescription( + key="bpPulse", + translation_key="bp_pulse", + native_unit_of_measurement="bpm", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:heart", + ), + GarminConnectSensorEntityDescription( + key="bpMeasurementTime", + translation_key="bp_measurement_time", + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:clock-outline", + value_fn=lambda data: data.get("bpMeasurementTime"), + ), +) + # Diagnostic Sensors DIAGNOSTIC_SENSORS: tuple[GarminConnectSensorEntityDescription, ...] = ( GarminConnectSensorEntityDescription( @@ -1103,6 +1135,7 @@ ALL_SENSOR_DESCRIPTIONS: tuple[GarminConnectSensorEntityDescription, ...] = ( *ADDITIONAL_DISTANCE_SENSORS, *WELLNESS_SENSORS, *MENSTRUAL_CYCLE_SENSORS, + *BLOOD_PRESSURE_SENSORS, *DIAGNOSTIC_SENSORS, ) diff --git a/custom_components/garmin_connect/services.yaml b/custom_components/garmin_connect/services.yaml index c80d6cf..718d8ca 100644 --- a/custom_components/garmin_connect/services.yaml +++ b/custom_components/garmin_connect/services.yaml @@ -36,10 +36,6 @@ set_active_gear: add_body_composition: name: Add body composition description: Add body composition metrics to Garmin Connect. - target: - entity: - integration: garmin_connect - domain: sensor fields: weight: name: Weight @@ -181,10 +177,6 @@ add_body_composition: add_blood_pressure: name: Add blood pressure description: Add blood pressure measurement to Garmin Connect. - target: - entity: - integration: garmin_connect - domain: sensor fields: systolic: name: Systolic diff --git a/custom_components/garmin_connect/translations/en.json b/custom_components/garmin_connect/translations/en.json index 9604859..9c0276d 100644 --- a/custom_components/garmin_connect/translations/en.json +++ b/custom_components/garmin_connect/translations/en.json @@ -379,6 +379,18 @@ "menstrual_period_length": { "name": "Menstrual period length" }, + "bp_systolic": { + "name": "Blood pressure systolic" + }, + "bp_diastolic": { + "name": "Blood pressure diastolic" + }, + "bp_pulse": { + "name": "Blood pressure pulse" + }, + "bp_measurement_time": { + "name": "Blood pressure measurement time" + }, "device_last_synced": { "name": "Device last synced" } diff --git a/docs/garmin_connect.markdown b/docs/garmin_connect.markdown index e233020..c73b6d3 100644 --- a/docs/garmin_connect.markdown +++ b/docs/garmin_connect.markdown @@ -83,6 +83,13 @@ This integration provides **110+ sensors** covering various health and fitness m - **Hydration Goal** - Target intake - **Sweat Loss** - Estimated fluid loss +### Blood Pressure + +- **Systolic** - Systolic blood pressure (mmHg) +- **Diastolic** - Diastolic blood pressure (mmHg) +- **Pulse** - Pulse from blood pressure reading (bpm) +- **Measurement Time** - When the BP was measured + ### Health Monitoring - **SpO2** - Blood oxygen levels (average, lowest, latest)