From 91c8b1fa3606da33cd05200936225de1049e4976 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 31 Jul 2026 08:47:20 -0700 Subject: [PATCH] Update Google Health sensor units to be more reasonable (#177657) --- .../components/google_health/sensor.py | 22 +++++++ .../google_health/snapshots/test_sensor.ambr | 9 ++- tests/components/google_health/test_sensor.py | 61 +++++++++++++++++-- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_health/sensor.py b/homeassistant/components/google_health/sensor.py index 109916395467..53a1a3722dc3 100644 --- a/homeassistant/components/google_health/sensor.py +++ b/homeassistant/components/google_health/sensor.py @@ -27,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem from . import GoogleHealthConfigEntry from .const import DOMAIN @@ -50,6 +51,7 @@ class GoogleHealthSensorEntityDescription[ """Class describing Google Health sensor entities.""" value_fn: Callable[[Any], _ValueT] + suggested_unit_fn: Callable[[UnitSystem], str | None] | None = None ACTIVITY_SENSORS: list[ @@ -69,6 +71,11 @@ ACTIVITY_SENSORS: list[ value_fn=lambda data: ( data.distance.millimeters_sum / 1000.0 if data and data.distance else 0.0 ), + suggested_unit_fn=lambda units: ( + UnitOfLength.MILES + if units is US_CUSTOMARY_SYSTEM + else UnitOfLength.KILOMETERS + ), ), GoogleHealthSensorEntityDescription[GoogleHealthActivityCoordinator, float]( key="active_calories", @@ -109,6 +116,9 @@ BODY_SENSORS: list[ value_fn=lambda data: ( data.weight.weight_grams / 1000.0 if data and data.weight else None ), + suggested_unit_fn=lambda units: ( + UnitOfMass.POUNDS if units is US_CUSTOMARY_SYSTEM else None + ), ), GoogleHealthSensorEntityDescription[GoogleHealthBodyCoordinator, int | None]( key="resting_heart_rate", @@ -212,6 +222,9 @@ NUTRITION_SENSORS: list[ if data and data.hydration and data.hydration.amount_consumed else 0.0 ), + suggested_unit_fn=lambda units: ( + UnitOfVolume.FLUID_OUNCES if units is US_CUSTOMARY_SYSTEM else None + ), ), GoogleHealthSensorEntityDescription[GoogleHealthNutritionCoordinator, float]( key="calories_consumed", @@ -345,6 +358,15 @@ class GoogleHealthSensor[_CoordinatorT: GoogleHealthDataUpdateCoordinator[Any]]( """Return the state of the sensor.""" return cast(StateType, self.entity_description.value_fn(self.coordinator.data)) + @property + @override + def suggested_unit_of_measurement(self) -> str | None: + """Return the suggested unit of measurement.""" + if (suggested_unit_fn := self.entity_description.suggested_unit_fn) is not None: + return suggested_unit_fn(self.hass.config.units) + + return super().suggested_unit_of_measurement + class GoogleHealthDeviceSensor( CoordinatorEntity[GoogleHealthDeviceCoordinator], SensorEntity diff --git a/tests/components/google_health/snapshots/test_sensor.ambr b/tests/components/google_health/snapshots/test_sensor.ambr index fc8e4cb9e3b1..a1a0471943a2 100644 --- a/tests/components/google_health/snapshots/test_sensor.ambr +++ b/tests/components/google_health/snapshots/test_sensor.ambr @@ -402,6 +402,9 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -412,7 +415,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_distance', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_all_entities[sensor.google_health_distance-state] @@ -421,14 +424,14 @@ : 'distance', : 'Google Health Distance', : , - : , + : , }), 'context': , 'entity_id': 'sensor.google_health_distance', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5000.0', + 'state': '5.0', }) # --- # name: test_all_entities[sensor.google_health_floors-entry] diff --git a/tests/components/google_health/test_sensor.py b/tests/components/google_health/test_sensor.py index 9e75cbfe8309..4530561d04a9 100644 --- a/tests/components/google_health/test_sensor.py +++ b/tests/components/google_health/test_sensor.py @@ -1,15 +1,20 @@ """Tests for Google Health sensor platform.""" from collections.abc import Awaitable, Callable -from unittest.mock import AsyncMock, patch +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch -from google_health_api.model import ListDataPointResult, _ListDataPointsModel import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util.unit_system import ( + METRIC_SYSTEM, + US_CUSTOMARY_SYSTEM, + UnitSystem, +) from tests.common import MockConfigEntry, snapshot_platform @@ -80,12 +85,58 @@ async def test_sensor_empty_sleep( integration_setup: Callable[[], Awaitable[bool]], ) -> None: """Test sleep sensors when the sleep endpoint returns no data.""" - mock_google_health_client.sleep.list.return_value = ListDataPointResult( - _ListDataPointsModel(data_points=[]) - ) + mock_google_health_client.sleep.list.return_value = MagicMock(data_points=[]) assert await integration_setup() time_asleep_state = hass.states.get("sensor.google_health_time_asleep") assert time_asleep_state is not None assert time_asleep_state.state == "unknown" + + +@pytest.mark.parametrize( + ("unit_system", "expected_sensors"), + [ + pytest.param( + METRIC_SYSTEM, + { + "sensor.google_health_weight": (pytest.approx(80.0), "kg"), + "sensor.google_health_distance": (pytest.approx(5.0), "km"), + "sensor.google_health_water_intake": (pytest.approx(2.5), "L"), + }, + id="metric", + ), + pytest.param( + US_CUSTOMARY_SYSTEM, + { + "sensor.google_health_weight": (pytest.approx(176.37, abs=1e-2), "lb"), + "sensor.google_health_distance": ( + pytest.approx(3.11, abs=1e-2), + "mi", + ), + "sensor.google_health_water_intake": ( + pytest.approx(84.54, abs=1e-1), + "fl. oz.", + ), + }, + id="us_customary", + ), + ], +) +@pytest.mark.usefixtures("mock_google_health_client") +async def test_sensor_unit_conversions( + hass: HomeAssistant, + integration_setup: Callable[[], Awaitable[bool]], + unit_system: UnitSystem, + expected_sensors: dict[str, tuple[Any, str]], +) -> None: + """Test sensors dynamically convert states and units under different unit systems.""" + hass.config.units = unit_system + + assert await integration_setup() + + for entity_id, (expected_state, expected_unit) in expected_sensors.items(): + state = hass.states.get(entity_id) + assert state is not None + assert float(state.state) == expected_state + assert state.attributes.get("unit_of_measurement") == expected_unit