Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensors flow for Withings."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import datetime
8 from typing import Any
9 
10 from aiowithings import (
11  Activity,
12  Device,
13  Goals,
14  MeasurementPosition,
15  MeasurementType,
16  SleepSummary,
17  Workout,
18  WorkoutCategory,
19 )
20 
22  SensorDeviceClass,
23  SensorEntity,
24  SensorEntityDescription,
25  SensorStateClass,
26 )
27 from homeassistant.config_entries import ConfigEntryState
28 from homeassistant.const import (
29  PERCENTAGE,
30  Platform,
31  UnitOfLength,
32  UnitOfMass,
33  UnitOfSpeed,
34  UnitOfTemperature,
35  UnitOfTime,
36 )
37 from homeassistant.core import HomeAssistant
38 from homeassistant.helpers import device_registry as dr, entity_registry as er
39 from homeassistant.helpers.entity_platform import AddEntitiesCallback
40 from homeassistant.helpers.typing import StateType
41 from homeassistant.util import dt as dt_util
42 
43 from . import WithingsConfigEntry
44 from .const import (
45  DOMAIN,
46  LOGGER,
47  SCORE_POINTS,
48  UOM_BEATS_PER_MINUTE,
49  UOM_BREATHS_PER_MINUTE,
50  UOM_FREQUENCY,
51  UOM_MMHG,
52 )
53 from .coordinator import (
54  WithingsActivityDataUpdateCoordinator,
55  WithingsDataUpdateCoordinator,
56  WithingsDeviceDataUpdateCoordinator,
57  WithingsGoalsDataUpdateCoordinator,
58  WithingsMeasurementDataUpdateCoordinator,
59  WithingsSleepDataUpdateCoordinator,
60  WithingsWorkoutDataUpdateCoordinator,
61 )
62 from .entity import WithingsDeviceEntity, WithingsEntity
63 
64 
65 @dataclass(frozen=True, kw_only=True)
67  """Immutable class for describing withings data."""
68 
69  measurement_type: MeasurementType
70  measurement_position: MeasurementPosition | None = None
71 
72 
73 MEASUREMENT_SENSORS: dict[
74  MeasurementType, WithingsMeasurementSensorEntityDescription
75 ] = {
76  MeasurementType.WEIGHT: WithingsMeasurementSensorEntityDescription(
77  key="weight_kg",
78  measurement_type=MeasurementType.WEIGHT,
79  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
80  suggested_display_precision=2,
81  device_class=SensorDeviceClass.WEIGHT,
82  state_class=SensorStateClass.MEASUREMENT,
83  ),
84  MeasurementType.FAT_MASS_WEIGHT: WithingsMeasurementSensorEntityDescription(
85  key="fat_mass_kg",
86  measurement_type=MeasurementType.FAT_MASS_WEIGHT,
87  translation_key="fat_mass",
88  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
89  suggested_display_precision=2,
90  device_class=SensorDeviceClass.WEIGHT,
91  state_class=SensorStateClass.MEASUREMENT,
92  ),
93  MeasurementType.FAT_FREE_MASS: WithingsMeasurementSensorEntityDescription(
94  key="fat_free_mass_kg",
95  measurement_type=MeasurementType.FAT_FREE_MASS,
96  translation_key="fat_free_mass",
97  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
98  suggested_display_precision=2,
99  device_class=SensorDeviceClass.WEIGHT,
100  state_class=SensorStateClass.MEASUREMENT,
101  ),
102  MeasurementType.MUSCLE_MASS: WithingsMeasurementSensorEntityDescription(
103  key="muscle_mass_kg",
104  measurement_type=MeasurementType.MUSCLE_MASS,
105  translation_key="muscle_mass",
106  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
107  suggested_display_precision=2,
108  device_class=SensorDeviceClass.WEIGHT,
109  state_class=SensorStateClass.MEASUREMENT,
110  ),
111  MeasurementType.BONE_MASS: WithingsMeasurementSensorEntityDescription(
112  key="bone_mass_kg",
113  measurement_type=MeasurementType.BONE_MASS,
114  translation_key="bone_mass",
115  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
116  suggested_display_precision=2,
117  device_class=SensorDeviceClass.WEIGHT,
118  state_class=SensorStateClass.MEASUREMENT,
119  ),
120  MeasurementType.HEIGHT: WithingsMeasurementSensorEntityDescription(
121  key="height_m",
122  measurement_type=MeasurementType.HEIGHT,
123  translation_key="height",
124  native_unit_of_measurement=UnitOfLength.METERS,
125  suggested_display_precision=1,
126  device_class=SensorDeviceClass.DISTANCE,
127  state_class=SensorStateClass.MEASUREMENT,
128  entity_registry_enabled_default=False,
129  ),
130  MeasurementType.TEMPERATURE: WithingsMeasurementSensorEntityDescription(
131  key="temperature_c",
132  measurement_type=MeasurementType.TEMPERATURE,
133  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
134  device_class=SensorDeviceClass.TEMPERATURE,
135  state_class=SensorStateClass.MEASUREMENT,
136  ),
137  MeasurementType.BODY_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
138  key="body_temperature_c",
139  measurement_type=MeasurementType.BODY_TEMPERATURE,
140  translation_key="body_temperature",
141  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
142  device_class=SensorDeviceClass.TEMPERATURE,
143  state_class=SensorStateClass.MEASUREMENT,
144  ),
145  MeasurementType.SKIN_TEMPERATURE: WithingsMeasurementSensorEntityDescription(
146  key="skin_temperature_c",
147  measurement_type=MeasurementType.SKIN_TEMPERATURE,
148  translation_key="skin_temperature",
149  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
150  device_class=SensorDeviceClass.TEMPERATURE,
151  state_class=SensorStateClass.MEASUREMENT,
152  ),
153  MeasurementType.FAT_RATIO: WithingsMeasurementSensorEntityDescription(
154  key="fat_ratio_pct",
155  measurement_type=MeasurementType.FAT_RATIO,
156  translation_key="fat_ratio",
157  native_unit_of_measurement=PERCENTAGE,
158  suggested_display_precision=2,
159  state_class=SensorStateClass.MEASUREMENT,
160  ),
161  MeasurementType.DIASTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
162  key="diastolic_blood_pressure_mmhg",
163  measurement_type=MeasurementType.DIASTOLIC_BLOOD_PRESSURE,
164  translation_key="diastolic_blood_pressure",
165  native_unit_of_measurement=UOM_MMHG,
166  state_class=SensorStateClass.MEASUREMENT,
167  ),
168  MeasurementType.SYSTOLIC_BLOOD_PRESSURE: WithingsMeasurementSensorEntityDescription(
169  key="systolic_blood_pressure_mmhg",
170  measurement_type=MeasurementType.SYSTOLIC_BLOOD_PRESSURE,
171  translation_key="systolic_blood_pressure",
172  native_unit_of_measurement=UOM_MMHG,
173  state_class=SensorStateClass.MEASUREMENT,
174  ),
175  MeasurementType.HEART_RATE: WithingsMeasurementSensorEntityDescription(
176  key="heart_pulse_bpm",
177  measurement_type=MeasurementType.HEART_RATE,
178  translation_key="heart_pulse",
179  native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
180  state_class=SensorStateClass.MEASUREMENT,
181  ),
182  MeasurementType.SP02: WithingsMeasurementSensorEntityDescription(
183  key="spo2_pct",
184  measurement_type=MeasurementType.SP02,
185  translation_key="spo2",
186  native_unit_of_measurement=PERCENTAGE,
187  state_class=SensorStateClass.MEASUREMENT,
188  ),
189  MeasurementType.HYDRATION: WithingsMeasurementSensorEntityDescription(
190  key="hydration",
191  measurement_type=MeasurementType.HYDRATION,
192  translation_key="hydration",
193  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
194  device_class=SensorDeviceClass.WEIGHT,
195  state_class=SensorStateClass.MEASUREMENT,
196  entity_registry_enabled_default=False,
197  ),
198  MeasurementType.PULSE_WAVE_VELOCITY: WithingsMeasurementSensorEntityDescription(
199  key="pulse_wave_velocity",
200  measurement_type=MeasurementType.PULSE_WAVE_VELOCITY,
201  translation_key="pulse_wave_velocity",
202  native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
203  device_class=SensorDeviceClass.SPEED,
204  state_class=SensorStateClass.MEASUREMENT,
205  ),
206  MeasurementType.VO2: WithingsMeasurementSensorEntityDescription(
207  key="vo2_max",
208  measurement_type=MeasurementType.VO2,
209  translation_key="vo2_max",
210  native_unit_of_measurement="ml/min/kg",
211  state_class=SensorStateClass.MEASUREMENT,
212  entity_registry_enabled_default=False,
213  ),
214  MeasurementType.EXTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription(
215  key="extracellular_water",
216  measurement_type=MeasurementType.EXTRACELLULAR_WATER,
217  translation_key="extracellular_water",
218  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
219  device_class=SensorDeviceClass.WEIGHT,
220  state_class=SensorStateClass.MEASUREMENT,
221  entity_registry_enabled_default=False,
222  ),
223  MeasurementType.INTRACELLULAR_WATER: WithingsMeasurementSensorEntityDescription(
224  key="intracellular_water",
225  measurement_type=MeasurementType.INTRACELLULAR_WATER,
226  translation_key="intracellular_water",
227  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
228  device_class=SensorDeviceClass.WEIGHT,
229  state_class=SensorStateClass.MEASUREMENT,
230  entity_registry_enabled_default=False,
231  ),
232  MeasurementType.VASCULAR_AGE: WithingsMeasurementSensorEntityDescription(
233  key="vascular_age",
234  measurement_type=MeasurementType.VASCULAR_AGE,
235  translation_key="vascular_age",
236  entity_registry_enabled_default=False,
237  ),
238  MeasurementType.VISCERAL_FAT: WithingsMeasurementSensorEntityDescription(
239  key="visceral_fat",
240  measurement_type=MeasurementType.VISCERAL_FAT,
241  translation_key="visceral_fat_index",
242  entity_registry_enabled_default=False,
243  ),
244  MeasurementType.ELECTRODERMAL_ACTIVITY_FEET: WithingsMeasurementSensorEntityDescription(
245  key="electrodermal_activity_feet",
246  measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_FEET,
247  translation_key="electrodermal_activity_feet",
248  native_unit_of_measurement=PERCENTAGE,
249  entity_registry_enabled_default=False,
250  ),
251  MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT: WithingsMeasurementSensorEntityDescription(
252  key="electrodermal_activity_left_foot",
253  measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_LEFT_FOOT,
254  translation_key="electrodermal_activity_left_foot",
255  native_unit_of_measurement=PERCENTAGE,
256  entity_registry_enabled_default=False,
257  ),
258  MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT: WithingsMeasurementSensorEntityDescription(
259  key="electrodermal_activity_right_foot",
260  measurement_type=MeasurementType.ELECTRODERMAL_ACTIVITY_RIGHT_FOOT,
261  translation_key="electrodermal_activity_right_foot",
262  native_unit_of_measurement=PERCENTAGE,
263  entity_registry_enabled_default=False,
264  ),
265 }
266 
267 
269  measurement_type: MeasurementType, measurement_position: MeasurementPosition
270 ) -> WithingsMeasurementSensorEntityDescription | None:
271  """Get the sensor description for a measurement type."""
272  if measurement_position not in (
273  MeasurementPosition.TORSO,
274  MeasurementPosition.LEFT_ARM,
275  MeasurementPosition.RIGHT_ARM,
276  MeasurementPosition.LEFT_LEG,
277  MeasurementPosition.RIGHT_LEG,
278  ) or measurement_type not in (
279  MeasurementType.MUSCLE_MASS_FOR_SEGMENTS,
280  MeasurementType.FAT_FREE_MASS_FOR_SEGMENTS,
281  MeasurementType.FAT_MASS_FOR_SEGMENTS,
282  ):
283  return None
285  key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}",
286  measurement_type=measurement_type,
287  measurement_position=measurement_position,
288  translation_key=f"{measurement_type.name.lower()}_{measurement_position.name.lower()}",
289  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
290  suggested_display_precision=2,
291  device_class=SensorDeviceClass.WEIGHT,
292  state_class=SensorStateClass.MEASUREMENT,
293  entity_registry_enabled_default=False,
294  )
295 
296 
298  measurement: tuple[MeasurementType, MeasurementPosition | None],
299 ) -> WithingsMeasurementSensorEntityDescription | None:
300  """Get the sensor description for a measurement type."""
301  measurement_type, measurement_position = measurement
302  if measurement_position is not None:
304  measurement_type, measurement_position
305  )
306  return MEASUREMENT_SENSORS.get(measurement_type)
307 
308 
309 @dataclass(frozen=True, kw_only=True)
311  """Immutable class for describing withings data."""
312 
313  value_fn: Callable[[SleepSummary], StateType]
314 
315 
316 SLEEP_SENSORS = [
318  key="sleep_breathing_disturbances_intensity",
319  value_fn=lambda sleep_summary: sleep_summary.breathing_disturbances_intensity,
320  translation_key="breathing_disturbances_intensity",
321  state_class=SensorStateClass.MEASUREMENT,
322  entity_registry_enabled_default=False,
323  ),
325  key="sleep_deep_duration_seconds",
326  value_fn=lambda sleep_summary: sleep_summary.deep_sleep_duration,
327  translation_key="deep_sleep",
328  native_unit_of_measurement=UnitOfTime.SECONDS,
329  device_class=SensorDeviceClass.DURATION,
330  state_class=SensorStateClass.MEASUREMENT,
331  ),
333  key="sleep_tosleep_duration_seconds",
334  value_fn=lambda sleep_summary: sleep_summary.sleep_latency,
335  translation_key="time_to_sleep",
336  native_unit_of_measurement=UnitOfTime.SECONDS,
337  device_class=SensorDeviceClass.DURATION,
338  state_class=SensorStateClass.MEASUREMENT,
339  entity_registry_enabled_default=False,
340  ),
342  key="sleep_towakeup_duration_seconds",
343  value_fn=lambda sleep_summary: sleep_summary.wake_up_latency,
344  translation_key="time_to_wakeup",
345  native_unit_of_measurement=UnitOfTime.SECONDS,
346  device_class=SensorDeviceClass.DURATION,
347  state_class=SensorStateClass.MEASUREMENT,
348  entity_registry_enabled_default=False,
349  ),
351  key="sleep_heart_rate_average_bpm",
352  value_fn=lambda sleep_summary: sleep_summary.average_heart_rate,
353  translation_key="average_heart_rate",
354  native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
355  state_class=SensorStateClass.MEASUREMENT,
356  entity_registry_enabled_default=False,
357  ),
359  key="sleep_heart_rate_max_bpm",
360  value_fn=lambda sleep_summary: sleep_summary.max_heart_rate,
361  translation_key="maximum_heart_rate",
362  native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
363  state_class=SensorStateClass.MEASUREMENT,
364  entity_registry_enabled_default=False,
365  ),
367  key="sleep_heart_rate_min_bpm",
368  value_fn=lambda sleep_summary: sleep_summary.min_heart_rate,
369  translation_key="minimum_heart_rate",
370  native_unit_of_measurement=UOM_BEATS_PER_MINUTE,
371  state_class=SensorStateClass.MEASUREMENT,
372  entity_registry_enabled_default=False,
373  ),
375  key="sleep_light_duration_seconds",
376  value_fn=lambda sleep_summary: sleep_summary.light_sleep_duration,
377  translation_key="light_sleep",
378  native_unit_of_measurement=UnitOfTime.SECONDS,
379  device_class=SensorDeviceClass.DURATION,
380  state_class=SensorStateClass.MEASUREMENT,
381  entity_registry_enabled_default=False,
382  ),
384  key="sleep_rem_duration_seconds",
385  value_fn=lambda sleep_summary: sleep_summary.rem_sleep_duration,
386  translation_key="rem_sleep",
387  native_unit_of_measurement=UnitOfTime.SECONDS,
388  device_class=SensorDeviceClass.DURATION,
389  state_class=SensorStateClass.MEASUREMENT,
390  entity_registry_enabled_default=False,
391  ),
393  key="sleep_respiratory_average_bpm",
394  value_fn=lambda sleep_summary: sleep_summary.average_respiration_rate,
395  translation_key="average_respiratory_rate",
396  native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
397  state_class=SensorStateClass.MEASUREMENT,
398  entity_registry_enabled_default=False,
399  ),
401  key="sleep_respiratory_max_bpm",
402  value_fn=lambda sleep_summary: sleep_summary.max_respiration_rate,
403  translation_key="maximum_respiratory_rate",
404  native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
405  state_class=SensorStateClass.MEASUREMENT,
406  entity_registry_enabled_default=False,
407  ),
409  key="sleep_respiratory_min_bpm",
410  value_fn=lambda sleep_summary: sleep_summary.min_respiration_rate,
411  translation_key="minimum_respiratory_rate",
412  native_unit_of_measurement=UOM_BREATHS_PER_MINUTE,
413  state_class=SensorStateClass.MEASUREMENT,
414  entity_registry_enabled_default=False,
415  ),
417  key="sleep_score",
418  value_fn=lambda sleep_summary: sleep_summary.sleep_score,
419  translation_key="sleep_score",
420  native_unit_of_measurement=SCORE_POINTS,
421  state_class=SensorStateClass.MEASUREMENT,
422  entity_registry_enabled_default=False,
423  ),
425  key="sleep_snoring",
426  value_fn=lambda sleep_summary: sleep_summary.snoring,
427  translation_key="snoring",
428  state_class=SensorStateClass.MEASUREMENT,
429  entity_registry_enabled_default=False,
430  ),
432  key="sleep_snoring_eposode_count",
433  value_fn=lambda sleep_summary: sleep_summary.snoring_count,
434  translation_key="snoring_episode_count",
435  state_class=SensorStateClass.MEASUREMENT,
436  entity_registry_enabled_default=False,
437  ),
439  key="sleep_wakeup_count",
440  value_fn=lambda sleep_summary: sleep_summary.wake_up_count,
441  translation_key="wakeup_count",
442  native_unit_of_measurement=UOM_FREQUENCY,
443  state_class=SensorStateClass.MEASUREMENT,
444  entity_registry_enabled_default=False,
445  ),
447  key="sleep_wakeup_duration_seconds",
448  value_fn=lambda sleep_summary: sleep_summary.total_time_awake,
449  translation_key="wakeup_time",
450  native_unit_of_measurement=UnitOfTime.SECONDS,
451  device_class=SensorDeviceClass.DURATION,
452  state_class=SensorStateClass.MEASUREMENT,
453  entity_registry_enabled_default=False,
454  ),
455 ]
456 
457 
458 @dataclass(frozen=True, kw_only=True)
460  """Immutable class for describing withings data."""
461 
462  value_fn: Callable[[Activity], StateType]
463 
464 
465 ACTIVITY_SENSORS = [
467  key="activity_steps_today",
468  value_fn=lambda activity: activity.steps,
469  translation_key="activity_steps_today",
470  native_unit_of_measurement="steps",
471  state_class=SensorStateClass.TOTAL,
472  ),
474  key="activity_distance_today",
475  value_fn=lambda activity: activity.distance,
476  translation_key="activity_distance_today",
477  suggested_display_precision=0,
478  native_unit_of_measurement=UnitOfLength.METERS,
479  device_class=SensorDeviceClass.DISTANCE,
480  state_class=SensorStateClass.TOTAL,
481  ),
483  key="activity_floors_climbed_today",
484  value_fn=lambda activity: activity.elevation,
485  translation_key="activity_elevation_today",
486  native_unit_of_measurement=UnitOfLength.METERS,
487  device_class=SensorDeviceClass.DISTANCE,
488  state_class=SensorStateClass.TOTAL,
489  ),
491  key="activity_soft_duration_today",
492  value_fn=lambda activity: activity.soft_activity,
493  translation_key="activity_soft_duration_today",
494  native_unit_of_measurement=UnitOfTime.SECONDS,
495  suggested_unit_of_measurement=UnitOfTime.MINUTES,
496  device_class=SensorDeviceClass.DURATION,
497  state_class=SensorStateClass.TOTAL,
498  entity_registry_enabled_default=False,
499  ),
501  key="activity_moderate_duration_today",
502  value_fn=lambda activity: activity.moderate_activity,
503  translation_key="activity_moderate_duration_today",
504  native_unit_of_measurement=UnitOfTime.SECONDS,
505  suggested_unit_of_measurement=UnitOfTime.MINUTES,
506  device_class=SensorDeviceClass.DURATION,
507  state_class=SensorStateClass.TOTAL,
508  entity_registry_enabled_default=False,
509  ),
511  key="activity_intense_duration_today",
512  value_fn=lambda activity: activity.intense_activity,
513  translation_key="activity_intense_duration_today",
514  native_unit_of_measurement=UnitOfTime.SECONDS,
515  suggested_unit_of_measurement=UnitOfTime.MINUTES,
516  device_class=SensorDeviceClass.DURATION,
517  state_class=SensorStateClass.TOTAL,
518  entity_registry_enabled_default=False,
519  ),
521  key="activity_active_duration_today",
522  value_fn=lambda activity: activity.total_time_active,
523  translation_key="activity_active_duration_today",
524  native_unit_of_measurement=UnitOfTime.SECONDS,
525  suggested_unit_of_measurement=UnitOfTime.HOURS,
526  device_class=SensorDeviceClass.DURATION,
527  state_class=SensorStateClass.TOTAL,
528  ),
530  key="activity_active_calories_burnt_today",
531  value_fn=lambda activity: activity.active_calories_burnt,
532  suggested_display_precision=1,
533  translation_key="activity_active_calories_burnt_today",
534  native_unit_of_measurement="calories",
535  state_class=SensorStateClass.TOTAL,
536  ),
538  key="activity_total_calories_burnt_today",
539  value_fn=lambda activity: activity.total_calories_burnt,
540  suggested_display_precision=1,
541  translation_key="activity_total_calories_burnt_today",
542  native_unit_of_measurement="calories",
543  state_class=SensorStateClass.TOTAL,
544  ),
545 ]
546 
547 
548 STEP_GOAL = "steps"
549 SLEEP_GOAL = "sleep"
550 WEIGHT_GOAL = "weight"
551 
552 
553 @dataclass(frozen=True, kw_only=True)
555  """Immutable class for describing withings data."""
556 
557  value_fn: Callable[[Goals], StateType]
558 
559 
560 GOALS_SENSORS: dict[str, WithingsGoalsSensorEntityDescription] = {
562  key="step_goal",
563  value_fn=lambda goals: goals.steps,
564  translation_key="step_goal",
565  native_unit_of_measurement="steps",
566  state_class=SensorStateClass.MEASUREMENT,
567  ),
569  key="sleep_goal",
570  value_fn=lambda goals: goals.sleep,
571  translation_key="sleep_goal",
572  native_unit_of_measurement=UnitOfTime.SECONDS,
573  suggested_unit_of_measurement=UnitOfTime.HOURS,
574  device_class=SensorDeviceClass.DURATION,
575  state_class=SensorStateClass.MEASUREMENT,
576  ),
578  key="weight_goal",
579  value_fn=lambda goals: goals.weight,
580  translation_key="weight_goal",
581  native_unit_of_measurement=UnitOfMass.KILOGRAMS,
582  device_class=SensorDeviceClass.WEIGHT,
583  state_class=SensorStateClass.MEASUREMENT,
584  ),
585 }
586 
587 
588 @dataclass(frozen=True, kw_only=True)
590  """Immutable class for describing withings data."""
591 
592  value_fn: Callable[[Workout], StateType]
593 
594 
595 _WORKOUT_CATEGORY = [
596  workout_category.name.lower() for workout_category in WorkoutCategory
597 ]
598 
599 
600 WORKOUT_SENSORS = [
602  key="workout_type",
603  value_fn=lambda workout: workout.category.name.lower(),
604  device_class=SensorDeviceClass.ENUM,
605  translation_key="workout_type",
606  options=_WORKOUT_CATEGORY,
607  ),
609  key="workout_active_calories_burnt",
610  value_fn=lambda workout: workout.active_calories_burnt,
611  translation_key="workout_active_calories_burnt",
612  suggested_display_precision=1,
613  native_unit_of_measurement="calories",
614  ),
616  key="workout_distance",
617  value_fn=lambda workout: workout.distance,
618  translation_key="workout_distance",
619  device_class=SensorDeviceClass.DISTANCE,
620  native_unit_of_measurement=UnitOfLength.METERS,
621  suggested_display_precision=0,
622  ),
624  key="workout_floors_climbed",
625  value_fn=lambda workout: workout.elevation,
626  translation_key="workout_elevation",
627  native_unit_of_measurement=UnitOfLength.METERS,
628  device_class=SensorDeviceClass.DISTANCE,
629  ),
631  key="workout_intensity",
632  value_fn=lambda workout: workout.intensity,
633  translation_key="workout_intensity",
634  ),
636  key="workout_pause_duration",
637  value_fn=lambda workout: workout.pause_duration or 0,
638  translation_key="workout_pause_duration",
639  device_class=SensorDeviceClass.DURATION,
640  native_unit_of_measurement=UnitOfTime.SECONDS,
641  suggested_unit_of_measurement=UnitOfTime.MINUTES,
642  ),
644  key="workout_duration",
645  value_fn=lambda workout: (
646  workout.end_date - workout.start_date
647  ).total_seconds(),
648  translation_key="workout_duration",
649  device_class=SensorDeviceClass.DURATION,
650  native_unit_of_measurement=UnitOfTime.SECONDS,
651  suggested_unit_of_measurement=UnitOfTime.MINUTES,
652  ),
653 ]
654 
655 
656 @dataclass(frozen=True, kw_only=True)
658  """Immutable class for describing withings data."""
659 
660  value_fn: Callable[[Device], StateType]
661 
662 
663 DEVICE_SENSORS = [
665  key="battery",
666  translation_key="battery",
667  options=["low", "medium", "high"],
668  device_class=SensorDeviceClass.ENUM,
669  value_fn=lambda device: device.battery,
670  )
671 ]
672 
673 
674 def get_current_goals(goals: Goals) -> set[str]:
675  """Return a list of present goals."""
676  result = set()
677  for goal in (STEP_GOAL, SLEEP_GOAL, WEIGHT_GOAL):
678  if getattr(goals, goal):
679  result.add(goal)
680  return result
681 
682 
684  hass: HomeAssistant,
685  entry: WithingsConfigEntry,
686  async_add_entities: AddEntitiesCallback,
687 ) -> None:
688  """Set up the sensor config entry."""
689  ent_reg = er.async_get(hass)
690 
691  withings_data = entry.runtime_data
692 
693  measurement_coordinator = withings_data.measurement_coordinator
694 
695  entities: list[SensorEntity] = []
696  entities.extend(
697  WithingsMeasurementSensor(measurement_coordinator, description)
698  for measurement_type in measurement_coordinator.data
699  if (description := get_measurement_description(measurement_type)) is not None
700  )
701 
702  current_measurement_types = set(measurement_coordinator.data)
703 
704  def _async_measurement_listener() -> None:
705  """Listen for new measurements and add sensors if they did not exist."""
706  received_measurement_types = set(measurement_coordinator.data)
707  new_measurement_types = received_measurement_types - current_measurement_types
708  if new_measurement_types:
709  current_measurement_types.update(new_measurement_types)
711  WithingsMeasurementSensor(measurement_coordinator, description)
712  for measurement_type in new_measurement_types
713  if (description := get_measurement_description(measurement_type))
714  is not None
715  )
716 
717  measurement_coordinator.async_add_listener(_async_measurement_listener)
718 
719  goals_coordinator = withings_data.goals_coordinator
720 
721  current_goals = get_current_goals(goals_coordinator.data)
722 
723  entities.extend(
724  WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
725  for goal in current_goals
726  )
727 
728  def _async_goals_listener() -> None:
729  """Listen for new goals and add sensors if they did not exist."""
730  received_goals = get_current_goals(goals_coordinator.data)
731  new_goals = received_goals - current_goals
732  if new_goals:
733  current_goals.update(new_goals)
735  WithingsGoalsSensor(goals_coordinator, GOALS_SENSORS[goal])
736  for goal in new_goals
737  )
738 
739  goals_coordinator.async_add_listener(_async_goals_listener)
740 
741  activity_coordinator = withings_data.activity_coordinator
742 
743  activity_entities_setup_before = ent_reg.async_get_entity_id(
744  Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_activity_steps_today"
745  )
746 
747  if activity_coordinator.data is not None or activity_entities_setup_before:
748  entities.extend(
749  WithingsActivitySensor(activity_coordinator, attribute)
750  for attribute in ACTIVITY_SENSORS
751  )
752  else:
753  remove_activity_listener: Callable[[], None]
754 
755  def _async_add_activity_entities() -> None:
756  """Add activity entities."""
757  if activity_coordinator.data is not None:
759  WithingsActivitySensor(activity_coordinator, attribute)
760  for attribute in ACTIVITY_SENSORS
761  )
762  remove_activity_listener()
763 
764  remove_activity_listener = activity_coordinator.async_add_listener(
765  _async_add_activity_entities
766  )
767 
768  sleep_coordinator = withings_data.sleep_coordinator
769 
770  sleep_entities_setup_before = ent_reg.async_get_entity_id(
771  Platform.SENSOR,
772  DOMAIN,
773  f"withings_{entry.unique_id}_sleep_deep_duration_seconds",
774  )
775 
776  if sleep_coordinator.data is not None or sleep_entities_setup_before:
777  entities.extend(
778  WithingsSleepSensor(sleep_coordinator, attribute)
779  for attribute in SLEEP_SENSORS
780  )
781  else:
782  remove_sleep_listener: Callable[[], None]
783 
784  def _async_add_sleep_entities() -> None:
785  """Add sleep entities."""
786  if sleep_coordinator.data is not None:
788  WithingsSleepSensor(sleep_coordinator, attribute)
789  for attribute in SLEEP_SENSORS
790  )
791  remove_sleep_listener()
792 
793  remove_sleep_listener = sleep_coordinator.async_add_listener(
794  _async_add_sleep_entities
795  )
796 
797  workout_coordinator = withings_data.workout_coordinator
798 
799  workout_entities_setup_before = ent_reg.async_get_entity_id(
800  Platform.SENSOR, DOMAIN, f"withings_{entry.unique_id}_workout_type"
801  )
802 
803  if workout_coordinator.data is not None or workout_entities_setup_before:
804  entities.extend(
805  WithingsWorkoutSensor(workout_coordinator, attribute)
806  for attribute in WORKOUT_SENSORS
807  )
808  else:
809  remove_workout_listener: Callable[[], None]
810 
811  def _async_add_workout_entities() -> None:
812  """Add workout entities."""
813  if workout_coordinator.data is not None:
815  WithingsWorkoutSensor(workout_coordinator, attribute)
816  for attribute in WORKOUT_SENSORS
817  )
818  remove_workout_listener()
819 
820  remove_workout_listener = workout_coordinator.async_add_listener(
821  _async_add_workout_entities
822  )
823 
824  device_coordinator = withings_data.device_coordinator
825 
826  current_devices: set[str] = set()
827 
828  def _async_device_listener() -> None:
829  """Add device entities."""
830  received_devices = set(device_coordinator.data)
831  new_devices = received_devices - current_devices
832  old_devices = current_devices - received_devices
833  if new_devices:
834  device_registry = dr.async_get(hass)
835  for device_id in new_devices:
836  if device := device_registry.async_get_device({(DOMAIN, device_id)}):
837  if any(
838  (
839  config_entry := hass.config_entries.async_get_entry(
840  config_entry_id
841  )
842  )
843  and config_entry.state == ConfigEntryState.LOADED
844  for config_entry_id in device.config_entries
845  ):
846  continue
848  WithingsDeviceSensor(device_coordinator, description, device_id)
849  for description in DEVICE_SENSORS
850  )
851  current_devices.add(device_id)
852 
853  if old_devices:
854  device_registry = dr.async_get(hass)
855  for device_id in old_devices:
856  if device := device_registry.async_get_device({(DOMAIN, device_id)}):
857  device_registry.async_update_device(
858  device.id, remove_config_entry_id=entry.entry_id
859  )
860  current_devices.remove(device_id)
861 
862  device_coordinator.async_add_listener(_async_device_listener)
863 
864  _async_device_listener()
865 
866  if not entities:
867  LOGGER.warning(
868  "No data found for Withings entry %s, sensors will be added when new data is available",
869  entry.title,
870  )
871 
872  async_add_entities(entities)
873 
874 
876  _T: WithingsDataUpdateCoordinator[Any],
877  _ED: SensorEntityDescription,
878 ](WithingsEntity[_T], SensorEntity):
879  """Implementation of a Withings sensor."""
880 
881  entity_description: _ED
882 
883  def __init__(
884  self,
885  coordinator: _T,
886  entity_description: _ED,
887  ) -> None:
888  """Initialize sensor."""
889  super().__init__(coordinator, entity_description.key)
890  self.entity_description = entity_description
891 
892 
894  WithingsSensor[
895  WithingsMeasurementDataUpdateCoordinator,
896  WithingsMeasurementSensorEntityDescription,
897  ]
898 ):
899  """Implementation of a Withings measurement sensor."""
900 
901  @property
902  def native_value(self) -> float:
903  """Return the state of the entity."""
904  return self.coordinator.data[
905  (
906  self.entity_description.measurement_type,
907  self.entity_description.measurement_position,
908  )
909  ]
910 
911  @property
912  def available(self) -> bool:
913  """Return if the sensor is available."""
914  return (
915  super().available
916  and (
917  self.entity_description.measurement_type,
918  self.entity_description.measurement_position,
919  )
920  in self.coordinator.data
921  )
922 
923 
925  WithingsSensor[
926  WithingsSleepDataUpdateCoordinator,
927  WithingsSleepSensorEntityDescription,
928  ]
929 ):
930  """Implementation of a Withings sleep sensor."""
931 
932  @property
933  def native_value(self) -> StateType:
934  """Return the state of the entity."""
935  if not self.coordinator.data:
936  return None
937  return self.entity_description.value_fn(self.coordinator.data)
938 
939 
941  WithingsSensor[
942  WithingsGoalsDataUpdateCoordinator,
943  WithingsGoalsSensorEntityDescription,
944  ]
945 ):
946  """Implementation of a Withings goals sensor."""
947 
948  @property
949  def native_value(self) -> StateType:
950  """Return the state of the entity."""
951  assert self.coordinator.data
952  return self.entity_description.value_fn(self.coordinator.data)
953 
954 
956  WithingsSensor[
957  WithingsActivityDataUpdateCoordinator,
958  WithingsActivitySensorEntityDescription,
959  ]
960 ):
961  """Implementation of a Withings activity sensor."""
962 
963  @property
964  def native_value(self) -> StateType:
965  """Return the state of the entity."""
966  if not self.coordinator.data:
967  return None
968  return self.entity_description.value_fn(self.coordinator.data)
969 
970  @property
971  def last_reset(self) -> datetime:
972  """These values reset every day."""
973  return dt_util.start_of_local_day()
974 
975 
977  WithingsSensor[
978  WithingsWorkoutDataUpdateCoordinator,
979  WithingsWorkoutSensorEntityDescription,
980  ]
981 ):
982  """Implementation of a Withings workout sensor."""
983 
984  @property
985  def native_value(self) -> StateType:
986  """Return the state of the entity."""
987  if not self.coordinator.data:
988  return None
989  return self.entity_description.value_fn(self.coordinator.data)
990 
991 
993  """Implementation of a Withings workout sensor."""
994 
995  entity_description: WithingsDeviceSensorEntityDescription
996 
997  def __init__(
998  self,
999  coordinator: WithingsDeviceDataUpdateCoordinator,
1000  entity_description: WithingsDeviceSensorEntityDescription,
1001  device_id: str,
1002  ) -> None:
1003  """Initialize sensor."""
1004  super().__init__(coordinator, device_id, entity_description.key)
1005  self.entity_descriptionentity_description = entity_description
1006 
1007  @property
1008  def native_value(self) -> StateType:
1009  """Return the state of the entity."""
1010  return self.entity_descriptionentity_description.value_fn(self.devicedevice)
None __init__(self, WithingsDeviceDataUpdateCoordinator coordinator, WithingsDeviceSensorEntityDescription entity_description, str device_id)
Definition: sensor.py:1002
None async_setup_entry(HomeAssistant hass, WithingsConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:687
WithingsMeasurementSensorEntityDescription|None get_measurement_description(tuple[MeasurementType, MeasurementPosition|None] measurement)
Definition: sensor.py:299
set[str] get_current_goals(Goals goals)
Definition: sensor.py:674
None __init__(self, _T coordinator, _ED entity_description)
Definition: sensor.py:887
WithingsMeasurementSensorEntityDescription|None get_positional_measurement_description(MeasurementType measurement_type, MeasurementPosition measurement_position)
Definition: sensor.py:270