diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index ba58ee650be..ef91f3368a9 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -56,6 +56,7 @@ from .const import ( CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN, + GOALS_COORDINATOR, LOGGER, MEASUREMENT_COORDINATOR, SLEEP_COORDINATOR, @@ -63,6 +64,7 @@ from .const import ( from .coordinator import ( WithingsBedPresenceDataUpdateCoordinator, WithingsDataUpdateCoordinator, + WithingsGoalsDataUpdateCoordinator, WithingsMeasurementDataUpdateCoordinator, WithingsSleepDataUpdateCoordinator, ) @@ -160,6 +162,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: BED_PRESENCE_COORDINATOR: WithingsBedPresenceDataUpdateCoordinator( hass, client ), + GOALS_COORDINATOR: WithingsGoalsDataUpdateCoordinator(hass, client), } for coordinator in coordinators.values(): diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 4eeaa56c76d..f04500bb3b8 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -16,6 +16,7 @@ PUSH_HANDLER = "push_handler" MEASUREMENT_COORDINATOR = "measurement_coordinator" SLEEP_COORDINATOR = "sleep_coordinator" BED_PRESENCE_COORDINATOR = "bed_presence_coordinator" +GOALS_COORDINATOR = "goals_coordinator" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index ac320aae3ae..2700b833cea 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from typing import TypeVar from aiowithings import ( + Goals, MeasurementType, NotificationCategory, SleepSummary, @@ -170,3 +171,17 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non async def _internal_update_data(self) -> None: """Update coordinator data.""" + + +class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]): + """Withings goals coordinator.""" + + _default_update_interval = timedelta(hours=1) + + def webhook_subscription_listener(self, connected: bool) -> None: + """Call when webhook status changed.""" + # Webhooks aren't available for this datapoint, so we keep polling + + async def _internal_update_data(self) -> Goals: + """Retrieve goals data.""" + return await self._client.get_goals() diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index d09ae550d0f..54c13500e1d 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from aiowithings import MeasurementType, SleepSummary +from aiowithings import Goals, MeasurementType, SleepSummary from homeassistant.components.sensor import ( SensorDeviceClass, @@ -27,6 +27,7 @@ from homeassistant.helpers.typing import StateType from .const import ( DOMAIN, + GOALS_COORDINATOR, MEASUREMENT_COORDINATOR, SCORE_POINTS, SLEEP_COORDINATOR, @@ -37,6 +38,7 @@ from .const import ( ) from .coordinator import ( WithingsDataUpdateCoordinator, + WithingsGoalsDataUpdateCoordinator, WithingsMeasurementDataUpdateCoordinator, WithingsSleepDataUpdateCoordinator, ) @@ -396,6 +398,64 @@ SLEEP_SENSORS = [ ] +STEP_GOAL = "steps" +SLEEP_GOAL = "sleep" +WEIGHT_GOAL = "weight" + + +@dataclass +class WithingsGoalsSensorEntityDescriptionMixin: + """Mixin for describing withings data.""" + + value_fn: Callable[[Goals], StateType] + + +@dataclass +class WithingsGoalsSensorEntityDescription( + SensorEntityDescription, WithingsGoalsSensorEntityDescriptionMixin +): + """Immutable class for describing withings data.""" + + +GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = { + STEP_GOAL: WithingsGoalsSensorEntityDescription( + key="step_goal", + value_fn=lambda goals: goals.steps, + icon="mdi:shoe-print", + translation_key="step_goal", + native_unit_of_measurement="Steps", + state_class=SensorStateClass.MEASUREMENT, + ), + SLEEP_GOAL: WithingsGoalsSensorEntityDescription( + key="sleep_goal", + value_fn=lambda goals: goals.sleep, + icon="mdi:bed-clock", + translation_key="sleep_goal", + native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + WEIGHT_GOAL: WithingsGoalsSensorEntityDescription( + key="weight_goal", + value_fn=lambda goals: goals.weight, + translation_key="weight_goal", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def get_current_goals(goals: Goals) -> set[str]: + """Return a list of present goals.""" + result = set() + for goal in (STEP_GOAL, SLEEP_GOAL, WEIGHT_GOAL): + if getattr(goals, goal): + result.add(goal) + return result + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -406,8 +466,6 @@ async def async_setup_entry( DOMAIN ][entry.entry_id][MEASUREMENT_COORDINATOR] - current_measurement_types = set(measurement_coordinator.data) - entities: list[SensorEntity] = [] entities.extend( WithingsMeasurementSensor( @@ -417,6 +475,8 @@ async def async_setup_entry( if measurement_type in MEASUREMENT_SENSORS ) + current_measurement_types = set(measurement_coordinator.data) + def _async_measurement_listener() -> None: """Listen for new measurements and add sensors if they did not exist.""" received_measurement_types = set(measurement_coordinator.data) @@ -431,6 +491,31 @@ async def async_setup_entry( ) measurement_coordinator.async_add_listener(_async_measurement_listener) + + goals_coordinator: WithingsGoalsDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ][GOALS_COORDINATOR] + + current_goals = get_current_goals(goals_coordinator.data) + + entities.extend( + WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal]) + for goal in current_goals + ) + + def _async_goals_listener() -> None: + """Listen for new goals and add sensors if they did not exist.""" + received_goals = get_current_goals(goals_coordinator.data) + new_goals = received_goals - current_goals + if new_goals: + current_goals.update(new_goals) + async_add_entities( + WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal]) + for goal in new_goals + ) + + goals_coordinator.async_add_listener(_async_goals_listener) + sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ][SLEEP_COORDINATOR] @@ -492,3 +577,17 @@ class WithingsSleepSensor(WithingsSensor): def available(self) -> bool: """Return if the sensor is available.""" return super().available and self.coordinator.data is not None + + +class WithingsGoalsSensor(WithingsSensor): + """Implementation of a Withings goals sensor.""" + + coordinator: WithingsGoalsDataUpdateCoordinator + + entity_description: WithingsGoalsSensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state of the entity.""" + assert self.coordinator.data + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 020509064b3..fcb94d6979a 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -134,6 +134,15 @@ }, "wakeup_time": { "name": "Wakeup time" + }, + "step_goal": { + "name": "Step goal" + }, + "sleep_goal": { + "name": "Sleep goal" + }, + "weight_goal": { + "name": "Weight goal" } } } diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 3f3a82a03f3..0131feba943 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from datetime import timedelta import time from unittest.mock import AsyncMock, patch -from aiowithings import Device, MeasurementGroup, SleepSummary, WithingsClient +from aiowithings import Device, Goals, MeasurementGroup, SleepSummary, WithingsClient from aiowithings.models import NotificationConfiguration import pytest @@ -148,8 +148,12 @@ def mock_withings(): for not_conf in notification_json["profiles"] ] + goals_json = load_json_object_fixture("withings/goals.json") + goals = Goals.from_api(goals_json) + mock = AsyncMock(spec=WithingsClient) mock.get_devices.return_value = devices + mock.get_goals.return_value = goals mock.get_measurement_in_period.return_value = measurement_groups mock.get_measurement_since.return_value = measurement_groups mock.get_sleep_summary_since.return_value = sleep_summaries diff --git a/tests/components/withings/fixtures/goals.json b/tests/components/withings/fixtures/goals.json new file mode 100644 index 00000000000..233ece9aac6 --- /dev/null +++ b/tests/components/withings/fixtures/goals.json @@ -0,0 +1,8 @@ +{ + "steps": 10000, + "sleep": 28800, + "weight": { + "value": 70500, + "unit": -3 + } +} diff --git a/tests/components/withings/fixtures/goals_1.json b/tests/components/withings/fixtures/goals_1.json new file mode 100644 index 00000000000..6b8046f0eb4 --- /dev/null +++ b/tests/components/withings/fixtures/goals_1.json @@ -0,0 +1,6 @@ +{ + "weight": { + "value": 70500, + "unit": -3 + } +} diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index 6a0bee0fbc8..3546a24d2fe 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -1,255 +1,5 @@ # serializer version: 1 -# name: test_all_entities - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Weight', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_weight', - 'last_changed': , - 'last_updated': , - 'state': '70', - }) -# --- -# name: test_all_entities.1 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Fat mass', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_fat_mass', - 'last_changed': , - 'last_updated': , - 'state': '5', - }) -# --- -# name: test_all_entities.10 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Diastolic blood pressure', - 'state_class': , - 'unit_of_measurement': 'mmhg', - }), - 'context': , - 'entity_id': 'sensor.henk_diastolic_blood_pressure', - 'last_changed': , - 'last_updated': , - 'state': '70', - }) -# --- -# name: test_all_entities.11 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Systolic blood pressure', - 'state_class': , - 'unit_of_measurement': 'mmhg', - }), - 'context': , - 'entity_id': 'sensor.henk_systolic_blood_pressure', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.12 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Heart pulse', - 'icon': 'mdi:heart-pulse', - 'state_class': , - 'unit_of_measurement': 'bpm', - }), - 'context': , - 'entity_id': 'sensor.henk_heart_pulse', - 'last_changed': , - 'last_updated': , - 'state': '60', - }) -# --- -# name: test_all_entities.13 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk SpO2', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.henk_spo2', - 'last_changed': , - 'last_updated': , - 'state': '0.95', - }) -# --- -# name: test_all_entities.14 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Hydration', - 'icon': 'mdi:water', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_hydration', - 'last_changed': , - 'last_updated': , - 'state': '0.95', - }) -# --- -# name: test_all_entities.15 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'speed', - 'friendly_name': 'henk Pulse wave velocity', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_pulse_wave_velocity', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.16 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk VO2 max', - 'state_class': , - 'unit_of_measurement': 'ml/min/kg', - }), - 'context': , - 'entity_id': 'sensor.henk_vo2_max', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.17 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Vascular age', - }), - 'context': , - 'entity_id': 'sensor.henk_vascular_age', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.18 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Extracellular water', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_extracellular_water', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.19 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Intracellular water', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_intracellular_water', - 'last_changed': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities.2 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Fat free mass', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_fat_free_mass', - 'last_changed': , - 'last_updated': , - 'state': '60', - }) -# --- -# name: test_all_entities.20 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Breathing disturbances intensity', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.henk_breathing_disturbances_intensity', - 'last_changed': , - 'last_updated': , - 'state': '10', - }) -# --- -# name: test_all_entities.21 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk Deep sleep', - 'icon': 'mdi:sleep', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_deep_sleep', - 'last_changed': , - 'last_updated': , - 'state': '26220', - }) -# --- -# name: test_all_entities.22 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk Time to sleep', - 'icon': 'mdi:sleep', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_time_to_sleep', - 'last_changed': , - 'last_updated': , - 'state': '780', - }) -# --- -# name: test_all_entities.23 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk Time to wakeup', - 'icon': 'mdi:sleep-off', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_time_to_wakeup', - 'last_changed': , - 'last_updated': , - 'state': '996', - }) -# --- -# name: test_all_entities.24 +# name: test_all_entities[sensor.henk_average_heart_rate] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Average heart rate', @@ -264,69 +14,7 @@ 'state': '83', }) # --- -# name: test_all_entities.25 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Maximum heart rate', - 'icon': 'mdi:heart-pulse', - 'state_class': , - 'unit_of_measurement': 'bpm', - }), - 'context': , - 'entity_id': 'sensor.henk_maximum_heart_rate', - 'last_changed': , - 'last_updated': , - 'state': '108', - }) -# --- -# name: test_all_entities.26 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Minimum heart rate', - 'icon': 'mdi:heart-pulse', - 'state_class': , - 'unit_of_measurement': 'bpm', - }), - 'context': , - 'entity_id': 'sensor.henk_minimum_heart_rate', - 'last_changed': , - 'last_updated': , - 'state': '58', - }) -# --- -# name: test_all_entities.27 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk Light sleep', - 'icon': 'mdi:sleep', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_light_sleep', - 'last_changed': , - 'last_updated': , - 'state': '58440', - }) -# --- -# name: test_all_entities.28 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk REM sleep', - 'icon': 'mdi:sleep', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_rem_sleep', - 'last_changed': , - 'last_updated': , - 'state': '17280', - }) -# --- -# name: test_all_entities.29 +# name: test_all_entities[sensor.henk_average_respiratory_rate] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'henk Average respiratory rate', @@ -340,122 +28,22 @@ 'state': '14', }) # --- -# name: test_all_entities.3 +# name: test_all_entities[sensor.henk_body_temperature] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'weight', - 'friendly_name': 'henk Muscle mass', + 'device_class': 'temperature', + 'friendly_name': 'henk Body temperature', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.henk_muscle_mass', + 'entity_id': 'sensor.henk_body_temperature', 'last_changed': , 'last_updated': , - 'state': '50', + 'state': '40', }) # --- -# name: test_all_entities.30 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Maximum respiratory rate', - 'state_class': , - 'unit_of_measurement': 'br/min', - }), - 'context': , - 'entity_id': 'sensor.henk_maximum_respiratory_rate', - 'last_changed': , - 'last_updated': , - 'state': '20', - }) -# --- -# name: test_all_entities.31 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Minimum respiratory rate', - 'state_class': , - 'unit_of_measurement': 'br/min', - }), - 'context': , - 'entity_id': 'sensor.henk_minimum_respiratory_rate', - 'last_changed': , - 'last_updated': , - 'state': '10', - }) -# --- -# name: test_all_entities.32 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Sleep score', - 'icon': 'mdi:medal', - 'state_class': , - 'unit_of_measurement': 'points', - }), - 'context': , - 'entity_id': 'sensor.henk_sleep_score', - 'last_changed': , - 'last_updated': , - 'state': '90', - }) -# --- -# name: test_all_entities.33 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Snoring', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.henk_snoring', - 'last_changed': , - 'last_updated': , - 'state': '1044', - }) -# --- -# name: test_all_entities.34 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Snoring episode count', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.henk_snoring_episode_count', - 'last_changed': , - 'last_updated': , - 'state': '87', - }) -# --- -# name: test_all_entities.35 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Wakeup count', - 'icon': 'mdi:sleep-off', - 'state_class': , - 'unit_of_measurement': 'times', - }), - 'context': , - 'entity_id': 'sensor.henk_wakeup_count', - 'last_changed': , - 'last_updated': , - 'state': '8', - }) -# --- -# name: test_all_entities.36 - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'henk Wakeup time', - 'icon': 'mdi:sleep-off', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.henk_wakeup_time', - 'last_changed': , - 'last_updated': , - 'state': '3468', - }) -# --- -# name: test_all_entities.4 +# name: test_all_entities[sensor.henk_bone_mass] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'weight', @@ -471,7 +59,124 @@ 'state': '10', }) # --- -# name: test_all_entities.5 +# name: test_all_entities[sensor.henk_breathing_disturbances_intensity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Breathing disturbances intensity', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.henk_breathing_disturbances_intensity', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_all_entities[sensor.henk_deep_sleep] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Deep sleep', + 'icon': 'mdi:sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_deep_sleep', + 'last_changed': , + 'last_updated': , + 'state': '26220', + }) +# --- +# name: test_all_entities[sensor.henk_diastolic_blood_pressure] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Diastolic blood pressure', + 'state_class': , + 'unit_of_measurement': 'mmhg', + }), + 'context': , + 'entity_id': 'sensor.henk_diastolic_blood_pressure', + 'last_changed': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_all_entities[sensor.henk_extracellular_water] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Extracellular water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_extracellular_water', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.henk_fat_free_mass] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat free mass', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_free_mass', + 'last_changed': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_all_entities[sensor.henk_fat_mass] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Fat mass', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_fat_mass', + 'last_changed': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_all_entities[sensor.henk_fat_ratio] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Fat ratio', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.henk_fat_ratio', + 'last_changed': , + 'last_updated': , + 'state': '0.07', + }) +# --- +# name: test_all_entities[sensor.henk_heart_pulse] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Heart pulse', + 'icon': 'mdi:heart-pulse', + 'state_class': , + 'unit_of_measurement': 'bpm', + }), + 'context': , + 'entity_id': 'sensor.henk_heart_pulse', + 'last_changed': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_all_entities[sensor.henk_height] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'distance', @@ -486,37 +191,158 @@ 'state': '2', }) # --- -# name: test_all_entities.6 +# name: test_all_entities[sensor.henk_hydration] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'henk Temperature', + 'device_class': 'weight', + 'friendly_name': 'henk Hydration', + 'icon': 'mdi:water', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.henk_temperature', + 'entity_id': 'sensor.henk_hydration', 'last_changed': , 'last_updated': , - 'state': '40', + 'state': '0.95', }) # --- -# name: test_all_entities.7 +# name: test_all_entities[sensor.henk_intracellular_water] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'henk Body temperature', + 'device_class': 'weight', + 'friendly_name': 'henk Intracellular water', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.henk_body_temperature', + 'entity_id': 'sensor.henk_intracellular_water', 'last_changed': , 'last_updated': , - 'state': '40', + 'state': '100', }) # --- -# name: test_all_entities.8 +# name: test_all_entities[sensor.henk_light_sleep] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Light sleep', + 'icon': 'mdi:sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_light_sleep', + 'last_changed': , + 'last_updated': , + 'state': '58440', + }) +# --- +# name: test_all_entities[sensor.henk_maximum_heart_rate] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Maximum heart rate', + 'icon': 'mdi:heart-pulse', + 'state_class': , + 'unit_of_measurement': 'bpm', + }), + 'context': , + 'entity_id': 'sensor.henk_maximum_heart_rate', + 'last_changed': , + 'last_updated': , + 'state': '108', + }) +# --- +# name: test_all_entities[sensor.henk_maximum_respiratory_rate] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Maximum respiratory rate', + 'state_class': , + 'unit_of_measurement': 'br/min', + }), + 'context': , + 'entity_id': 'sensor.henk_maximum_respiratory_rate', + 'last_changed': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_all_entities[sensor.henk_minimum_heart_rate] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Minimum heart rate', + 'icon': 'mdi:heart-pulse', + 'state_class': , + 'unit_of_measurement': 'bpm', + }), + 'context': , + 'entity_id': 'sensor.henk_minimum_heart_rate', + 'last_changed': , + 'last_updated': , + 'state': '58', + }) +# --- +# name: test_all_entities[sensor.henk_minimum_respiratory_rate] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Minimum respiratory rate', + 'state_class': , + 'unit_of_measurement': 'br/min', + }), + 'context': , + 'entity_id': 'sensor.henk_minimum_respiratory_rate', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_all_entities[sensor.henk_muscle_mass] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Muscle mass', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_muscle_mass', + 'last_changed': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_all_entities[sensor.henk_pulse_wave_velocity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'henk Pulse wave velocity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_pulse_wave_velocity', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.henk_rem_sleep] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk REM sleep', + 'icon': 'mdi:sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_rem_sleep', + 'last_changed': , + 'last_updated': , + 'state': '17280', + }) +# --- +# name: test_all_entities[sensor.henk_skin_temperature] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -531,17 +357,237 @@ 'state': '20', }) # --- -# name: test_all_entities.9 +# name: test_all_entities[sensor.henk_sleep_goal] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'henk Fat ratio', + 'device_class': 'duration', + 'friendly_name': 'henk Sleep goal', + 'icon': 'mdi:bed-clock', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_sleep_goal', + 'last_changed': , + 'last_updated': , + 'state': '28800', + }) +# --- +# name: test_all_entities[sensor.henk_sleep_score] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Sleep score', + 'icon': 'mdi:medal', + 'state_class': , + 'unit_of_measurement': 'points', + }), + 'context': , + 'entity_id': 'sensor.henk_sleep_score', + 'last_changed': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_all_entities[sensor.henk_snoring] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Snoring', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.henk_snoring', + 'last_changed': , + 'last_updated': , + 'state': '1044', + }) +# --- +# name: test_all_entities[sensor.henk_snoring_episode_count] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Snoring episode count', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.henk_snoring_episode_count', + 'last_changed': , + 'last_updated': , + 'state': '87', + }) +# --- +# name: test_all_entities[sensor.henk_spo2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk SpO2', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.henk_fat_ratio', + 'entity_id': 'sensor.henk_spo2', 'last_changed': , 'last_updated': , - 'state': '0.07', + 'state': '0.95', + }) +# --- +# name: test_all_entities[sensor.henk_step_goal] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Step goal', + 'icon': 'mdi:shoe-print', + 'state_class': , + 'unit_of_measurement': 'Steps', + }), + 'context': , + 'entity_id': 'sensor.henk_step_goal', + 'last_changed': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_all_entities[sensor.henk_systolic_blood_pressure] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Systolic blood pressure', + 'state_class': , + 'unit_of_measurement': 'mmhg', + }), + 'context': , + 'entity_id': 'sensor.henk_systolic_blood_pressure', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.henk_temperature] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'henk Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_temperature', + 'last_changed': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_all_entities[sensor.henk_time_to_sleep] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Time to sleep', + 'icon': 'mdi:sleep', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_time_to_sleep', + 'last_changed': , + 'last_updated': , + 'state': '780', + }) +# --- +# name: test_all_entities[sensor.henk_time_to_wakeup] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Time to wakeup', + 'icon': 'mdi:sleep-off', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_time_to_wakeup', + 'last_changed': , + 'last_updated': , + 'state': '996', + }) +# --- +# name: test_all_entities[sensor.henk_vascular_age] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Vascular age', + }), + 'context': , + 'entity_id': 'sensor.henk_vascular_age', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.henk_vo2_max] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk VO2 max', + 'state_class': , + 'unit_of_measurement': 'ml/min/kg', + }), + 'context': , + 'entity_id': 'sensor.henk_vo2_max', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.henk_wakeup_count] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'henk Wakeup count', + 'icon': 'mdi:sleep-off', + 'state_class': , + 'unit_of_measurement': 'times', + }), + 'context': , + 'entity_id': 'sensor.henk_wakeup_count', + 'last_changed': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_all_entities[sensor.henk_wakeup_time] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'henk Wakeup time', + 'icon': 'mdi:sleep-off', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_wakeup_time', + 'last_changed': , + 'last_updated': , + 'state': '3468', + }) +# --- +# name: test_all_entities[sensor.henk_weight] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Weight', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_weight', + 'last_changed': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_all_entities[sensor.henk_weight_goal] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'henk Weight goal', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.henk_weight_goal', + 'last_changed': , + 'last_updated': , + 'state': '70.5', }) # --- diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 2e5be3e74aa..6738d9a3eb4 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aiowithings import MeasurementGroup +from aiowithings import Goals, MeasurementGroup from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -38,7 +38,9 @@ async def test_all_entities( assert entity_entries for entity_entry in entity_entries: - assert hass.states.get(entity_entry.entity_id) == snapshot + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=entity_entry.entity_id + ) async def test_update_failed( @@ -135,3 +137,29 @@ async def test_update_new_measurement_creates_new_sensor( await hass.async_block_till_done() assert hass.states.get("sensor.henk_fat_mass") is not None + + +async def test_update_new_goals_creates_new_sensor( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test fetching new goals will add a new sensor.""" + goals_json = load_json_object_fixture("withings/goals_1.json") + goals = Goals.from_api(goals_json) + withings.get_goals.return_value = goals + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_step_goal") is None + assert hass.states.get("sensor.henk_weight_goal") is not None + + goals_json = load_json_object_fixture("withings/goals.json") + goals = Goals.from_api(goals_json) + withings.get_goals.return_value = goals + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_step_goal") is not None