From 42c062de68bed451dd007cb4431dea2f7a260f4d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 23 Oct 2023 12:59:13 +0200 Subject: [PATCH] Only add Withings sleep sensors when we have data (#102578) Co-authored-by: Robert Resch --- homeassistant/components/withings/sensor.py | 37 +++++++++---- tests/components/withings/__init__.py | 10 +++- tests/components/withings/conftest.py | 10 ++-- .../components/withings/test_binary_sensor.py | 4 +- tests/components/withings/test_sensor.py | 53 ++++++++++++++----- 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 0d841c4bb2c..a531bf49986 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -263,7 +263,6 @@ SLEEP_SENSORS = [ icon="mdi:sleep", device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=False, ), WithingsSleepSensorEntityDescription( key="sleep_tosleep_duration_seconds", @@ -645,9 +644,33 @@ async def async_setup_entry( sleep_coordinator = withings_data.sleep_coordinator - entities.extend( - WithingsSleepSensor(sleep_coordinator, attribute) for attribute in SLEEP_SENSORS + sleep_entities_setup_before = ent_reg.async_get_entity_id( + Platform.SENSOR, + DOMAIN, + f"withings_{entry.unique_id}_sleep_deep_duration_seconds", ) + + if sleep_coordinator.data is not None or sleep_entities_setup_before: + entities.extend( + WithingsSleepSensor(sleep_coordinator, attribute) + for attribute in SLEEP_SENSORS + ) + else: + remove_listener: Callable[[], None] + + def _async_add_sleep_entities() -> None: + """Add sleep entities.""" + if sleep_coordinator.data is not None: + async_add_entities( + WithingsSleepSensor(sleep_coordinator, attribute) + for attribute in SLEEP_SENSORS + ) + remove_listener() + + remove_listener = sleep_coordinator.async_add_listener( + _async_add_sleep_entities + ) + async_add_entities(entities) @@ -695,14 +718,10 @@ class WithingsSleepSensor(WithingsSensor): @property def native_value(self) -> StateType: """Return the state of the entity.""" - assert self.coordinator.data + if not self.coordinator.data: + return None return self.entity_description.value_fn(self.coordinator.data) - @property - 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.""" diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py index 56bee0c30db..8d8207cdf9a 100644 --- a/tests/components/withings/__init__.py +++ b/tests/components/withings/__init__.py @@ -5,7 +5,7 @@ from typing import Any from urllib.parse import urlparse from aiohttp.test_utils import TestClient -from aiowithings import Activity, Goals, MeasurementGroup +from aiowithings import Activity, Goals, MeasurementGroup, SleepSummary from freezegun.api import FrozenDateTimeFactory from homeassistant.components.webhook import async_generate_url @@ -92,3 +92,11 @@ def load_activity_fixture( """Return measurement from fixture.""" activity_json = load_json_array_fixture(fixture) return [Activity.from_api(activity) for activity in activity_json] + + +def load_sleep_fixture( + fixture: str = "withings/sleep_summaries.json", +) -> list[SleepSummary]: + """Return sleep summaries from fixture.""" + sleep_json = load_json_array_fixture("withings/sleep_summaries.json") + return [SleepSummary.from_api(sleep_summary) for sleep_summary in sleep_json] diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 066a9eed031..b040ccd2b58 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, SleepSummary, WithingsClient +from aiowithings import Device, WithingsClient from aiowithings.models import NotificationConfiguration import pytest @@ -20,6 +20,7 @@ from tests.components.withings import ( load_activity_fixture, load_goals_fixture, load_measurements_fixture, + load_sleep_fixture, ) CLIENT_ID = "1234" @@ -138,11 +139,6 @@ def mock_withings(): measurement_groups = load_measurements_fixture() - sleep_json = load_json_array_fixture("withings/sleep_summaries.json") - sleep_summaries = [ - SleepSummary.from_api(sleep_summary) for sleep_summary in sleep_json - ] - notification_json = load_json_array_fixture("withings/notifications.json") notifications = [ NotificationConfiguration.from_api(not_conf) for not_conf in notification_json @@ -155,7 +151,7 @@ def mock_withings(): mock.get_goals.return_value = load_goals_fixture() 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 + mock.get_sleep_summary_since.return_value = load_sleep_fixture() mock.get_activities_since.return_value = activities mock.get_activities_in_period.return_value = activities mock.list_notification_configurations.return_value = notifications diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py index c56b14ae893..c93c4522684 100644 --- a/tests/components/withings/test_binary_sensor.py +++ b/tests/components/withings/test_binary_sensor.py @@ -59,8 +59,8 @@ async def test_binary_sensor( assert hass.states.get(entity_id).state == STATE_UNKNOWN assert ( - "Platform withings does not generate unique IDs. ID withings_12345_in_bed already exists - ignoring binary_sensor.henk_in_bed" - not in caplog.text + "Platform withings does not generate unique IDs. ID withings_12345_in_bed " + "already exists - ignoring binary_sensor.henk_in_bed" not in caplog.text ) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 1a405dd4844..d7add6905e5 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -14,6 +14,7 @@ from . import ( load_activity_fixture, load_goals_fixture, load_measurements_fixture, + load_sleep_fixture, setup_integration, ) @@ -127,7 +128,7 @@ async def test_update_new_measurement_creates_new_sensor( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("sensor.henk_fat_mass") is not None + assert hass.states.get("sensor.henk_fat_mass") async def test_update_new_goals_creates_new_sensor( @@ -143,7 +144,7 @@ async def test_update_new_goals_creates_new_sensor( 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 + assert hass.states.get("sensor.henk_weight_goal") withings.get_goals.return_value = load_goals_fixture() @@ -151,7 +152,7 @@ async def test_update_new_goals_creates_new_sensor( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("sensor.henk_step_goal") is not None + assert hass.states.get("sensor.henk_step_goal") async def test_activity_sensors_unknown_next_day( @@ -164,7 +165,7 @@ async def test_activity_sensors_unknown_next_day( freezer.move_to("2023-10-21") await setup_integration(hass, polling_config_entry, False) - assert hass.states.get("sensor.henk_steps_today") is not None + assert hass.states.get("sensor.henk_steps_today") withings.get_activities_since.return_value = [] @@ -206,7 +207,7 @@ async def test_activity_sensors_created_when_existed( freezer.move_to("2023-10-21") await setup_integration(hass, polling_config_entry, False) - assert hass.states.get("sensor.henk_steps_today") is not None + assert hass.states.get("sensor.henk_steps_today") assert hass.states.get("sensor.henk_steps_today").state != STATE_UNKNOWN withings.get_activities_in_period.return_value = [] @@ -242,25 +243,53 @@ async def test_activity_sensors_created_when_receive_activity_data( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("sensor.henk_steps_today") is not None + assert hass.states.get("sensor.henk_steps_today") @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_no_sleep( +async def test_sleep_sensors_created_when_existed( hass: HomeAssistant, - snapshot: SnapshotAssertion, withings: AsyncMock, polling_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: - """Test no sleep found.""" + """Test sleep sensors will be added if they existed before.""" await setup_integration(hass, polling_config_entry, False) + assert hass.states.get("sensor.henk_deep_sleep") + assert hass.states.get("sensor.henk_deep_sleep").state != STATE_UNKNOWN + withings.get_sleep_summary_since.return_value = [] + + await hass.config_entries.async_reload(polling_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_deep_sleep").state == STATE_UNKNOWN + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sleep_sensors_created_when_receive_sleep_data( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sleep sensors will be added if we receive sleep data.""" + withings.get_sleep_summary_since.return_value = [] + await setup_integration(hass, polling_config_entry, False) + + assert hass.states.get("sensor.henk_deep_sleep") is None + freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("sensor.henk_average_respiratory_rate") - assert state is not None - assert state.state == STATE_UNAVAILABLE + assert hass.states.get("sensor.henk_deep_sleep") is None + + withings.get_sleep_summary_since.return_value = load_sleep_fixture() + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("sensor.henk_deep_sleep")