Add device class to withings (#84103)

pull/84125/head
epenet 2022-12-16 20:04:07 +01:00 committed by GitHub
parent b41d0be952
commit 1af72e3671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 399 additions and 365 deletions

View File

@ -1,11 +1,14 @@
"""Sensors flow for Withings.""" """Sensors flow for Withings."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from withings_api.common import NotifyAppli from withings_api.common import NotifyAppli
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -14,21 +17,29 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import ( from .common import (
BaseWithingsSensor, BaseWithingsSensor,
UpdateType, UpdateType,
WithingsAttribute, WithingsEntityDescription,
async_get_data_manager, async_get_data_manager,
) )
from .const import Measurement from .const import Measurement
@dataclass
class WithingsBinarySensorEntityDescription(
BinarySensorEntityDescription, WithingsEntityDescription
):
"""Immutable class for describing withings binary sensor data."""
BINARY_SENSORS = [ BINARY_SENSORS = [
# Webhook measurements. # Webhook measurements.
WithingsAttribute( WithingsBinarySensorEntityDescription(
Measurement.IN_BED, key=Measurement.IN_BED.value,
NotifyAppli.BED_IN, measurement=Measurement.IN_BED,
"In bed", measure_type=NotifyAppli.BED_IN,
"", name="In bed",
"mdi:bed", icon="mdi:bed",
True, update_type=UpdateType.WEBHOOK,
UpdateType.WEBHOOK, device_class=BinarySensorDeviceClass.OCCUPANCY,
), ),
] ]
@ -52,7 +63,7 @@ async def async_setup_entry(
class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity):
"""Implementation of a Withings sensor.""" """Implementation of a Withings sensor."""
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY entity_description: WithingsBinarySensorEntityDescription
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:

View File

@ -34,13 +34,12 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.config_entry_oauth2_flow import (
AbstractOAuth2Implementation, AbstractOAuth2Implementation,
OAuth2Session, OAuth2Session,
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt from homeassistant.util import dt
@ -55,16 +54,6 @@ NOT_AUTHENTICATED_ERROR = re.compile(
) )
DATA_UPDATED_SIGNAL = "withings_entity_state_updated" 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): class UpdateType(StrEnum):
"""Data update type.""" """Data update type."""
@ -74,18 +63,19 @@ class UpdateType(StrEnum):
@dataclass @dataclass
class WithingsAttribute: class WithingsEntityDescriptionMixin:
"""Immutable class for describing withings sensor data.""" """Mixin for describing withings data."""
measurement: Measurement measurement: Measurement
measure_type: NotifyAppli | GetSleepSummaryField | MeasureType measure_type: NotifyAppli | GetSleepSummaryField | MeasureType
friendly_name: str
unit_of_measurement: str
icon: str | None
enabled_by_default: bool
update_type: UpdateType update_type: UpdateType
@dataclass
class WithingsEntityDescription(EntityDescription, WithingsEntityDescriptionMixin):
"""Immutable class for describing withings data."""
@dataclass @dataclass
class WebhookConfig: class WebhookConfig:
"""Config for a webhook.""" """Config for a webhook."""
@ -95,14 +85,6 @@ class WebhookConfig:
enabled: bool 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[ WITHINGS_MEASURE_TYPE_MAP: dict[
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
] = { ] = {
@ -199,7 +181,7 @@ class WebhookUpdateCoordinator:
self._hass = hass self._hass = hass
self._user_id = user_id self._user_id = user_id
self._listeners: list[CALLBACK_TYPE] = [] self._listeners: list[CALLBACK_TYPE] = []
self.data: MeasurementData = {} self.data: dict[Measurement, Any] = {}
def async_add_listener(self, listener: CALLBACK_TYPE) -> Callable[[], None]: def async_add_listener(self, listener: CALLBACK_TYPE) -> Callable[[], None]:
"""Add a listener.""" """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.""" """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): class BaseWithingsSensor(Entity):
"""Base class for withings sensors.""" """Base class for withings sensors."""
_attr_should_poll = False _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.""" """Initialize the Withings sensor."""
self._data_manager = data_manager self._data_manager = data_manager
self._attribute = attribute self.entity_description = description
self._profile = self._data_manager.profile self._attr_name = (
self._user_id = self._data_manager.user_id f"Withings {description.measurement.value} {data_manager.profile}"
self._name = f"Withings {self._attribute.measurement.value} {self._profile}" )
self._unique_id = get_attribute_unique_id(self._attribute, self._user_id) self._attr_unique_id = get_attribute_unique_id(
description, data_manager.user_id
)
self._state_data: Any | None = None self._state_data: Any | None = None
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if entity is available.""" """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 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 ( return self._data_manager.webhook_config.enabled and (
self._attribute.measurement self.entity_description.measurement
in self._data_manager.webhook_update_coordinator.data in self._data_manager.webhook_update_coordinator.data
) )
return True 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 @callback
def _on_poll_data_updated(self) -> None: def _on_poll_data_updated(self) -> None:
self._update_state_data( self._update_state_data(
@ -621,14 +590,14 @@ class BaseWithingsSensor(Entity):
self._data_manager.webhook_update_coordinator.data or {} 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.""" """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() self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register update dispatcher.""" """Register update dispatcher."""
if self._attribute.update_type == UpdateType.POLL: if self.entity_description.update_type == UpdateType.POLL:
self.async_on_remove( self.async_on_remove(
self._data_manager.poll_data_update_coordinator.async_add_listener( self._data_manager.poll_data_update_coordinator.async_add_listener(
self._on_poll_data_updated self._on_poll_data_updated
@ -636,7 +605,7 @@ class BaseWithingsSensor(Entity):
) )
self._on_poll_data_updated() 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.async_on_remove(
self._data_manager.webhook_update_coordinator.async_add_listener( self._data_manager.webhook_update_coordinator.async_add_listener(
self._on_webhook_data_updated self._on_webhook_data_updated

View File

@ -58,5 +58,3 @@ UOM_BEATS_PER_MINUTE = "bpm"
UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}" UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}"
UOM_FREQUENCY = "times" UOM_FREQUENCY = "times"
UOM_MMHG = "mmhg" UOM_MMHG = "mmhg"
UOM_LENGTH_M = const.LENGTH_METERS
UOM_TEMP_C = const.TEMP_CELSIUS

View File

@ -1,15 +1,24 @@
"""Sensors flow for Withings.""" """Sensors flow for Withings."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from withings_api.common import GetSleepSummaryField, MeasureType 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
MASS_KILOGRAMS,
PERCENTAGE, PERCENTAGE,
SPEED_METERS_PER_SECOND, UnitOfLength,
TIME_SECONDS, UnitOfMass,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -17,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import ( from .common import (
BaseWithingsSensor, BaseWithingsSensor,
UpdateType, UpdateType,
WithingsAttribute, WithingsEntityDescription,
async_get_data_manager, async_get_data_manager,
) )
from .const import ( from .const import (
@ -25,309 +34,361 @@ from .const import (
UOM_BEATS_PER_MINUTE, UOM_BEATS_PER_MINUTE,
UOM_BREATHS_PER_MINUTE, UOM_BREATHS_PER_MINUTE,
UOM_FREQUENCY, UOM_FREQUENCY,
UOM_LENGTH_M,
UOM_MMHG, UOM_MMHG,
UOM_TEMP_C,
Measurement, Measurement,
) )
@dataclass
class WithingsSensorEntityDescription(
SensorEntityDescription, WithingsEntityDescription
):
"""Immutable class for describing withings binary sensor data."""
SENSORS = [ SENSORS = [
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.WEIGHT_KG, key=Measurement.WEIGHT_KG.value,
MeasureType.WEIGHT, measurement=Measurement.WEIGHT_KG,
"Weight", measure_type=MeasureType.WEIGHT,
MASS_KILOGRAMS, name="Weight",
"mdi:weight-kilogram", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
True, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.FAT_MASS_KG, key=Measurement.FAT_MASS_KG.value,
MeasureType.FAT_MASS_WEIGHT, measurement=Measurement.FAT_MASS_KG,
"Fat Mass", measure_type=MeasureType.FAT_MASS_WEIGHT,
MASS_KILOGRAMS, name="Fat Mass",
"mdi:weight-kilogram", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
True, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.FAT_FREE_MASS_KG, key=Measurement.FAT_FREE_MASS_KG.value,
MeasureType.FAT_FREE_MASS, measurement=Measurement.FAT_FREE_MASS_KG,
"Fat Free Mass", measure_type=MeasureType.FAT_FREE_MASS,
MASS_KILOGRAMS, name="Fat Free Mass",
"mdi:weight-kilogram", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
True, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.MUSCLE_MASS_KG, key=Measurement.MUSCLE_MASS_KG.value,
MeasureType.MUSCLE_MASS, measurement=Measurement.MUSCLE_MASS_KG,
"Muscle Mass", measure_type=MeasureType.MUSCLE_MASS,
MASS_KILOGRAMS, name="Muscle Mass",
"mdi:weight-kilogram", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
True, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.BONE_MASS_KG, key=Measurement.BONE_MASS_KG.value,
MeasureType.BONE_MASS, measurement=Measurement.BONE_MASS_KG,
"Bone Mass", measure_type=MeasureType.BONE_MASS,
MASS_KILOGRAMS, name="Bone Mass",
"mdi:weight-kilogram", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
True, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.HEIGHT_M, key=Measurement.HEIGHT_M.value,
MeasureType.HEIGHT, measurement=Measurement.HEIGHT_M,
"Height", measure_type=MeasureType.HEIGHT,
UOM_LENGTH_M, name="Height",
"mdi:ruler", native_unit_of_measurement=UnitOfLength.METERS,
False, device_class=SensorDeviceClass.DISTANCE,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.TEMP_C, key=Measurement.TEMP_C.value,
MeasureType.TEMPERATURE, measurement=Measurement.TEMP_C,
"Temperature", measure_type=MeasureType.TEMPERATURE,
UOM_TEMP_C, name="Temperature",
"mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS,
True, device_class=SensorDeviceClass.TEMPERATURE,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.BODY_TEMP_C, key=Measurement.BODY_TEMP_C.value,
MeasureType.BODY_TEMPERATURE, measurement=Measurement.BODY_TEMP_C,
"Body Temperature", measure_type=MeasureType.BODY_TEMPERATURE,
UOM_TEMP_C, name="Body Temperature",
"mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS,
True, device_class=SensorDeviceClass.TEMPERATURE,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SKIN_TEMP_C, key=Measurement.SKIN_TEMP_C.value,
MeasureType.SKIN_TEMPERATURE, measurement=Measurement.SKIN_TEMP_C,
"Skin Temperature", measure_type=MeasureType.SKIN_TEMPERATURE,
UOM_TEMP_C, name="Skin Temperature",
"mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS,
True, device_class=SensorDeviceClass.TEMPERATURE,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.FAT_RATIO_PCT, key=Measurement.FAT_RATIO_PCT.value,
MeasureType.FAT_RATIO, measurement=Measurement.FAT_RATIO_PCT,
"Fat Ratio", measure_type=MeasureType.FAT_RATIO,
PERCENTAGE, name="Fat Ratio",
None, native_unit_of_measurement=PERCENTAGE,
True, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.DIASTOLIC_MMHG, key=Measurement.DIASTOLIC_MMHG.value,
MeasureType.DIASTOLIC_BLOOD_PRESSURE, measurement=Measurement.DIASTOLIC_MMHG,
"Diastolic Blood Pressure", measure_type=MeasureType.DIASTOLIC_BLOOD_PRESSURE,
UOM_MMHG, name="Diastolic Blood Pressure",
None, native_unit_of_measurement=UOM_MMHG,
True, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SYSTOLIC_MMGH, key=Measurement.SYSTOLIC_MMGH.value,
MeasureType.SYSTOLIC_BLOOD_PRESSURE, measurement=Measurement.SYSTOLIC_MMGH,
"Systolic Blood Pressure", measure_type=MeasureType.SYSTOLIC_BLOOD_PRESSURE,
UOM_MMHG, name="Systolic Blood Pressure",
None, native_unit_of_measurement=UOM_MMHG,
True, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.HEART_PULSE_BPM, key=Measurement.HEART_PULSE_BPM.value,
MeasureType.HEART_RATE, measurement=Measurement.HEART_PULSE_BPM,
"Heart Pulse", measure_type=MeasureType.HEART_RATE,
UOM_BEATS_PER_MINUTE, name="Heart Pulse",
"mdi:heart-pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
True, icon="mdi:heart-pulse",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SPO2_PCT, key=Measurement.SPO2_PCT.value,
MeasureType.SP02, measurement=Measurement.SPO2_PCT,
"SP02", measure_type=MeasureType.SP02,
PERCENTAGE, name="SP02",
None, native_unit_of_measurement=PERCENTAGE,
True, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.HYDRATION, key=Measurement.HYDRATION.value,
MeasureType.HYDRATION, measurement=Measurement.HYDRATION,
"Hydration", measure_type=MeasureType.HYDRATION,
MASS_KILOGRAMS, name="Hydration",
"mdi:water", native_unit_of_measurement=UnitOfMass.KILOGRAMS,
False, device_class=SensorDeviceClass.WEIGHT,
UpdateType.POLL, icon="mdi:water",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.PWV, key=Measurement.PWV.value,
MeasureType.PULSE_WAVE_VELOCITY, measurement=Measurement.PWV,
"Pulse Wave Velocity", measure_type=MeasureType.PULSE_WAVE_VELOCITY,
SPEED_METERS_PER_SECOND, name="Pulse Wave Velocity",
None, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
True, device_class=SensorDeviceClass.SPEED,
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY, key=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY.value,
GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, measurement=Measurement.SLEEP_BREATHING_DISTURBANCES_INTENSITY,
"Breathing disturbances intensity", measure_type=GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY,
"", name="Breathing disturbances intensity",
"", state_class=SensorStateClass.MEASUREMENT,
False, entity_registry_enabled_default=False,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_DEEP_DURATION_SECONDS, key=Measurement.SLEEP_DEEP_DURATION_SECONDS.value,
GetSleepSummaryField.DEEP_SLEEP_DURATION, measurement=Measurement.SLEEP_DEEP_DURATION_SECONDS,
"Deep sleep", measure_type=GetSleepSummaryField.DEEP_SLEEP_DURATION,
TIME_SECONDS, name="Deep sleep",
"mdi:sleep", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep",
UpdateType.POLL, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_TOSLEEP_DURATION_SECONDS, key=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS.value,
GetSleepSummaryField.DURATION_TO_SLEEP, measurement=Measurement.SLEEP_TOSLEEP_DURATION_SECONDS,
"Time to sleep", measure_type=GetSleepSummaryField.DURATION_TO_SLEEP,
TIME_SECONDS, name="Time to sleep",
"mdi:sleep", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep",
UpdateType.POLL, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS, key=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS.value,
GetSleepSummaryField.DURATION_TO_WAKEUP, measurement=Measurement.SLEEP_TOWAKEUP_DURATION_SECONDS,
"Time to wakeup", measure_type=GetSleepSummaryField.DURATION_TO_WAKEUP,
TIME_SECONDS, name="Time to wakeup",
"mdi:sleep-off", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep-off",
UpdateType.POLL, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_HEART_RATE_AVERAGE, key=Measurement.SLEEP_HEART_RATE_AVERAGE.value,
GetSleepSummaryField.HR_AVERAGE, measurement=Measurement.SLEEP_HEART_RATE_AVERAGE,
"Average heart rate", measure_type=GetSleepSummaryField.HR_AVERAGE,
UOM_BEATS_PER_MINUTE, name="Average heart rate",
"mdi:heart-pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
False, icon="mdi:heart-pulse",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_HEART_RATE_MAX, key=Measurement.SLEEP_HEART_RATE_MAX.value,
GetSleepSummaryField.HR_MAX, measurement=Measurement.SLEEP_HEART_RATE_MAX,
"Maximum heart rate", measure_type=GetSleepSummaryField.HR_MAX,
UOM_BEATS_PER_MINUTE, name="Maximum heart rate",
"mdi:heart-pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
False, icon="mdi:heart-pulse",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_HEART_RATE_MIN, key=Measurement.SLEEP_HEART_RATE_MIN.value,
GetSleepSummaryField.HR_MIN, measurement=Measurement.SLEEP_HEART_RATE_MIN,
"Minimum heart rate", measure_type=GetSleepSummaryField.HR_MIN,
UOM_BEATS_PER_MINUTE, name="Minimum heart rate",
"mdi:heart-pulse", native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
False, icon="mdi:heart-pulse",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_LIGHT_DURATION_SECONDS, key=Measurement.SLEEP_LIGHT_DURATION_SECONDS.value,
GetSleepSummaryField.LIGHT_SLEEP_DURATION, measurement=Measurement.SLEEP_LIGHT_DURATION_SECONDS,
"Light sleep", measure_type=GetSleepSummaryField.LIGHT_SLEEP_DURATION,
TIME_SECONDS, name="Light sleep",
"mdi:sleep", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep",
UpdateType.POLL, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_REM_DURATION_SECONDS, key=Measurement.SLEEP_REM_DURATION_SECONDS.value,
GetSleepSummaryField.REM_SLEEP_DURATION, measurement=Measurement.SLEEP_REM_DURATION_SECONDS,
"REM sleep", measure_type=GetSleepSummaryField.REM_SLEEP_DURATION,
TIME_SECONDS, name="REM sleep",
"mdi:sleep", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep",
UpdateType.POLL, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE, key=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE.value,
GetSleepSummaryField.RR_AVERAGE, measurement=Measurement.SLEEP_RESPIRATORY_RATE_AVERAGE,
"Average respiratory rate", measure_type=GetSleepSummaryField.RR_AVERAGE,
UOM_BREATHS_PER_MINUTE, name="Average respiratory rate",
None, native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
False, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_RESPIRATORY_RATE_MAX, key=Measurement.SLEEP_RESPIRATORY_RATE_MAX.value,
GetSleepSummaryField.RR_MAX, measurement=Measurement.SLEEP_RESPIRATORY_RATE_MAX,
"Maximum respiratory rate", measure_type=GetSleepSummaryField.RR_MAX,
UOM_BREATHS_PER_MINUTE, name="Maximum respiratory rate",
None, native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
False, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_RESPIRATORY_RATE_MIN, key=Measurement.SLEEP_RESPIRATORY_RATE_MIN.value,
GetSleepSummaryField.RR_MIN, measurement=Measurement.SLEEP_RESPIRATORY_RATE_MIN,
"Minimum respiratory rate", measure_type=GetSleepSummaryField.RR_MIN,
UOM_BREATHS_PER_MINUTE, name="Minimum respiratory rate",
None, native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
False, state_class=SensorStateClass.MEASUREMENT,
UpdateType.POLL, entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_SCORE, key=Measurement.SLEEP_SCORE.value,
GetSleepSummaryField.SLEEP_SCORE, measurement=Measurement.SLEEP_SCORE,
"Sleep score", measure_type=GetSleepSummaryField.SLEEP_SCORE,
SCORE_POINTS, name="Sleep score",
"mdi:medal", native_unit_of_measurement=SCORE_POINTS,
False, icon="mdi:medal",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_SNORING, key=Measurement.SLEEP_SNORING.value,
GetSleepSummaryField.SNORING, measurement=Measurement.SLEEP_SNORING,
"Snoring", measure_type=GetSleepSummaryField.SNORING,
"", name="Snoring",
None, state_class=SensorStateClass.MEASUREMENT,
False, entity_registry_enabled_default=False,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_SNORING_EPISODE_COUNT, key=Measurement.SLEEP_SNORING_EPISODE_COUNT.value,
GetSleepSummaryField.SNORING_EPISODE_COUNT, measurement=Measurement.SLEEP_SNORING_EPISODE_COUNT,
"Snoring episode count", measure_type=GetSleepSummaryField.SNORING_EPISODE_COUNT,
"", name="Snoring episode count",
None, state_class=SensorStateClass.MEASUREMENT,
False, entity_registry_enabled_default=False,
UpdateType.POLL, update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_WAKEUP_COUNT, key=Measurement.SLEEP_WAKEUP_COUNT.value,
GetSleepSummaryField.WAKEUP_COUNT, measurement=Measurement.SLEEP_WAKEUP_COUNT,
"Wakeup count", measure_type=GetSleepSummaryField.WAKEUP_COUNT,
UOM_FREQUENCY, name="Wakeup count",
"mdi:sleep-off", native_unit_of_measurement=UOM_FREQUENCY,
False, icon="mdi:sleep-off",
UpdateType.POLL, state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
update_type=UpdateType.POLL,
), ),
WithingsAttribute( WithingsSensorEntityDescription(
Measurement.SLEEP_WAKEUP_DURATION_SECONDS, key=Measurement.SLEEP_WAKEUP_DURATION_SECONDS.value,
GetSleepSummaryField.WAKEUP_DURATION, measurement=Measurement.SLEEP_WAKEUP_DURATION_SECONDS,
"Wakeup time", measure_type=GetSleepSummaryField.WAKEUP_DURATION,
TIME_SECONDS, name="Wakeup time",
"mdi:sleep-off", native_unit_of_measurement=UnitOfTime.SECONDS,
False, icon="mdi:sleep-off",
UpdateType.POLL, 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): class WithingsHealthSensor(BaseWithingsSensor, SensorEntity):
"""Implementation of a Withings sensor.""" """Implementation of a Withings sensor."""
entity_description: WithingsSensorEntityDescription
@property @property
def native_value(self) -> None | str | int | float: def native_value(self) -> None | str | int | float:
"""Return the state of the entity.""" """Return the state of the entity."""
return self._state_data 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

View File

@ -24,7 +24,7 @@ from homeassistant.components.withings import async_unload_entry
from homeassistant.components.withings.common import ( from homeassistant.components.withings.common import (
ConfigEntryWithingsApi, ConfigEntryWithingsApi,
DataManager, DataManager,
WithingsAttribute, WithingsEntityDescription,
get_all_data_managers, get_all_data_managers,
get_attribute_unique_id, get_attribute_unique_id,
) )
@ -325,10 +325,13 @@ def get_data_manager_by_user_id(
async def async_get_entity_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: ) -> str | None:
"""Get an entity id for a user's attribute.""" """Get an entity id for a user's attribute."""
entity_registry = er.async_get(hass) 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) return entity_registry.async_get_entity_id(platform, const.DOMAIN, unique_id)

View File

@ -3,7 +3,7 @@ from withings_api.common import NotifyAppli
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.withings.binary_sensor import BINARY_SENSORS 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.components.withings.const import Measurement
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant 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 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 attr.measurement: attr for attr in BINARY_SENSORS
} }

View File

@ -18,7 +18,7 @@ from withings_api.common import (
) )
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 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.const import Measurement
from homeassistant.components.withings.sensor import SENSORS from homeassistant.components.withings.sensor import SENSORS
from homeassistant.core import HomeAssistant, State 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 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 attr.measurement: attr for attr in SENSORS
} }
@ -293,14 +293,14 @@ def async_assert_state_equals(
entity_id: str, entity_id: str,
state_obj: State, state_obj: State,
expected: Any, expected: Any,
attribute: WithingsAttribute, description: WithingsEntityDescription,
) -> None: ) -> None:
"""Assert at given state matches what is expected.""" """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, f"Expected entity {entity_id} to exist but it did not"
assert state_obj.state == str(expected), ( assert state_obj.state == str(expected), (
f"Expected {expected} but was {state_obj.state} " 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) 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) async_assert_state_equals(entity_id, state_obj, expected, attribute)
else: else:
assert state_obj is None assert state_obj is None