Update Google Health sensor units to be more reasonable (#177657)

This commit is contained in:
Allen Porter
2026-07-31 17:47:20 +02:00
committed by GitHub
parent a5f4dceba4
commit 91c8b1fa36
3 changed files with 84 additions and 8 deletions
@@ -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
@@ -402,6 +402,9 @@
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
}),
}),
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None,
@@ -412,7 +415,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_distance',
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
})
# ---
# name: test_all_entities[sensor.google_health_distance-state]
@@ -421,14 +424,14 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'distance',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Google Health Distance',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfLength.METERS: 'm'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfLength.KILOMETERS: 'km'>,
}),
'context': <ANY>,
'entity_id': 'sensor.google_health_distance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5000.0',
'state': '5.0',
})
# ---
# name: test_all_entities[sensor.google_health_floors-entry]
+56 -5
View File
@@ -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