Only add Withings sleep sensors when we have data (#102578)

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Joost Lekkerkerker 2023-10-23 12:59:13 +02:00 committed by GitHub
parent d5af6c595d
commit 42c062de68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 31 deletions

View File

@ -263,7 +263,6 @@ SLEEP_SENSORS = [
icon="mdi:sleep", icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
), ),
WithingsSleepSensorEntityDescription( WithingsSleepSensorEntityDescription(
key="sleep_tosleep_duration_seconds", key="sleep_tosleep_duration_seconds",
@ -645,9 +644,33 @@ async def async_setup_entry(
sleep_coordinator = withings_data.sleep_coordinator sleep_coordinator = withings_data.sleep_coordinator
entities.extend( sleep_entities_setup_before = ent_reg.async_get_entity_id(
WithingsSleepSensor(sleep_coordinator, attribute) for attribute in SLEEP_SENSORS 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) async_add_entities(entities)
@ -695,14 +718,10 @@ class WithingsSleepSensor(WithingsSensor):
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the entity.""" """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) 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): class WithingsGoalsSensor(WithingsSensor):
"""Implementation of a Withings goals sensor.""" """Implementation of a Withings goals sensor."""

View File

@ -5,7 +5,7 @@ from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from aiohttp.test_utils import TestClient 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 freezegun.api import FrozenDateTimeFactory
from homeassistant.components.webhook import async_generate_url from homeassistant.components.webhook import async_generate_url
@ -92,3 +92,11 @@ def load_activity_fixture(
"""Return measurement from fixture.""" """Return measurement from fixture."""
activity_json = load_json_array_fixture(fixture) activity_json = load_json_array_fixture(fixture)
return [Activity.from_api(activity) for activity in activity_json] 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]

View File

@ -3,7 +3,7 @@ from datetime import timedelta
import time import time
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiowithings import Device, SleepSummary, WithingsClient from aiowithings import Device, WithingsClient
from aiowithings.models import NotificationConfiguration from aiowithings.models import NotificationConfiguration
import pytest import pytest
@ -20,6 +20,7 @@ from tests.components.withings import (
load_activity_fixture, load_activity_fixture,
load_goals_fixture, load_goals_fixture,
load_measurements_fixture, load_measurements_fixture,
load_sleep_fixture,
) )
CLIENT_ID = "1234" CLIENT_ID = "1234"
@ -138,11 +139,6 @@ def mock_withings():
measurement_groups = load_measurements_fixture() 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") notification_json = load_json_array_fixture("withings/notifications.json")
notifications = [ notifications = [
NotificationConfiguration.from_api(not_conf) for not_conf in notification_json 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_goals.return_value = load_goals_fixture()
mock.get_measurement_in_period.return_value = measurement_groups mock.get_measurement_in_period.return_value = measurement_groups
mock.get_measurement_since.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_since.return_value = activities
mock.get_activities_in_period.return_value = activities mock.get_activities_in_period.return_value = activities
mock.list_notification_configurations.return_value = notifications mock.list_notification_configurations.return_value = notifications

View File

@ -59,8 +59,8 @@ async def test_binary_sensor(
assert hass.states.get(entity_id).state == STATE_UNKNOWN assert hass.states.get(entity_id).state == STATE_UNKNOWN
assert ( assert (
"Platform withings does not generate unique IDs. ID withings_12345_in_bed already exists - ignoring binary_sensor.henk_in_bed" "Platform withings does not generate unique IDs. ID withings_12345_in_bed "
not in caplog.text "already exists - ignoring binary_sensor.henk_in_bed" not in caplog.text
) )

View File

@ -14,6 +14,7 @@ from . import (
load_activity_fixture, load_activity_fixture,
load_goals_fixture, load_goals_fixture,
load_measurements_fixture, load_measurements_fixture,
load_sleep_fixture,
setup_integration, setup_integration,
) )
@ -127,7 +128,7 @@ async def test_update_new_measurement_creates_new_sensor(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() 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( 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) await setup_integration(hass, polling_config_entry, False)
assert hass.states.get("sensor.henk_step_goal") is None 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() 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) async_fire_time_changed(hass)
await hass.async_block_till_done() 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( 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") freezer.move_to("2023-10-21")
await setup_integration(hass, polling_config_entry, False) 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 = [] withings.get_activities_since.return_value = []
@ -206,7 +207,7 @@ async def test_activity_sensors_created_when_existed(
freezer.move_to("2023-10-21") freezer.move_to("2023-10-21")
await setup_integration(hass, polling_config_entry, False) 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 assert hass.states.get("sensor.henk_steps_today").state != STATE_UNKNOWN
withings.get_activities_in_period.return_value = [] 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) async_fire_time_changed(hass)
await hass.async_block_till_done() 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") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_no_sleep( async def test_sleep_sensors_created_when_existed(
hass: HomeAssistant, hass: HomeAssistant,
snapshot: SnapshotAssertion,
withings: AsyncMock, withings: AsyncMock,
polling_config_entry: MockConfigEntry, polling_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test no sleep found.""" """Test sleep sensors will be added if they existed before."""
await setup_integration(hass, polling_config_entry, False) 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 = [] 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)) freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.henk_average_respiratory_rate") assert hass.states.get("sensor.henk_deep_sleep") is None
assert state is not None
assert state.state == STATE_UNAVAILABLE 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")