hass-core/homeassistant/components/withings/sensor.py
2023-10-15 21:38:30 +02:00

494 lines
19 KiB
Python

"""Sensors flow for Withings."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from aiowithings import MeasurementType, SleepSummary
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
UnitOfLength,
UnitOfMass,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import (
DOMAIN,
MEASUREMENT_COORDINATOR,
SCORE_POINTS,
SLEEP_COORDINATOR,
UOM_BEATS_PER_MINUTE,
UOM_BREATHS_PER_MINUTE,
UOM_FREQUENCY,
UOM_MMHG,
)
from .coordinator import (
WithingsDataUpdateCoordinator,
WithingsMeasurementDataUpdateCoordinator,
WithingsSleepDataUpdateCoordinator,
)
from .entity import WithingsEntity
@dataclass
class WithingsMeasurementSensorEntityDescriptionMixin:
"""Mixin for describing withings data."""
measurement_type: MeasurementType
@dataclass
class WithingsMeasurementSensorEntityDescription(
SensorEntityDescription, WithingsMeasurementSensorEntityDescriptionMixin
):
"""Immutable class for describing withings data."""
MEASUREMENT_SENSORS: dict[
MeasurementType, WithingsMeasurementSensorEntityDescription
] = {
MeasurementType.WEIGHT: WithingsMeasurementSensorEntityDescription(
key="weight_kg",
measurement_type=MeasurementType.WEIGHT,
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.FAT_MASS_WEIGHT: WithingsMeasurementSensorEntityDescription(
key="fat_mass_kg",
measurement_type=MeasurementType.FAT_MASS_WEIGHT,
translation_key="fat_mass",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.FAT_FREE_MASS: WithingsMeasurementSensorEntityDescription(
key="fat_free_mass_kg",
measurement_type=MeasurementType.FAT_FREE_MASS,
translation_key="fat_free_mass",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.MUSCLE_MASS: WithingsMeasurementSensorEntityDescription(
key="muscle_mass_kg",
measurement_type=MeasurementType.MUSCLE_MASS,
translation_key="muscle_mass",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.BONE_MASS: WithingsMeasurementSensorEntityDescription(
key="bone_mass_kg",
measurement_type=MeasurementType.BONE_MASS,
translation_key="bone_mass",
icon="mdi:bone",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.HEIGHT: WithingsMeasurementSensorEntityDescription(
key="height_m",
measurement_type=MeasurementType.HEIGHT,
translation_key="height",
native_unit_of_measurement=UnitOfLength.METERS,
suggested_display_precision=1,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
MeasurementType.TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="temperature_c",
measurement_type=MeasurementType.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.BODY_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="body_temperature_c",
measurement_type=MeasurementType.BODY_TEMPERATURE,
translation_key="body_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.SKIN_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
key="skin_temperature_c",
measurement_type=MeasurementType.SKIN_TEMPERATURE,
translation_key="skin_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.FAT_RATIO: WithingsMeasurementSensorEntityDescription(
key="fat_ratio_pct",
measurement_type=MeasurementType.FAT_RATIO,
translation_key="fat_ratio",
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.DIASTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
key="diastolic_blood_pressure_mmhg",
measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE,
translation_key="diastolic_blood_pressure",
native_unit_of_measurement=UOM_MMHG,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.SYSTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
key="systolic_blood_pressure_mmhg",
measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE,
translation_key="systolic_blood_pressure",
native_unit_of_measurement=UOM_MMHG,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.HEART_RATE: WithingsMeasurementSensorEntityDescription(
key="heart_pulse_bpm",
measurement_type=MeasurementType.HEART_RATE,
translation_key="heart_pulse",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.SP02: WithingsMeasurementSensorEntityDescription(
key="spo2_pct",
measurement_type=MeasurementType.SP02,
translation_key="spo2",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.HYDRATION: WithingsMeasurementSensorEntityDescription(
key="hydration",
measurement_type=MeasurementType.HYDRATION,
translation_key="hydration",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
icon="mdi:water",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
MeasurementType.PULSE_WAVE_VELOCITY: WithingsMeasurementSensorEntityDescription(
key="pulse_wave_velocity",
measurement_type=MeasurementType.PULSE_WAVE_VELOCITY,
translation_key="pulse_wave_velocity",
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
device_class=SensorDeviceClass.SPEED,
state_class=SensorStateClass.MEASUREMENT,
),
MeasurementType.VO2: WithingsMeasurementSensorEntityDescription(
key="vo2_max",
measurement_type=MeasurementType.VO2,
translation_key="vo2_max",
native_unit_of_measurement="ml/min/kg",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
MeasurementType.EXTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription(
key="extracellular_water",
measurement_type=MeasurementType.EXTRACELLULAR_WATER,
translation_key="extracellular_water",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
MeasurementType.INTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription(
key="intracellular_water",
measurement_type=MeasurementType.INTRACELLULAR_WATER,
translation_key="intracellular_water",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
MeasurementType.VASCULAR_AGE: WithingsMeasurementSensorEntityDescription(
key="vascular_age",
measurement_type=MeasurementType.VASCULAR_AGE,
translation_key="vascular_age",
entity_registry_enabled_default=False,
),
}
@dataclass
class WithingsSleepSensorEntityDescriptionMixin:
"""Mixin for describing withings data."""
value_fn: Callable[[SleepSummary], StateType]
@dataclass
class WithingsSleepSensorEntityDescription(
SensorEntityDescription, WithingsSleepSensorEntityDescriptionMixin
):
"""Immutable class for describing withings data."""
SLEEP_SENSORS = [
WithingsSleepSensorEntityDescription(
key="sleep_breathing_disturbances_intensity",
value_fn=lambda sleep_summary: sleep_summary.breathing_disturbances_intensity,
translation_key="breathing_disturbances_intensity",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_deep_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.deep_sleep_duration,
translation_key="deep_sleep",
native_unit_of_measurement=UnitOfTime.SECONDS,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_tosleep_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.sleep_latency,
translation_key="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,
),
WithingsSleepSensorEntityDescription(
key="sleep_towakeup_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.wake_up_latency,
translation_key="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,
),
WithingsSleepSensorEntityDescription(
key="sleep_heart_rate_average_bpm",
value_fn=lambda sleep_summary: sleep_summary.average_heart_rate,
translation_key="average_heart_rate",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_heart_rate_max_bpm",
value_fn=lambda sleep_summary: sleep_summary.max_heart_rate,
translation_key="maximum_heart_rate",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_heart_rate_min_bpm",
value_fn=lambda sleep_summary: sleep_summary.min_heart_rate,
translation_key="minimum_heart_rate",
native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
icon="mdi:heart-pulse",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_light_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.light_sleep_duration,
translation_key="light_sleep",
native_unit_of_measurement=UnitOfTime.SECONDS,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_rem_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.rem_sleep_duration,
translation_key="rem_sleep",
native_unit_of_measurement=UnitOfTime.SECONDS,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_respiratory_average_bpm",
value_fn=lambda sleep_summary: sleep_summary.average_respiration_rate,
translation_key="average_respiratory_rate",
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_respiratory_max_bpm",
value_fn=lambda sleep_summary: sleep_summary.max_respiration_rate,
translation_key="maximum_respiratory_rate",
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_respiratory_min_bpm",
value_fn=lambda sleep_summary: sleep_summary.min_respiration_rate,
translation_key="minimum_respiratory_rate",
native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_score",
value_fn=lambda sleep_summary: sleep_summary.sleep_score,
translation_key="sleep_score",
native_unit_of_measurement=SCORE_POINTS,
icon="mdi:medal",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_snoring",
value_fn=lambda sleep_summary: sleep_summary.snoring,
translation_key="snoring",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_snoring_eposode_count",
value_fn=lambda sleep_summary: sleep_summary.snoring_count,
translation_key="snoring_episode_count",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_wakeup_count",
value_fn=lambda sleep_summary: sleep_summary.wake_up_count,
translation_key="wakeup_count",
native_unit_of_measurement=UOM_FREQUENCY,
icon="mdi:sleep-off",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
WithingsSleepSensorEntityDescription(
key="sleep_wakeup_duration_seconds",
value_fn=lambda sleep_summary: sleep_summary.total_time_awake,
translation_key="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,
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor config entry."""
measurement_coordinator: WithingsMeasurementDataUpdateCoordinator = hass.data[
DOMAIN
][entry.entry_id][MEASUREMENT_COORDINATOR]
current_measurement_types = set(measurement_coordinator.data.keys())
entities: list[SensorEntity] = []
entities.extend(
WithingsMeasurementSensor(
measurement_coordinator, MEASUREMENT_SENSORS[measurement_type]
)
for measurement_type in measurement_coordinator.data
if measurement_type in MEASUREMENT_SENSORS
)
def _async_measurement_listener() -> None:
"""Listen for new measurements and add sensors if they did not exist."""
received_measurement_types = set(measurement_coordinator.data.keys())
new_measurement_types = received_measurement_types - current_measurement_types
if new_measurement_types:
current_measurement_types.update(new_measurement_types)
async_add_entities(
WithingsMeasurementSensor(
measurement_coordinator, MEASUREMENT_SENSORS[measurement_type]
)
for measurement_type in new_measurement_types
)
measurement_coordinator.async_add_listener(_async_measurement_listener)
sleep_coordinator: WithingsSleepDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
][SLEEP_COORDINATOR]
entities.extend(
WithingsSleepSensor(sleep_coordinator, attribute) for attribute in SLEEP_SENSORS
)
async_add_entities(entities)
class WithingsSensor(WithingsEntity, SensorEntity):
"""Implementation of a Withings sensor."""
def __init__(
self,
coordinator: WithingsDataUpdateCoordinator,
entity_description: SensorEntityDescription,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator, entity_description.key)
self.entity_description = entity_description
class WithingsMeasurementSensor(WithingsSensor):
"""Implementation of a Withings measurement sensor."""
coordinator: WithingsMeasurementDataUpdateCoordinator
entity_description: WithingsMeasurementSensorEntityDescription
@property
def native_value(self) -> float:
"""Return the state of the entity."""
return self.coordinator.data[self.entity_description.measurement_type]
@property
def available(self) -> bool:
"""Return if the sensor is available."""
return (
super().available
and self.entity_description.measurement_type in self.coordinator.data
)
class WithingsSleepSensor(WithingsSensor):
"""Implementation of a Withings sleep sensor."""
coordinator: WithingsSleepDataUpdateCoordinator
entity_description: WithingsSleepSensorEntityDescription
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
assert 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