From 3e4edc8eddf96734eb5edeacdff30acbb7ee25f0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 12 Oct 2023 13:42:00 +0200 Subject: [PATCH] Move Withings entity descriptions to platforms (#101820) --- .../components/withings/binary_sensor.py | 41 ++----- homeassistant/components/withings/entity.py | 26 +--- homeassistant/components/withings/sensor.py | 23 +++- tests/components/withings/test_sensor.py | 114 ++---------------- 4 files changed, 40 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 309ef45623f..629114247ce 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -1,42 +1,17 @@ """Sensors flow for Withings.""" from __future__ import annotations -from dataclasses import dataclass - -from withings_api.common import NotifyAppli - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, - BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, Measurement +from .const import DOMAIN from .coordinator import WithingsDataUpdateCoordinator -from .entity import WithingsEntity, WithingsEntityDescription - - -@dataclass -class WithingsBinarySensorEntityDescription( - BinarySensorEntityDescription, WithingsEntityDescription -): - """Immutable class for describing withings binary sensor data.""" - - -BINARY_SENSORS = [ - # Webhook measurements. - WithingsBinarySensorEntityDescription( - key=Measurement.IN_BED.value, - measurement=Measurement.IN_BED, - measure_type=NotifyAppli.BED_IN, - translation_key="in_bed", - icon="mdi:bed", - device_class=BinarySensorDeviceClass.OCCUPANCY, - ), -] +from .entity import WithingsEntity async def async_setup_entry( @@ -47,9 +22,7 @@ async def async_setup_entry( """Set up the sensor config entry.""" coordinator: WithingsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [ - WithingsBinarySensor(coordinator, attribute) for attribute in BINARY_SENSORS - ] + entities = [WithingsBinarySensor(coordinator)] async_add_entities(entities) @@ -57,7 +30,13 @@ async def async_setup_entry( class WithingsBinarySensor(WithingsEntity, BinarySensorEntity): """Implementation of a Withings sensor.""" - entity_description: WithingsBinarySensorEntityDescription + _attr_icon = "mdi:bed" + _attr_translation_key = "in_bed" + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + + def __init__(self, coordinator: WithingsDataUpdateCoordinator) -> None: + """Initialize binary sensor.""" + super().__init__(coordinator, "in_bed") @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/withings/entity.py b/homeassistant/components/withings/entity.py index 8005f97bfaa..8d2c815b340 100644 --- a/homeassistant/components/withings/entity.py +++ b/homeassistant/components/withings/entity.py @@ -1,46 +1,26 @@ """Base entity for Withings.""" from __future__ import annotations -from dataclasses import dataclass - -from withings_api.common import GetSleepSummaryField, MeasureType, NotifyAppli - from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, Measurement +from .const import DOMAIN from .coordinator import WithingsDataUpdateCoordinator -@dataclass -class WithingsEntityDescriptionMixin: - """Mixin for describing withings data.""" - - measurement: Measurement - measure_type: NotifyAppli | GetSleepSummaryField | MeasureType - - -@dataclass -class WithingsEntityDescription(EntityDescription, WithingsEntityDescriptionMixin): - """Immutable class for describing withings data.""" - - class WithingsEntity(CoordinatorEntity[WithingsDataUpdateCoordinator]): """Base class for withings entities.""" - entity_description: WithingsEntityDescription _attr_has_entity_name = True def __init__( self, coordinator: WithingsDataUpdateCoordinator, - description: WithingsEntityDescription, + key: str, ) -> None: """Initialize the Withings entity.""" super().__init__(coordinator) - self.entity_description = description - self._attr_unique_id = f"withings_{coordinator.config_entry.unique_id}_{description.measurement.value}" + self._attr_unique_id = f"withings_{coordinator.config_entry.unique_id}_{key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, str(coordinator.config_entry.unique_id))}, manufacturer="Withings", diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 77a706dc55d..bb615dfb7ca 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -33,14 +33,22 @@ from .const import ( Measurement, ) from .coordinator import WithingsDataUpdateCoordinator -from .entity import WithingsEntity, WithingsEntityDescription +from .entity import WithingsEntity + + +@dataclass +class WithingsEntityDescriptionMixin: + """Mixin for describing withings data.""" + + measurement: Measurement + measure_type: GetSleepSummaryField | MeasureType @dataclass class WithingsSensorEntityDescription( - SensorEntityDescription, WithingsEntityDescription + SensorEntityDescription, WithingsEntityDescriptionMixin ): - """Immutable class for describing withings binary sensor data.""" + """Immutable class for describing withings data.""" SENSORS = [ @@ -371,6 +379,15 @@ class WithingsSensor(WithingsEntity, SensorEntity): entity_description: WithingsSensorEntityDescription + def __init__( + self, + coordinator: WithingsDataUpdateCoordinator, + entity_description: WithingsSensorEntityDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(coordinator, entity_description.key) + self.entity_description = entity_description + @property def native_value(self) -> None | str | int | float: """Return the state of the entity.""" diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 44ae10b6a94..febf0a1a5d9 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -1,137 +1,37 @@ """Tests for the Withings component.""" from datetime import timedelta -from typing import Any from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -from withings_api.common import NotifyAppli from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.withings.const import DOMAIN, Measurement -from homeassistant.components.withings.entity import WithingsEntityDescription +from homeassistant.components.withings.const import DOMAIN from homeassistant.components.withings.sensor import SENSORS from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_registry import EntityRegistry -from . import call_webhook, prepare_webhook_setup, setup_integration -from .conftest import USER_ID, WEBHOOK_ID +from . import setup_integration +from .conftest import USER_ID from tests.common import MockConfigEntry, async_fire_time_changed -from tests.typing import ClientSessionGenerator - -WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsEntityDescription] = { - attr.measurement: attr for attr in SENSORS -} - - -EXPECTED_DATA = ( - (Measurement.WEIGHT_KG, 70.0), - (Measurement.FAT_MASS_KG, 5.0), - (Measurement.FAT_FREE_MASS_KG, 60.0), - (Measurement.MUSCLE_MASS_KG, 50.0), - (Measurement.BONE_MASS_KG, 10.0), - (Measurement.HEIGHT_M, 2.0), - (Measurement.FAT_RATIO_PCT, 0.07), - (Measurement.DIASTOLIC_MMHG, 70.0), - (Measurement.SYSTOLIC_MMGH, 100.0), - (Measurement.HEART_PULSE_BPM, 60.0), - (Measurement.SPO2_PCT, 0.95), - (Measurement.HYDRATION, 0.95), - (Measurement.PWV, 100.0), - (Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY, 160.0), - (Measurement.SLEEP_DEEP_DURATION_SECONDS, 322), - (Measurement.SLEEP_HEART_RATE_AVERAGE, 164.0), - (Measurement.SLEEP_HEART_RATE_MAX, 165.0), - (Measurement.SLEEP_HEART_RATE_MIN, 166.0), - (Measurement.SLEEP_LIGHT_DURATION_SECONDS, 334), - (Measurement.SLEEP_REM_DURATION_SECONDS, 336), - (Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE, 169.0), - (Measurement.SLEEP_RESPIRATORY_RATE_MAX, 170.0), - (Measurement.SLEEP_RESPIRATORY_RATE_MIN, 171.0), - (Measurement.SLEEP_SCORE, 222), - (Measurement.SLEEP_SNORING, 173.0), - (Measurement.SLEEP_SNORING_EPISODE_COUNT, 348), - (Measurement.SLEEP_TOSLEEP_DURATION_SECONDS, 162.0), - (Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS, 163.0), - (Measurement.SLEEP_WAKEUP_COUNT, 350), - (Measurement.SLEEP_WAKEUP_DURATION_SECONDS, 176.0), -) async def async_get_entity_id( hass: HomeAssistant, - description: WithingsEntityDescription, + key: str, user_id: int, platform: str, ) -> str | None: """Get an entity id for a user's attribute.""" entity_registry = er.async_get(hass) - unique_id = f"withings_{user_id}_{description.measurement.value}" + unique_id = f"withings_{user_id}_{key}" return entity_registry.async_get_entity_id(platform, DOMAIN, unique_id) -def async_assert_state_equals( - entity_id: str, - state_obj: State, - expected: Any, - description: WithingsEntityDescription, -) -> None: - """Assert at given state matches what is expected.""" - assert state_obj, f"Expected entity {entity_id} to exist but it did not" - - assert state_obj.state == str(expected), ( - f"Expected {expected} but was {state_obj.state} " - f"for measure {description.measurement}, {entity_id}" - ) - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_sensor_default_enabled_entities( - hass: HomeAssistant, - withings: AsyncMock, - webhook_config_entry: MockConfigEntry, - hass_client_no_auth: ClientSessionGenerator, - freezer: FrozenDateTimeFactory, -) -> None: - """Test entities enabled by default.""" - await setup_integration(hass, webhook_config_entry) - await prepare_webhook_setup(hass, freezer) - entity_registry: EntityRegistry = er.async_get(hass) - - client = await hass_client_no_auth() - # Assert entities should exist. - for attribute in SENSORS: - entity_id = await async_get_entity_id(hass, attribute, USER_ID, SENSOR_DOMAIN) - assert entity_id - assert entity_registry.async_is_registered(entity_id) - resp = await call_webhook( - hass, - WEBHOOK_ID, - {"userid": USER_ID, "appli": NotifyAppli.SLEEP}, - client, - ) - assert resp.message_code == 0 - resp = await call_webhook( - hass, - WEBHOOK_ID, - {"userid": USER_ID, "appli": NotifyAppli.WEIGHT}, - client, - ) - assert resp.message_code == 0 - - for measurement, expected in EXPECTED_DATA: - attribute = WITHINGS_MEASUREMENTS_MAP[measurement] - entity_id = await async_get_entity_id(hass, attribute, USER_ID, SENSOR_DOMAIN) - state_obj = hass.states.get(entity_id) - - async_assert_state_equals(entity_id, state_obj, expected, attribute) - - @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_entities( hass: HomeAssistant, @@ -143,7 +43,7 @@ async def test_all_entities( await setup_integration(hass, polling_config_entry) for sensor in SENSORS: - entity_id = await async_get_entity_id(hass, sensor, USER_ID, SENSOR_DOMAIN) + entity_id = await async_get_entity_id(hass, sensor.key, USER_ID, SENSOR_DOMAIN) assert hass.states.get(entity_id) == snapshot