core/homeassistant/components/withings/sensor.py

1011 lines
37 KiB
Python

"""Sensors flow for Withings."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from aiowithings import (
Activity,
Device,
Goals,
MeasurementPosition,
MeasurementType,
SleepSummary,
Workout,
WorkoutCategory,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
PERCENTAGE,
Platform,
UnitOfLength,
UnitOfMass,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from . import WithingsConfigEntry
from .const import (
DOMAIN,
LOGGER,
SCORE_POINTS,
UOM_BEATS_PER_MINUTE,
UOM_BREATHS_PER_MINUTE,
UOM_FREQUENCY,
UOM_MMHG,
)
from .coordinator import (
WithingsActivityDataUpdateCoordinator,
WithingsDataUpdateCoordinator,
WithingsDeviceDataUpdateCoordinator,
WithingsGoalsDataUpdateCoordinator,
WithingsMeasurementDataUpdateCoordinator,
WithingsSleepDataUpdateCoordinator,
WithingsWorkoutDataUpdateCoordinator,
)
from .entity import WithingsDeviceEntity, WithingsEntity
@dataclass(frozen=True, kw_only=True)
class WithingsMeasurementSensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
measurement_type: MeasurementType
measurement_position: MeasurementPosition | None = None
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",
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,
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,
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,
),
MeasurementType.VISCERAL_FAT: WithingsMeasurementSensorEntityDescription(
key="visceral_fat",
measurement_type=MeasurementType.VISCERAL_FAT,
translation_key="visceral_fat_index",
entity_registry_enabled_default=False,
),
MeasurementType.ELECTRODERMAL_ACTIVITY_FEET: WithingsMeasurementSensorEntityDescription(
key="electrodermal_activity_feet",
measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_FEET,
translation_key="electrodermal_activity_feet",
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
),
MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT: WithingsMeasurementSensorEntityDescription(
key="electrodermal_activity_left_foot",
measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT,
translation_key="electrodermal_activity_left_foot",
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
),
MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT: WithingsMeasurementSensorEntityDescription(
key="electrodermal_activity_right_foot",
measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT,
translation_key="electrodermal_activity_right_foot",
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
),
}
def get_positional_measurement_description(
measurement_type: MeasurementType, measurement_position: MeasurementPosition
) -> WithingsMeasurementSensorEntityDescription | None:
"""Get the sensor description for a measurement type."""
if measurement_position not in (
MeasurementPosition.TORSO,
MeasurementPosition.LEFT_ARM,
MeasurementPosition.RIGHT_ARM,
MeasurementPosition.LEFT_LEG,
MeasurementPosition.RIGHT_LEG,
) or measurement_type not in (
MeasurementType.MUSCLE_MASS_FOR_SEGMENTS,
MeasurementType.FAT_FREE_MASS_FOR_SEGMENTS,
MeasurementType.FAT_MASS_FOR_SEGMENTS,
):
return None
return WithingsMeasurementSensorEntityDescription(
key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}",
measurement_type=measurement_type,
measurement_position=measurement_position,
translation_key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
suggested_display_precision=2,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
)
def get_measurement_description(
measurement: tuple[MeasurementType, MeasurementPosition | None],
) -> WithingsMeasurementSensorEntityDescription | None:
"""Get the sensor description for a measurement type."""
measurement_type, measurement_position = measurement
if measurement_position is not None:
return get_positional_measurement_description(
measurement_type, measurement_position
)
return MEASUREMENT_SENSORS.get(measurement_type)
@dataclass(frozen=True, kw_only=True)
class WithingsSleepSensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
value_fn: Callable[[SleepSummary], StateType]
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,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
]
@dataclass(frozen=True, kw_only=True)
class WithingsActivitySensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
value_fn: Callable[[Activity], StateType]
ACTIVITY_SENSORS = [
WithingsActivitySensorEntityDescription(
key="activity_steps_today",
value_fn=lambda activity: activity.steps,
translation_key="activity_steps_today",
native_unit_of_measurement="steps",
state_class=SensorStateClass.TOTAL,
),
WithingsActivitySensorEntityDescription(
key="activity_distance_today",
value_fn=lambda activity: activity.distance,
translation_key="activity_distance_today",
suggested_display_precision=0,
native_unit_of_measurement=UnitOfLength.METERS,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.TOTAL,
),
WithingsActivitySensorEntityDescription(
key="activity_floors_climbed_today",
value_fn=lambda activity: activity.elevation,
translation_key="activity_elevation_today",
native_unit_of_measurement=UnitOfLength.METERS,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.TOTAL,
),
WithingsActivitySensorEntityDescription(
key="activity_soft_duration_today",
value_fn=lambda activity: activity.soft_activity,
translation_key="activity_soft_duration_today",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
WithingsActivitySensorEntityDescription(
key="activity_moderate_duration_today",
value_fn=lambda activity: activity.moderate_activity,
translation_key="activity_moderate_duration_today",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
WithingsActivitySensorEntityDescription(
key="activity_intense_duration_today",
value_fn=lambda activity: activity.intense_activity,
translation_key="activity_intense_duration_today",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=False,
),
WithingsActivitySensorEntityDescription(
key="activity_active_duration_today",
value_fn=lambda activity: activity.total_time_active,
translation_key="activity_active_duration_today",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
),
WithingsActivitySensorEntityDescription(
key="activity_active_calories_burnt_today",
value_fn=lambda activity: activity.active_calories_burnt,
suggested_display_precision=1,
translation_key="activity_active_calories_burnt_today",
native_unit_of_measurement="calories",
state_class=SensorStateClass.TOTAL,
),
WithingsActivitySensorEntityDescription(
key="activity_total_calories_burnt_today",
value_fn=lambda activity: activity.total_calories_burnt,
suggested_display_precision=1,
translation_key="activity_total_calories_burnt_today",
native_unit_of_measurement="calories",
state_class=SensorStateClass.TOTAL,
),
]
STEP_GOAL = "steps"
SLEEP_GOAL = "sleep"
WEIGHT_GOAL = "weight"
@dataclass(frozen=True, kw_only=True)
class WithingsGoalsSensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
value_fn: Callable[[Goals], StateType]
GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = {
STEP_GOAL: WithingsGoalsSensorEntityDescription(
key="step_goal",
value_fn=lambda goals: goals.steps,
translation_key="step_goal",
native_unit_of_measurement="steps",
state_class=SensorStateClass.MEASUREMENT,
),
SLEEP_GOAL: WithingsGoalsSensorEntityDescription(
key="sleep_goal",
value_fn=lambda goals: goals.sleep,
translation_key="sleep_goal",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
WEIGHT_GOAL: WithingsGoalsSensorEntityDescription(
key="weight_goal",
value_fn=lambda goals: goals.weight,
translation_key="weight_goal",
native_unit_of_measurement=UnitOfMass.KILOGRAMS,
device_class=SensorDeviceClass.WEIGHT,
state_class=SensorStateClass.MEASUREMENT,
),
}
@dataclass(frozen=True, kw_only=True)
class WithingsWorkoutSensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
value_fn: Callable[[Workout], StateType]
_WORKOUT_CATEGORY = [
workout_category.name.lower() for workout_category in WorkoutCategory
]
WORKOUT_SENSORS = [
WithingsWorkoutSensorEntityDescription(
key="workout_type",
value_fn=lambda workout: workout.category.name.lower(),
device_class=SensorDeviceClass.ENUM,
translation_key="workout_type",
options=_WORKOUT_CATEGORY,
),
WithingsWorkoutSensorEntityDescription(
key="workout_active_calories_burnt",
value_fn=lambda workout: workout.active_calories_burnt,
translation_key="workout_active_calories_burnt",
suggested_display_precision=1,
native_unit_of_measurement="calories",
),
WithingsWorkoutSensorEntityDescription(
key="workout_distance",
value_fn=lambda workout: workout.distance,
translation_key="workout_distance",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.METERS,
suggested_display_precision=0,
),
WithingsWorkoutSensorEntityDescription(
key="workout_floors_climbed",
value_fn=lambda workout: workout.elevation,
translation_key="workout_elevation",
native_unit_of_measurement=UnitOfLength.METERS,
device_class=SensorDeviceClass.DISTANCE,
),
WithingsWorkoutSensorEntityDescription(
key="workout_intensity",
value_fn=lambda workout: workout.intensity,
translation_key="workout_intensity",
),
WithingsWorkoutSensorEntityDescription(
key="workout_pause_duration",
value_fn=lambda workout: workout.pause_duration or 0,
translation_key="workout_pause_duration",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
),
WithingsWorkoutSensorEntityDescription(
key="workout_duration",
value_fn=lambda workout: (
workout.end_date - workout.start_date
).total_seconds(),
translation_key="workout_duration",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
),
]
@dataclass(frozen=True, kw_only=True)
class WithingsDeviceSensorEntityDescription(SensorEntityDescription):
"""Immutable class for describing withings data."""
value_fn: Callable[[Device], StateType]
DEVICE_SENSORS = [
WithingsDeviceSensorEntityDescription(
key="battery",
translation_key="battery",
options=["low", "medium", "high"],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda device: device.battery,
)
]
def get_current_goals(goals: Goals) -> set[str]:
"""Return a list of present goals."""
result = set()
for goal in (STEP_GOAL, SLEEP_GOAL, WEIGHT_GOAL):
if getattr(goals, goal):
result.add(goal)
return result
async def async_setup_entry(
hass: HomeAssistant,
entry: WithingsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor config entry."""
ent_reg = er.async_get(hass)
withings_data = entry.runtime_data
measurement_coordinator = withings_data.measurement_coordinator
entities: list[SensorEntity] = []
entities.extend(
WithingsMeasurementSensor(measurement_coordinator, description)
for measurement_type in measurement_coordinator.data
if (description := get_measurement_description(measurement_type)) is not None
)
current_measurement_types = set(measurement_coordinator.data)
def _async_measurement_listener() -> None:
"""Listen for new measurements and add sensors if they did not exist."""
received_measurement_types = set(measurement_coordinator.data)
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, description)
for measurement_type in new_measurement_types
if (description := get_measurement_description(measurement_type))
is not None
)
measurement_coordinator.async_add_listener(_async_measurement_listener)
goals_coordinator = withings_data.goals_coordinator
current_goals = get_current_goals(goals_coordinator.data)
entities.extend(
WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
for goal in current_goals
)
def _async_goals_listener() -> None:
"""Listen for new goals and add sensors if they did not exist."""
received_goals = get_current_goals(goals_coordinator.data)
new_goals = received_goals - current_goals
if new_goals:
current_goals.update(new_goals)
async_add_entities(
WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
for goal in new_goals
)
goals_coordinator.async_add_listener(_async_goals_listener)
activity_coordinator = withings_data.activity_coordinator
activity_entities_setup_before = ent_reg.async_get_entity_id(
Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today"
)
if activity_coordinator.data is not None or activity_entities_setup_before:
entities.extend(
WithingsActivitySensor(activity_coordinator, attribute)
for attribute in ACTIVITY_SENSORS
)
else:
remove_activity_listener: Callable[[], None]
def _async_add_activity_entities() -> None:
"""Add activity entities."""
if activity_coordinator.data is not None:
async_add_entities(
WithingsActivitySensor(activity_coordinator, attribute)
for attribute in ACTIVITY_SENSORS
)
remove_activity_listener()
remove_activity_listener = activity_coordinator.async_add_listener(
_async_add_activity_entities
)
sleep_coordinator = withings_data.sleep_coordinator
sleep_entities_setup_before = ent_reg.async_get_entity_id(
Platform.SENSOR,
DOMAIN,
f"withings_{entry.unique_id}_sleep_deep_duration_seconds",
)
if sleep_coordinator.data is not None or sleep_entities_setup_before:
entities.extend(
WithingsSleepSensor(sleep_coordinator, attribute)
for attribute in SLEEP_SENSORS
)
else:
remove_sleep_listener: Callable[[], None]
def _async_add_sleep_entities() -> None:
"""Add sleep entities."""
if sleep_coordinator.data is not None:
async_add_entities(
WithingsSleepSensor(sleep_coordinator, attribute)
for attribute in SLEEP_SENSORS
)
remove_sleep_listener()
remove_sleep_listener = sleep_coordinator.async_add_listener(
_async_add_sleep_entities
)
workout_coordinator = withings_data.workout_coordinator
workout_entities_setup_before = ent_reg.async_get_entity_id(
Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_workout_type"
)
if workout_coordinator.data is not None or workout_entities_setup_before:
entities.extend(
WithingsWorkoutSensor(workout_coordinator, attribute)
for attribute in WORKOUT_SENSORS
)
else:
remove_workout_listener: Callable[[], None]
def _async_add_workout_entities() -> None:
"""Add workout entities."""
if workout_coordinator.data is not None:
async_add_entities(
WithingsWorkoutSensor(workout_coordinator, attribute)
for attribute in WORKOUT_SENSORS
)
remove_workout_listener()
remove_workout_listener = workout_coordinator.async_add_listener(
_async_add_workout_entities
)
device_coordinator = withings_data.device_coordinator
current_devices: set[str] = set()
def _async_device_listener() -> None:
"""Add device entities."""
received_devices = set(device_coordinator.data)
new_devices = received_devices - current_devices
old_devices = current_devices - received_devices
if new_devices:
device_registry = dr.async_get(hass)
for device_id in new_devices:
if device := device_registry.async_get_device({(DOMAIN, device_id)}):
if any(
(
config_entry := hass.config_entries.async_get_entry(
config_entry_id
)
)
and config_entry.state == ConfigEntryState.LOADED
for config_entry_id in device.config_entries
):
continue
async_add_entities(
WithingsDeviceSensor(device_coordinator, description, device_id)
for description in DEVICE_SENSORS
)
current_devices.add(device_id)
if old_devices:
device_registry = dr.async_get(hass)
for device_id in old_devices:
if device := device_registry.async_get_device({(DOMAIN, device_id)}):
device_registry.async_update_device(
device.id, remove_config_entry_id=entry.entry_id
)
current_devices.remove(device_id)
device_coordinator.async_add_listener(_async_device_listener)
_async_device_listener()
if not entities:
LOGGER.warning(
"No data found for Withings entry %s, sensors will be added when new data is available",
entry.title,
)
async_add_entities(entities)
class WithingsSensor[
_T: WithingsDataUpdateCoordinator[Any],
_ED: SensorEntityDescription,
](WithingsEntity[_T], SensorEntity):
"""Implementation of a Withings sensor."""
entity_description: _ED
def __init__(
self,
coordinator: _T,
entity_description: _ED,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator, entity_description.key)
self.entity_description = entity_description
class WithingsMeasurementSensor(
WithingsSensor[
WithingsMeasurementDataUpdateCoordinator,
WithingsMeasurementSensorEntityDescription,
]
):
"""Implementation of a Withings measurement sensor."""
@property
def native_value(self) -> float:
"""Return the state of the entity."""
return self.coordinator.data[
(
self.entity_description.measurement_type,
self.entity_description.measurement_position,
)
]
@property
def available(self) -> bool:
"""Return if the sensor is available."""
return (
super().available
and (
self.entity_description.measurement_type,
self.entity_description.measurement_position,
)
in self.coordinator.data
)
class WithingsSleepSensor(
WithingsSensor[
WithingsSleepDataUpdateCoordinator,
WithingsSleepSensorEntityDescription,
]
):
"""Implementation of a Withings sleep sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
if not self.coordinator.data:
return None
return self.entity_description.value_fn(self.coordinator.data)
class WithingsGoalsSensor(
WithingsSensor[
WithingsGoalsDataUpdateCoordinator,
WithingsGoalsSensorEntityDescription,
]
):
"""Implementation of a Withings goals sensor."""
@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)
class WithingsActivitySensor(
WithingsSensor[
WithingsActivityDataUpdateCoordinator,
WithingsActivitySensorEntityDescription,
]
):
"""Implementation of a Withings activity sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
if not self.coordinator.data:
return None
return self.entity_description.value_fn(self.coordinator.data)
@property
def last_reset(self) -> datetime:
"""These values reset every day."""
return dt_util.start_of_local_day()
class WithingsWorkoutSensor(
WithingsSensor[
WithingsWorkoutDataUpdateCoordinator,
WithingsWorkoutSensorEntityDescription,
]
):
"""Implementation of a Withings workout sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
if not self.coordinator.data:
return None
return self.entity_description.value_fn(self.coordinator.data)
class WithingsDeviceSensor(WithingsDeviceEntity, SensorEntity):
"""Implementation of a Withings workout sensor."""
entity_description: WithingsDeviceSensorEntityDescription
def __init__(
self,
coordinator: WithingsDeviceDataUpdateCoordinator,
entity_description: WithingsDeviceSensorEntityDescription,
device_id: str,
) -> None:
"""Initialize sensor."""
super().__init__(coordinator, device_id, entity_description.key)
self.entity_description = entity_description
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
return self.entity_description.value_fn(self.device)