diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 815962ebce9..6b072030bda 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -1,11 +1,14 @@ """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 @@ -14,21 +17,29 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import ( BaseWithingsSensor, UpdateType, - WithingsAttribute, + WithingsEntityDescription, async_get_data_manager, ) from .const import Measurement + +@dataclass +class WithingsBinarySensorEntityDescription( + BinarySensorEntityDescription, WithingsEntityDescription +): + """Immutable class for describing withings binary sensor data.""" + + BINARY_SENSORS = [ # Webhook measurements. - WithingsAttribute( - Measurement.IN_BED, - NotifyAppli.BED_IN, - "In bed", - "", - "mdi:bed", - True, - UpdateType.WEBHOOK, + WithingsBinarySensorEntityDescription( + key=Measurement.IN_BED.value, + measurement=Measurement.IN_BED, + measure_type=NotifyAppli.BED_IN, + name="In bed", + icon="mdi:bed", + update_type=UpdateType.WEBHOOK, + device_class=BinarySensorDeviceClass.OCCUPANCY, ), ] @@ -52,7 +63,7 @@ async def async_setup_entry( class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): """Implementation of a Withings sensor.""" - _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + entity_description: WithingsBinarySensorEntityDescription @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 1c4ac7f51d0..c1616a062ca 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -34,13 +34,12 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import ( AbstractOAuth2Implementation, OAuth2Session, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt @@ -55,16 +54,6 @@ NOT_AUTHENTICATED_ERROR = re.compile( ) DATA_UPDATED_SIGNAL = "withings_entity_state_updated" -MeasurementData = dict[Measurement, Any] - - -class NotAuthenticatedError(HomeAssistantError): - """Raise when not authenticated with the service.""" - - -class ServiceError(HomeAssistantError): - """Raise when the service has an error.""" - class UpdateType(StrEnum): """Data update type.""" @@ -74,18 +63,19 @@ class UpdateType(StrEnum): @dataclass -class WithingsAttribute: - """Immutable class for describing withings sensor data.""" +class WithingsEntityDescriptionMixin: + """Mixin for describing withings data.""" measurement: Measurement measure_type: NotifyAppli | GetSleepSummaryField | MeasureType - friendly_name: str - unit_of_measurement: str - icon: str | None - enabled_by_default: bool update_type: UpdateType +@dataclass +class WithingsEntityDescription(EntityDescription, WithingsEntityDescriptionMixin): + """Immutable class for describing withings data.""" + + @dataclass class WebhookConfig: """Config for a webhook.""" @@ -95,14 +85,6 @@ class WebhookConfig: enabled: bool -@dataclass -class StateData: - """State data held by data manager for retrieval by entities.""" - - unique_id: str - state: Any - - WITHINGS_MEASURE_TYPE_MAP: dict[ NotifyAppli | GetSleepSummaryField | MeasureType, Measurement ] = { @@ -199,7 +181,7 @@ class WebhookUpdateCoordinator: self._hass = hass self._user_id = user_id self._listeners: list[CALLBACK_TYPE] = [] - self.data: MeasurementData = {} + self.data: dict[Measurement, Any] = {} def async_add_listener(self, listener: CALLBACK_TYPE) -> Callable[[], None]: """Add a listener.""" @@ -555,60 +537,47 @@ class DataManager: ) -def get_attribute_unique_id(attribute: WithingsAttribute, user_id: int) -> str: +def get_attribute_unique_id( + description: WithingsEntityDescription, user_id: int +) -> str: """Get a entity unique id for a user's attribute.""" - return f"withings_{user_id}_{attribute.measurement.value}" + return f"withings_{user_id}_{description.measurement.value}" class BaseWithingsSensor(Entity): """Base class for withings sensors.""" _attr_should_poll = False + entity_description: WithingsEntityDescription - def __init__(self, data_manager: DataManager, attribute: WithingsAttribute) -> None: + def __init__( + self, data_manager: DataManager, description: WithingsEntityDescription + ) -> None: """Initialize the Withings sensor.""" self._data_manager = data_manager - self._attribute = attribute - self._profile = self._data_manager.profile - self._user_id = self._data_manager.user_id - self._name = f"Withings {self._attribute.measurement.value} {self._profile}" - self._unique_id = get_attribute_unique_id(self._attribute, self._user_id) + self.entity_description = description + self._attr_name = ( + f"Withings {description.measurement.value} {data_manager.profile}" + ) + self._attr_unique_id = get_attribute_unique_id( + description, data_manager.user_id + ) self._state_data: Any | None = None - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - @property def available(self) -> bool: """Return True if entity is available.""" - if self._attribute.update_type == UpdateType.POLL: + if self.entity_description.update_type == UpdateType.POLL: return self._data_manager.poll_data_update_coordinator.last_update_success - if self._attribute.update_type == UpdateType.WEBHOOK: + if self.entity_description.update_type == UpdateType.WEBHOOK: return self._data_manager.webhook_config.enabled and ( - self._attribute.measurement + self.entity_description.measurement in self._data_manager.webhook_update_coordinator.data ) return True - @property - def unique_id(self) -> str: - """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._unique_id - - @property - def icon(self) -> str | None: - """Icon to use in the frontend, if any.""" - return self._attribute.icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._attribute.enabled_by_default - @callback def _on_poll_data_updated(self) -> None: self._update_state_data( @@ -621,14 +590,14 @@ class BaseWithingsSensor(Entity): self._data_manager.webhook_update_coordinator.data or {} ) - def _update_state_data(self, data: MeasurementData) -> None: + def _update_state_data(self, data: dict[Measurement, Any]) -> None: """Update the state data.""" - self._state_data = data.get(self._attribute.measurement) + self._state_data = data.get(self.entity_description.measurement) self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Register update dispatcher.""" - if self._attribute.update_type == UpdateType.POLL: + if self.entity_description.update_type == UpdateType.POLL: self.async_on_remove( self._data_manager.poll_data_update_coordinator.async_add_listener( self._on_poll_data_updated @@ -636,7 +605,7 @@ class BaseWithingsSensor(Entity): ) self._on_poll_data_updated() - elif self._attribute.update_type == UpdateType.WEBHOOK: + elif self.entity_description.update_type == UpdateType.WEBHOOK: self.async_on_remove( self._data_manager.webhook_update_coordinator.async_add_listener( self._on_webhook_data_updated diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index a0dfbd68a48..20ad91f16cf 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -58,5 +58,3 @@ UOM_BEATS_PER_MINUTE = "bpm" UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}" UOM_FREQUENCY = "times" UOM_MMHG = "mmhg" -UOM_LENGTH_M = const.LENGTH_METERS -UOM_TEMP_C = const.TEMP_CELSIUS diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index a7c91f354ab..c2cdd89a17f 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,15 +1,24 @@ """Sensors flow for Withings.""" from __future__ import annotations +from dataclasses import dataclass + from withings_api.common import GetSleepSummaryField, MeasureType -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - MASS_KILOGRAMS, PERCENTAGE, - SPEED_METERS_PER_SECOND, - TIME_SECONDS, + UnitOfLength, + UnitOfMass, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import ( BaseWithingsSensor, UpdateType, - WithingsAttribute, + WithingsEntityDescription, async_get_data_manager, ) from .const import ( @@ -25,309 +34,361 @@ from .const import ( UOM_BEATS_PER_MINUTE, UOM_BREATHS_PER_MINUTE, UOM_FREQUENCY, - UOM_LENGTH_M, UOM_MMHG, - UOM_TEMP_C, Measurement, ) + +@dataclass +class WithingsSensorEntityDescription( + SensorEntityDescription, WithingsEntityDescription +): + """Immutable class for describing withings binary sensor data.""" + + SENSORS = [ - WithingsAttribute( - Measurement.WEIGHT_KG, - MeasureType.WEIGHT, - "Weight", - MASS_KILOGRAMS, - "mdi:weight-kilogram", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.WEIGHT_KG.value, + measurement=Measurement.WEIGHT_KG, + measure_type=MeasureType.WEIGHT, + name="Weight", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.FAT_MASS_KG, - MeasureType.FAT_MASS_WEIGHT, - "Fat Mass", - MASS_KILOGRAMS, - "mdi:weight-kilogram", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.FAT_MASS_KG.value, + measurement=Measurement.FAT_MASS_KG, + measure_type=MeasureType.FAT_MASS_WEIGHT, + name="Fat Mass", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.FAT_FREE_MASS_KG, - MeasureType.FAT_FREE_MASS, - "Fat Free Mass", - MASS_KILOGRAMS, - "mdi:weight-kilogram", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.FAT_FREE_MASS_KG.value, + measurement=Measurement.FAT_FREE_MASS_KG, + measure_type=MeasureType.FAT_FREE_MASS, + name="Fat Free Mass", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.MUSCLE_MASS_KG, - MeasureType.MUSCLE_MASS, - "Muscle Mass", - MASS_KILOGRAMS, - "mdi:weight-kilogram", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.MUSCLE_MASS_KG.value, + measurement=Measurement.MUSCLE_MASS_KG, + measure_type=MeasureType.MUSCLE_MASS, + name="Muscle Mass", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.BONE_MASS_KG, - MeasureType.BONE_MASS, - "Bone Mass", - MASS_KILOGRAMS, - "mdi:weight-kilogram", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.BONE_MASS_KG.value, + measurement=Measurement.BONE_MASS_KG, + measure_type=MeasureType.BONE_MASS, + name="Bone Mass", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.HEIGHT_M, - MeasureType.HEIGHT, - "Height", - UOM_LENGTH_M, - "mdi:ruler", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.HEIGHT_M.value, + measurement=Measurement.HEIGHT_M, + measure_type=MeasureType.HEIGHT, + name="Height", + native_unit_of_measurement=UnitOfLength.METERS, + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.TEMP_C, - MeasureType.TEMPERATURE, - "Temperature", - UOM_TEMP_C, - "mdi:thermometer", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.TEMP_C.value, + measurement=Measurement.TEMP_C, + measure_type=MeasureType.TEMPERATURE, + name="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.BODY_TEMP_C, - MeasureType.BODY_TEMPERATURE, - "Body Temperature", - UOM_TEMP_C, - "mdi:thermometer", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.BODY_TEMP_C.value, + measurement=Measurement.BODY_TEMP_C, + measure_type=MeasureType.BODY_TEMPERATURE, + name="Body Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SKIN_TEMP_C, - MeasureType.SKIN_TEMPERATURE, - "Skin Temperature", - UOM_TEMP_C, - "mdi:thermometer", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SKIN_TEMP_C.value, + measurement=Measurement.SKIN_TEMP_C, + measure_type=MeasureType.SKIN_TEMPERATURE, + name="Skin Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.FAT_RATIO_PCT, - MeasureType.FAT_RATIO, - "Fat Ratio", - PERCENTAGE, - None, - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.FAT_RATIO_PCT.value, + measurement=Measurement.FAT_RATIO_PCT, + measure_type=MeasureType.FAT_RATIO, + name="Fat Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.DIASTOLIC_MMHG, - MeasureType.DIASTOLIC_BLOOD_PRESSURE, - "Diastolic Blood Pressure", - UOM_MMHG, - None, - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.DIASTOLIC_MMHG.value, + measurement=Measurement.DIASTOLIC_MMHG, + measure_type=MeasureType.DIASTOLIC_BLOOD_PRESSURE, + name="Diastolic Blood Pressure", + native_unit_of_measurement=UOM_MMHG, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SYSTOLIC_MMGH, - MeasureType.SYSTOLIC_BLOOD_PRESSURE, - "Systolic Blood Pressure", - UOM_MMHG, - None, - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SYSTOLIC_MMGH.value, + measurement=Measurement.SYSTOLIC_MMGH, + measure_type=MeasureType.SYSTOLIC_BLOOD_PRESSURE, + name="Systolic Blood Pressure", + native_unit_of_measurement=UOM_MMHG, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.HEART_PULSE_BPM, - MeasureType.HEART_RATE, - "Heart Pulse", - UOM_BEATS_PER_MINUTE, - "mdi:heart-pulse", - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.HEART_PULSE_BPM.value, + measurement=Measurement.HEART_PULSE_BPM, + measure_type=MeasureType.HEART_RATE, + name="Heart Pulse", + native_unit_of_measurement=UOM_BEATS_PER_MINUTE, + icon="mdi:heart-pulse", + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SPO2_PCT, - MeasureType.SP02, - "SP02", - PERCENTAGE, - None, - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SPO2_PCT.value, + measurement=Measurement.SPO2_PCT, + measure_type=MeasureType.SP02, + name="SP02", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.HYDRATION, - MeasureType.HYDRATION, - "Hydration", - MASS_KILOGRAMS, - "mdi:water", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.HYDRATION.value, + measurement=Measurement.HYDRATION, + measure_type=MeasureType.HYDRATION, + name="Hydration", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + icon="mdi:water", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.PWV, - MeasureType.PULSE_WAVE_VELOCITY, - "Pulse Wave Velocity", - SPEED_METERS_PER_SECOND, - None, - True, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.PWV.value, + measurement=Measurement.PWV, + measure_type=MeasureType.PULSE_WAVE_VELOCITY, + name="Pulse Wave Velocity", + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, + state_class=SensorStateClass.MEASUREMENT, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY, - GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, - "Breathing disturbances intensity", - "", - "", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY.value, + measurement=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY, + measure_type=GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, + name="Breathing disturbances intensity", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_DEEP_DURATION_SECONDS, - GetSleepSummaryField.DEEP_SLEEP_DURATION, - "Deep sleep", - TIME_SECONDS, - "mdi:sleep", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_DEEP_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_DEEP_DURATION_SECONDS, + measure_type=GetSleepSummaryField.DEEP_SLEEP_DURATION, + name="Deep sleep", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_TOSLEEP_DURATION_SECONDS, - GetSleepSummaryField.DURATION_TO_SLEEP, - "Time to sleep", - TIME_SECONDS, - "mdi:sleep", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS, + measure_type=GetSleepSummaryField.DURATION_TO_SLEEP, + name="Time to sleep", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS, - GetSleepSummaryField.DURATION_TO_WAKEUP, - "Time to wakeup", - TIME_SECONDS, - "mdi:sleep-off", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS, + measure_type=GetSleepSummaryField.DURATION_TO_WAKEUP, + name="Time to wakeup", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep-off", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_HEART_RATE_AVERAGE, - GetSleepSummaryField.HR_AVERAGE, - "Average heart rate", - UOM_BEATS_PER_MINUTE, - "mdi:heart-pulse", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_HEART_RATE_AVERAGE.value, + measurement=Measurement.SLEEP_HEART_RATE_AVERAGE, + measure_type=GetSleepSummaryField.HR_AVERAGE, + name="Average heart rate", + native_unit_of_measurement=UOM_BEATS_PER_MINUTE, + icon="mdi:heart-pulse", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_HEART_RATE_MAX, - GetSleepSummaryField.HR_MAX, - "Maximum heart rate", - UOM_BEATS_PER_MINUTE, - "mdi:heart-pulse", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_HEART_RATE_MAX.value, + measurement=Measurement.SLEEP_HEART_RATE_MAX, + measure_type=GetSleepSummaryField.HR_MAX, + name="Maximum heart rate", + native_unit_of_measurement=UOM_BEATS_PER_MINUTE, + icon="mdi:heart-pulse", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_HEART_RATE_MIN, - GetSleepSummaryField.HR_MIN, - "Minimum heart rate", - UOM_BEATS_PER_MINUTE, - "mdi:heart-pulse", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_HEART_RATE_MIN.value, + measurement=Measurement.SLEEP_HEART_RATE_MIN, + measure_type=GetSleepSummaryField.HR_MIN, + name="Minimum heart rate", + native_unit_of_measurement=UOM_BEATS_PER_MINUTE, + icon="mdi:heart-pulse", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_LIGHT_DURATION_SECONDS, - GetSleepSummaryField.LIGHT_SLEEP_DURATION, - "Light sleep", - TIME_SECONDS, - "mdi:sleep", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_LIGHT_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_LIGHT_DURATION_SECONDS, + measure_type=GetSleepSummaryField.LIGHT_SLEEP_DURATION, + name="Light sleep", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_REM_DURATION_SECONDS, - GetSleepSummaryField.REM_SLEEP_DURATION, - "REM sleep", - TIME_SECONDS, - "mdi:sleep", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_REM_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_REM_DURATION_SECONDS, + measure_type=GetSleepSummaryField.REM_SLEEP_DURATION, + name="REM sleep", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE, - GetSleepSummaryField.RR_AVERAGE, - "Average respiratory rate", - UOM_BREATHS_PER_MINUTE, - None, - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE.value, + measurement=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE, + measure_type=GetSleepSummaryField.RR_AVERAGE, + name="Average respiratory rate", + native_unit_of_measurement=UOM_BREATHS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_RESPIRATORY_RATE_MAX, - GetSleepSummaryField.RR_MAX, - "Maximum respiratory rate", - UOM_BREATHS_PER_MINUTE, - None, - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_RESPIRATORY_RATE_MAX.value, + measurement=Measurement.SLEEP_RESPIRATORY_RATE_MAX, + measure_type=GetSleepSummaryField.RR_MAX, + name="Maximum respiratory rate", + native_unit_of_measurement=UOM_BREATHS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_RESPIRATORY_RATE_MIN, - GetSleepSummaryField.RR_MIN, - "Minimum respiratory rate", - UOM_BREATHS_PER_MINUTE, - None, - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_RESPIRATORY_RATE_MIN.value, + measurement=Measurement.SLEEP_RESPIRATORY_RATE_MIN, + measure_type=GetSleepSummaryField.RR_MIN, + name="Minimum respiratory rate", + native_unit_of_measurement=UOM_BREATHS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_SCORE, - GetSleepSummaryField.SLEEP_SCORE, - "Sleep score", - SCORE_POINTS, - "mdi:medal", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_SCORE.value, + measurement=Measurement.SLEEP_SCORE, + measure_type=GetSleepSummaryField.SLEEP_SCORE, + name="Sleep score", + native_unit_of_measurement=SCORE_POINTS, + icon="mdi:medal", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_SNORING, - GetSleepSummaryField.SNORING, - "Snoring", - "", - None, - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_SNORING.value, + measurement=Measurement.SLEEP_SNORING, + measure_type=GetSleepSummaryField.SNORING, + name="Snoring", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_SNORING_EPISODE_COUNT, - GetSleepSummaryField.SNORING_EPISODE_COUNT, - "Snoring episode count", - "", - None, - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_SNORING_EPISODE_COUNT.value, + measurement=Measurement.SLEEP_SNORING_EPISODE_COUNT, + measure_type=GetSleepSummaryField.SNORING_EPISODE_COUNT, + name="Snoring episode count", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_WAKEUP_COUNT, - GetSleepSummaryField.WAKEUP_COUNT, - "Wakeup count", - UOM_FREQUENCY, - "mdi:sleep-off", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_WAKEUP_COUNT.value, + measurement=Measurement.SLEEP_WAKEUP_COUNT, + measure_type=GetSleepSummaryField.WAKEUP_COUNT, + name="Wakeup count", + native_unit_of_measurement=UOM_FREQUENCY, + icon="mdi:sleep-off", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), - WithingsAttribute( - Measurement.SLEEP_WAKEUP_DURATION_SECONDS, - GetSleepSummaryField.WAKEUP_DURATION, - "Wakeup time", - TIME_SECONDS, - "mdi:sleep-off", - False, - UpdateType.POLL, + WithingsSensorEntityDescription( + key=Measurement.SLEEP_WAKEUP_DURATION_SECONDS.value, + measurement=Measurement.SLEEP_WAKEUP_DURATION_SECONDS, + measure_type=GetSleepSummaryField.WAKEUP_DURATION, + name="Wakeup time", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:sleep-off", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + update_type=UpdateType.POLL, ), ] @@ -348,17 +409,9 @@ async def async_setup_entry( class WithingsHealthSensor(BaseWithingsSensor, SensorEntity): """Implementation of a Withings sensor.""" + entity_description: WithingsSensorEntityDescription + @property def native_value(self) -> None | str | int | float: """Return the state of the entity.""" return self._state_data - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return self._attribute.unit_of_measurement - - @property - def state_class(self) -> str: - """Return the state_class of this entity, if any.""" - return SensorStateClass.MEASUREMENT diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index c62460ebd16..96a35d10c40 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -24,7 +24,7 @@ from homeassistant.components.withings import async_unload_entry from homeassistant.components.withings.common import ( ConfigEntryWithingsApi, DataManager, - WithingsAttribute, + WithingsEntityDescription, get_all_data_managers, get_attribute_unique_id, ) @@ -325,10 +325,13 @@ def get_data_manager_by_user_id( async def async_get_entity_id( - hass: HomeAssistant, attribute: WithingsAttribute, user_id: int, platform: str + hass: HomeAssistant, + description: WithingsEntityDescription, + user_id: int, + platform: str, ) -> str | None: """Get an entity id for a user's attribute.""" entity_registry = er.async_get(hass) - unique_id = get_attribute_unique_id(attribute, user_id) + unique_id = get_attribute_unique_id(description, user_id) return entity_registry.async_get_entity_id(platform, const.DOMAIN, unique_id) diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py index 76deeefd3d4..e3205ffa2db 100644 --- a/tests/components/withings/test_binary_sensor.py +++ b/tests/components/withings/test_binary_sensor.py @@ -3,7 +3,7 @@ from withings_api.common import NotifyAppli from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.withings.binary_sensor import BINARY_SENSORS -from homeassistant.components.withings.common import WithingsAttribute +from homeassistant.components.withings.common import WithingsEntityDescription from homeassistant.components.withings.const import Measurement from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from .common import ComponentFactory, async_get_entity_id, new_profile_config -WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsAttribute] = { +WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsEntityDescription] = { attr.measurement: attr for attr in BINARY_SENSORS } diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 1abca0e1ea7..a22ba36e59a 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -18,7 +18,7 @@ from withings_api.common import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.withings.common import WithingsAttribute +from homeassistant.components.withings.common import WithingsEntityDescription from homeassistant.components.withings.const import Measurement from homeassistant.components.withings.sensor import SENSORS from homeassistant.core import HomeAssistant, State @@ -28,7 +28,7 @@ from homeassistant.util import dt as dt_util from .common import ComponentFactory, async_get_entity_id, new_profile_config -WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsAttribute] = { +WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsEntityDescription] = { attr.measurement: attr for attr in SENSORS } @@ -293,14 +293,14 @@ def async_assert_state_equals( entity_id: str, state_obj: State, expected: Any, - attribute: WithingsAttribute, + 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 {attribute.measurement}, {entity_id}" + f"for measure {description.measurement}, {entity_id}" ) @@ -342,7 +342,7 @@ async def test_sensor_default_enabled_entities( ) state_obj = hass.states.get(entity_id) - if attribute.enabled_by_default: + if attribute.entity_registry_enabled_default: async_assert_state_equals(entity_id, state_obj, expected, attribute) else: assert state_obj is None