Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for the Fitbit API."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import datetime
8 import logging
9 from typing import Any, Final, cast
10 
12  SensorDeviceClass,
13  SensorEntity,
14  SensorEntityDescription,
15  SensorStateClass,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import (
19  PERCENTAGE,
20  EntityCategory,
21  UnitOfLength,
22  UnitOfMass,
23  UnitOfTime,
24  UnitOfVolume,
25 )
26 from homeassistant.core import HomeAssistant, callback
27 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 from homeassistant.helpers.icon import icon_for_battery_level
30 from homeassistant.helpers.update_coordinator import CoordinatorEntity
31 
32 from .api import FitbitApi
33 from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
34 from .coordinator import FitbitData, FitbitDeviceCoordinator
35 from .exceptions import FitbitApiException, FitbitAuthException
36 from .model import FitbitDevice, config_from_entry_data
37 
38 _LOGGER: Final = logging.getLogger(__name__)
39 
40 _CONFIGURING: dict[str, str] = {}
41 
42 SCAN_INTERVAL: Final = datetime.timedelta(minutes=30)
43 
44 FITBIT_TRACKER_SUBSTRING = "/tracker/"
45 
46 
47 def _default_value_fn(result: dict[str, Any]) -> str:
48  """Parse a Fitbit timeseries API responses."""
49  return cast(str, result["value"])
50 
51 
52 def _distance_value_fn(result: dict[str, Any]) -> int | str:
53  """Format function for distance values."""
54  return format(float(_default_value_fn(result)), ".2f")
55 
56 
57 def _body_value_fn(result: dict[str, Any]) -> int | str:
58  """Format function for body values."""
59  return format(float(_default_value_fn(result)), ".1f")
60 
61 
62 def _clock_format_12h(result: dict[str, Any]) -> str:
63  raw_state = result["value"]
64  if raw_state == "":
65  return "-"
66  hours_str, minutes_str = raw_state.split(":")
67  hours, minutes = int(hours_str), int(minutes_str)
68  setting = "AM"
69  if hours > 12:
70  setting = "PM"
71  hours -= 12
72  elif hours == 0:
73  hours = 12
74  return f"{hours}:{minutes:02d} {setting}"
75 
76 
77 def _weight_unit(unit_system: FitbitUnitSystem) -> UnitOfMass:
78  """Determine the weight unit."""
79  if unit_system == FitbitUnitSystem.EN_US:
80  return UnitOfMass.POUNDS
81  if unit_system == FitbitUnitSystem.EN_GB:
82  return UnitOfMass.STONES
83  return UnitOfMass.KILOGRAMS
84 
85 
86 def _distance_unit(unit_system: FitbitUnitSystem) -> UnitOfLength:
87  """Determine the distance unit."""
88  if unit_system == FitbitUnitSystem.EN_US:
89  return UnitOfLength.MILES
90  return UnitOfLength.KILOMETERS
91 
92 
93 def _elevation_unit(unit_system: FitbitUnitSystem) -> UnitOfLength:
94  """Determine the elevation unit."""
95  if unit_system == FitbitUnitSystem.EN_US:
96  return UnitOfLength.FEET
97  return UnitOfLength.METERS
98 
99 
100 def _water_unit(unit_system: FitbitUnitSystem) -> UnitOfVolume:
101  """Determine the water unit."""
102  if unit_system == FitbitUnitSystem.EN_US:
103  return UnitOfVolume.FLUID_OUNCES
104  return UnitOfVolume.MILLILITERS
105 
106 
107 def _int_value_or_none(field: str) -> Callable[[dict[str, Any]], int | None]:
108  """Value function that will parse the specified field if present."""
109 
110  def convert(result: dict[str, Any]) -> int | None:
111  if (value := result["value"].get(field)) is not None:
112  return int(value)
113  return None
114 
115  return convert
116 
117 
118 @dataclass(frozen=True)
120  """Describes Fitbit sensor entity."""
121 
122  unit_type: str | None = None
123  value_fn: Callable[[dict[str, Any]], Any] = _default_value_fn
124  unit_fn: Callable[[FitbitUnitSystem], str | None] = lambda x: None
125  scope: FitbitScope | None = None
126 
127  @property
128  def is_tracker(self) -> bool:
129  """Return if the entity is a tracker."""
130  return FITBIT_TRACKER_SUBSTRING in self.key
131 
132 
134  config_entry: ConfigEntry, entity_description: FitbitSensorEntityDescription
135 ) -> DeviceInfo:
136  """Build device info for sensor entities info across devices."""
137  unique_id = cast(str, config_entry.unique_id)
138  if entity_description.is_tracker:
139  return DeviceInfo(
140  entry_type=DeviceEntryType.SERVICE,
141  identifiers={(DOMAIN, f"{unique_id}_tracker")},
142  translation_key="tracker",
143  translation_placeholders={"display_name": config_entry.title},
144  )
145  return DeviceInfo(
146  entry_type=DeviceEntryType.SERVICE,
147  identifiers={(DOMAIN, unique_id)},
148  )
149 
150 
151 FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
153  key="activities/activityCalories",
154  translation_key="activity_calories",
155  native_unit_of_measurement="cal",
156  icon="mdi:fire",
157  scope=FitbitScope.ACTIVITY,
158  state_class=SensorStateClass.TOTAL_INCREASING,
159  entity_category=EntityCategory.DIAGNOSTIC,
160  ),
162  key="activities/calories",
163  translation_key="calories",
164  native_unit_of_measurement="cal",
165  icon="mdi:fire",
166  scope=FitbitScope.ACTIVITY,
167  state_class=SensorStateClass.TOTAL_INCREASING,
168  ),
170  key="activities/caloriesBMR",
171  translation_key="calories_bmr",
172  native_unit_of_measurement="cal",
173  icon="mdi:fire",
174  scope=FitbitScope.ACTIVITY,
175  entity_registry_enabled_default=False,
176  state_class=SensorStateClass.TOTAL_INCREASING,
177  entity_category=EntityCategory.DIAGNOSTIC,
178  ),
180  key="activities/distance",
181  icon="mdi:map-marker",
182  device_class=SensorDeviceClass.DISTANCE,
183  value_fn=_distance_value_fn,
184  unit_fn=_distance_unit,
185  scope=FitbitScope.ACTIVITY,
186  state_class=SensorStateClass.TOTAL_INCREASING,
187  ),
189  key="activities/elevation",
190  translation_key="elevation",
191  icon="mdi:walk",
192  device_class=SensorDeviceClass.DISTANCE,
193  unit_fn=_elevation_unit,
194  scope=FitbitScope.ACTIVITY,
195  state_class=SensorStateClass.MEASUREMENT,
196  entity_category=EntityCategory.DIAGNOSTIC,
197  ),
199  key="activities/floors",
200  translation_key="floors",
201  native_unit_of_measurement="floors",
202  icon="mdi:walk",
203  scope=FitbitScope.ACTIVITY,
204  state_class=SensorStateClass.TOTAL_INCREASING,
205  entity_category=EntityCategory.DIAGNOSTIC,
206  ),
208  key="activities/heart",
209  translation_key="resting_heart_rate",
210  native_unit_of_measurement="bpm",
211  icon="mdi:heart-pulse",
212  value_fn=_int_value_or_none("restingHeartRate"),
213  scope=FitbitScope.HEART_RATE,
214  state_class=SensorStateClass.MEASUREMENT,
215  ),
217  key="activities/minutesFairlyActive",
218  translation_key="minutes_fairly_active",
219  native_unit_of_measurement=UnitOfTime.MINUTES,
220  icon="mdi:walk",
221  device_class=SensorDeviceClass.DURATION,
222  scope=FitbitScope.ACTIVITY,
223  state_class=SensorStateClass.MEASUREMENT,
224  entity_category=EntityCategory.DIAGNOSTIC,
225  ),
227  key="activities/minutesLightlyActive",
228  translation_key="minutes_lightly_active",
229  native_unit_of_measurement=UnitOfTime.MINUTES,
230  icon="mdi:walk",
231  device_class=SensorDeviceClass.DURATION,
232  scope=FitbitScope.ACTIVITY,
233  state_class=SensorStateClass.MEASUREMENT,
234  entity_category=EntityCategory.DIAGNOSTIC,
235  ),
237  key="activities/minutesSedentary",
238  translation_key="minutes_sedentary",
239  native_unit_of_measurement=UnitOfTime.MINUTES,
240  icon="mdi:seat-recline-normal",
241  device_class=SensorDeviceClass.DURATION,
242  scope=FitbitScope.ACTIVITY,
243  state_class=SensorStateClass.MEASUREMENT,
244  entity_category=EntityCategory.DIAGNOSTIC,
245  ),
247  key="activities/minutesVeryActive",
248  translation_key="minutes_very_active",
249  native_unit_of_measurement=UnitOfTime.MINUTES,
250  icon="mdi:run",
251  device_class=SensorDeviceClass.DURATION,
252  scope=FitbitScope.ACTIVITY,
253  state_class=SensorStateClass.MEASUREMENT,
254  entity_category=EntityCategory.DIAGNOSTIC,
255  ),
257  key="activities/steps",
258  translation_key="steps",
259  native_unit_of_measurement="steps",
260  icon="mdi:walk",
261  scope=FitbitScope.ACTIVITY,
262  state_class=SensorStateClass.TOTAL_INCREASING,
263  ),
265  key="activities/tracker/activityCalories",
266  translation_key="activity_calories",
267  native_unit_of_measurement="cal",
268  icon="mdi:fire",
269  scope=FitbitScope.ACTIVITY,
270  entity_registry_enabled_default=False,
271  state_class=SensorStateClass.TOTAL_INCREASING,
272  entity_category=EntityCategory.DIAGNOSTIC,
273  ),
275  key="activities/tracker/calories",
276  translation_key="calories",
277  native_unit_of_measurement="cal",
278  icon="mdi:fire",
279  scope=FitbitScope.ACTIVITY,
280  entity_registry_enabled_default=False,
281  state_class=SensorStateClass.TOTAL_INCREASING,
282  entity_category=EntityCategory.DIAGNOSTIC,
283  ),
285  key="activities/tracker/distance",
286  icon="mdi:map-marker",
287  device_class=SensorDeviceClass.DISTANCE,
288  value_fn=_distance_value_fn,
289  unit_fn=_distance_unit,
290  scope=FitbitScope.ACTIVITY,
291  entity_registry_enabled_default=False,
292  state_class=SensorStateClass.TOTAL_INCREASING,
293  entity_category=EntityCategory.DIAGNOSTIC,
294  ),
296  key="activities/tracker/elevation",
297  translation_key="elevation",
298  icon="mdi:walk",
299  device_class=SensorDeviceClass.DISTANCE,
300  unit_fn=_elevation_unit,
301  scope=FitbitScope.ACTIVITY,
302  entity_registry_enabled_default=False,
303  state_class=SensorStateClass.MEASUREMENT,
304  entity_category=EntityCategory.DIAGNOSTIC,
305  ),
307  key="activities/tracker/floors",
308  translation_key="floors",
309  native_unit_of_measurement="floors",
310  icon="mdi:walk",
311  scope=FitbitScope.ACTIVITY,
312  entity_registry_enabled_default=False,
313  state_class=SensorStateClass.TOTAL_INCREASING,
314  entity_category=EntityCategory.DIAGNOSTIC,
315  ),
317  key="activities/tracker/minutesFairlyActive",
318  translation_key="minutes_fairly_active",
319  native_unit_of_measurement=UnitOfTime.MINUTES,
320  icon="mdi:walk",
321  device_class=SensorDeviceClass.DURATION,
322  scope=FitbitScope.ACTIVITY,
323  entity_registry_enabled_default=False,
324  state_class=SensorStateClass.TOTAL_INCREASING,
325  entity_category=EntityCategory.DIAGNOSTIC,
326  ),
328  key="activities/tracker/minutesLightlyActive",
329  translation_key="minutes_lightly_active",
330  native_unit_of_measurement=UnitOfTime.MINUTES,
331  icon="mdi:walk",
332  device_class=SensorDeviceClass.DURATION,
333  scope=FitbitScope.ACTIVITY,
334  entity_registry_enabled_default=False,
335  state_class=SensorStateClass.TOTAL_INCREASING,
336  entity_category=EntityCategory.DIAGNOSTIC,
337  ),
339  key="activities/tracker/minutesSedentary",
340  translation_key="minutes_sedentary",
341  native_unit_of_measurement=UnitOfTime.MINUTES,
342  icon="mdi:seat-recline-normal",
343  device_class=SensorDeviceClass.DURATION,
344  scope=FitbitScope.ACTIVITY,
345  entity_registry_enabled_default=False,
346  state_class=SensorStateClass.TOTAL_INCREASING,
347  entity_category=EntityCategory.DIAGNOSTIC,
348  ),
350  key="activities/tracker/minutesVeryActive",
351  translation_key="minutes_very_active",
352  native_unit_of_measurement=UnitOfTime.MINUTES,
353  icon="mdi:run",
354  device_class=SensorDeviceClass.DURATION,
355  scope=FitbitScope.ACTIVITY,
356  entity_registry_enabled_default=False,
357  state_class=SensorStateClass.TOTAL_INCREASING,
358  entity_category=EntityCategory.DIAGNOSTIC,
359  ),
361  key="activities/tracker/steps",
362  translation_key="steps",
363  native_unit_of_measurement="steps",
364  icon="mdi:walk",
365  scope=FitbitScope.ACTIVITY,
366  entity_registry_enabled_default=False,
367  state_class=SensorStateClass.TOTAL_INCREASING,
368  entity_category=EntityCategory.DIAGNOSTIC,
369  ),
371  key="body/bmi",
372  translation_key="bmi",
373  native_unit_of_measurement="BMI",
374  icon="mdi:human",
375  state_class=SensorStateClass.MEASUREMENT,
376  value_fn=_body_value_fn,
377  scope=FitbitScope.WEIGHT,
378  entity_registry_enabled_default=False,
379  entity_category=EntityCategory.DIAGNOSTIC,
380  ),
382  key="body/fat",
383  translation_key="body_fat",
384  native_unit_of_measurement=PERCENTAGE,
385  icon="mdi:human",
386  state_class=SensorStateClass.MEASUREMENT,
387  value_fn=_body_value_fn,
388  scope=FitbitScope.WEIGHT,
389  entity_registry_enabled_default=False,
390  entity_category=EntityCategory.DIAGNOSTIC,
391  ),
393  key="body/weight",
394  icon="mdi:human",
395  state_class=SensorStateClass.MEASUREMENT,
396  device_class=SensorDeviceClass.WEIGHT,
397  value_fn=_body_value_fn,
398  unit_fn=_weight_unit,
399  scope=FitbitScope.WEIGHT,
400  ),
402  key="sleep/awakeningsCount",
403  translation_key="awakenings_count",
404  native_unit_of_measurement="times awaken",
405  icon="mdi:sleep",
406  scope=FitbitScope.SLEEP,
407  state_class=SensorStateClass.TOTAL_INCREASING,
408  entity_category=EntityCategory.DIAGNOSTIC,
409  ),
411  key="sleep/efficiency",
412  translation_key="sleep_efficiency",
413  native_unit_of_measurement=PERCENTAGE,
414  icon="mdi:sleep",
415  state_class=SensorStateClass.MEASUREMENT,
416  scope=FitbitScope.SLEEP,
417  entity_category=EntityCategory.DIAGNOSTIC,
418  ),
420  key="sleep/minutesAfterWakeup",
421  translation_key="minutes_after_wakeup",
422  native_unit_of_measurement=UnitOfTime.MINUTES,
423  icon="mdi:sleep",
424  device_class=SensorDeviceClass.DURATION,
425  scope=FitbitScope.SLEEP,
426  state_class=SensorStateClass.MEASUREMENT,
427  entity_category=EntityCategory.DIAGNOSTIC,
428  ),
430  key="sleep/minutesAsleep",
431  translation_key="sleep_minutes_asleep",
432  native_unit_of_measurement=UnitOfTime.MINUTES,
433  icon="mdi:sleep",
434  device_class=SensorDeviceClass.DURATION,
435  scope=FitbitScope.SLEEP,
436  state_class=SensorStateClass.TOTAL_INCREASING,
437  entity_category=EntityCategory.DIAGNOSTIC,
438  ),
440  key="sleep/minutesAwake",
441  translation_key="sleep_minutes_awake",
442  native_unit_of_measurement=UnitOfTime.MINUTES,
443  icon="mdi:sleep",
444  device_class=SensorDeviceClass.DURATION,
445  scope=FitbitScope.SLEEP,
446  state_class=SensorStateClass.TOTAL_INCREASING,
447  entity_category=EntityCategory.DIAGNOSTIC,
448  ),
450  key="sleep/minutesToFallAsleep",
451  translation_key="sleep_minutes_to_fall_asleep",
452  native_unit_of_measurement=UnitOfTime.MINUTES,
453  icon="mdi:sleep",
454  device_class=SensorDeviceClass.DURATION,
455  scope=FitbitScope.SLEEP,
456  state_class=SensorStateClass.TOTAL_INCREASING,
457  entity_category=EntityCategory.DIAGNOSTIC,
458  ),
460  key="sleep/timeInBed",
461  translation_key="sleep_time_in_bed",
462  native_unit_of_measurement=UnitOfTime.MINUTES,
463  icon="mdi:hotel",
464  device_class=SensorDeviceClass.DURATION,
465  scope=FitbitScope.SLEEP,
466  state_class=SensorStateClass.TOTAL_INCREASING,
467  entity_category=EntityCategory.DIAGNOSTIC,
468  ),
470  key="foods/log/caloriesIn",
471  translation_key="calories_in",
472  native_unit_of_measurement="cal",
473  icon="mdi:food-apple",
474  state_class=SensorStateClass.TOTAL_INCREASING,
475  scope=FitbitScope.NUTRITION,
476  entity_category=EntityCategory.DIAGNOSTIC,
477  ),
479  key="foods/log/water",
480  translation_key="water",
481  icon="mdi:cup-water",
482  unit_fn=_water_unit,
483  state_class=SensorStateClass.TOTAL_INCREASING,
484  scope=FitbitScope.NUTRITION,
485  entity_category=EntityCategory.DIAGNOSTIC,
486  ),
487 )
488 
489 # Different description depending on clock format
490 SLEEP_START_TIME = FitbitSensorEntityDescription(
491  key="sleep/startTime",
492  translation_key="sleep_start_time",
493  icon="mdi:clock",
494  scope=FitbitScope.SLEEP,
495  entity_category=EntityCategory.DIAGNOSTIC,
496 )
497 SLEEP_START_TIME_12HR = FitbitSensorEntityDescription(
498  key="sleep/startTime",
499  translation_key="sleep_start_time",
500  icon="mdi:clock",
501  value_fn=_clock_format_12h,
502  scope=FitbitScope.SLEEP,
503  entity_category=EntityCategory.DIAGNOSTIC,
504 )
505 
506 FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
507  key="devices/battery",
508  translation_key="battery",
509  icon="mdi:battery",
510  scope=FitbitScope.DEVICE,
511  entity_category=EntityCategory.DIAGNOSTIC,
512  has_entity_name=True,
513 )
514 FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
515  key="devices/battery_level",
516  translation_key="battery_level",
517  scope=FitbitScope.DEVICE,
518  entity_category=EntityCategory.DIAGNOSTIC,
519  has_entity_name=True,
520  device_class=SensorDeviceClass.BATTERY,
521  native_unit_of_measurement=PERCENTAGE,
522 )
523 
524 
526  hass: HomeAssistant,
527  entry: ConfigEntry,
528  async_add_entities: AddEntitiesCallback,
529 ) -> None:
530  """Set up the Fitbit sensor platform."""
531 
532  data: FitbitData = hass.data[DOMAIN][entry.entry_id]
533  api = data.api
534 
535  # These are run serially to reuse the cached user profile, not gathered
536  # to avoid two racing requests.
537  user_profile = await api.async_get_user_profile()
538  unit_system = await api.async_get_unit_system()
539 
540  fitbit_config = config_from_entry_data(entry.data)
541 
542  def is_explicit_enable(description: FitbitSensorEntityDescription) -> bool:
543  """Determine if entity is enabled by default."""
544  return fitbit_config.is_explicit_enable(description.key)
545 
546  def is_allowed_resource(description: FitbitSensorEntityDescription) -> bool:
547  """Determine if an entity is allowed to be created."""
548  return fitbit_config.is_allowed_resource(description.scope, description.key)
549 
550  resource_list = [
551  *FITBIT_RESOURCES_LIST,
552  SLEEP_START_TIME_12HR
553  if fitbit_config.clock_format == "12H"
554  else SLEEP_START_TIME,
555  ]
556 
557  entities = [
558  FitbitSensor(
559  entry,
560  api,
561  user_profile.encoded_id,
562  description,
563  units=description.unit_fn(unit_system),
564  enable_default_override=is_explicit_enable(description),
565  device_info=_build_device_info(entry, description),
566  )
567  for description in resource_list
568  if is_allowed_resource(description)
569  ]
570  async_add_entities(entities)
571 
572  if data.device_coordinator and is_allowed_resource(FITBIT_RESOURCE_BATTERY):
573  battery_entities: list[SensorEntity] = [
575  data.device_coordinator,
576  user_profile.encoded_id,
577  FITBIT_RESOURCE_BATTERY,
578  device=device,
579  enable_default_override=is_explicit_enable(FITBIT_RESOURCE_BATTERY),
580  )
581  for device in data.device_coordinator.data.values()
582  ]
583  battery_entities.extend(
585  data.device_coordinator,
586  user_profile.encoded_id,
587  FITBIT_RESOURCE_BATTERY_LEVEL,
588  device=device,
589  )
590  for device in data.device_coordinator.data.values()
591  )
592  async_add_entities(battery_entities)
593 
594 
596  """Implementation of a Fitbit sensor."""
597 
598  entity_description: FitbitSensorEntityDescription
599  _attr_attribution = ATTRIBUTION
600  _attr_has_entity_name = True
601 
602  def __init__(
603  self,
604  config_entry: ConfigEntry,
605  api: FitbitApi,
606  user_profile_id: str,
607  description: FitbitSensorEntityDescription,
608  units: str | None,
609  enable_default_override: bool,
610  device_info: DeviceInfo,
611  ) -> None:
612  """Initialize the Fitbit sensor."""
613  self.config_entryconfig_entry = config_entry
614  self.entity_descriptionentity_description = description
615  self.apiapi = api
616 
617  self._attr_unique_id_attr_unique_id = f"{user_profile_id}_{description.key}"
618  self._attr_device_info_attr_device_info = device_info
619 
620  if units is not None:
621  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = units
622 
623  if enable_default_override:
624  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = True
625 
626  async def async_update(self) -> None:
627  """Get the latest data from the Fitbit API and update the states."""
628  try:
629  result = await self.apiapi.async_get_latest_time_series(
630  self.entity_descriptionentity_description.key
631  )
632  except FitbitAuthException:
633  self._attr_available_attr_available = False
634  self.config_entryconfig_entry.async_start_reauth(self.hasshass)
635  except FitbitApiException:
636  self._attr_available_attr_available = False
637  else:
638  self._attr_available_attr_available = True
639  self._attr_native_value_attr_native_value = self.entity_descriptionentity_description.value_fn(result)
640 
641  async def async_added_to_hass(self) -> None:
642  """When entity is added to hass."""
643  await super().async_added_to_hass()
644 
645  # We do not ask for an update with async_add_entities()
646  # because it will update disabled entities.
647  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(force_refresh=True)
648 
649 
650 class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity):
651  """Implementation of a Fitbit battery sensor."""
652 
653  entity_description: FitbitSensorEntityDescription
654  _attr_attribution = ATTRIBUTION
655 
656  def __init__(
657  self,
658  coordinator: FitbitDeviceCoordinator,
659  user_profile_id: str,
660  description: FitbitSensorEntityDescription,
661  device: FitbitDevice,
662  enable_default_override: bool,
663  ) -> None:
664  """Initialize the Fitbit sensor."""
665  super().__init__(coordinator)
666  self.entity_descriptionentity_description = description
667  self.devicedevice = device
668  self._attr_unique_id_attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
669  self._attr_device_info_attr_device_info = DeviceInfo(
670  identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
671  name=device.device_version,
672  model=device.device_version,
673  )
674 
675  if enable_default_override:
676  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = True
677 
678  @property
679  def icon(self) -> str | None:
680  """Icon to use in the frontend, if any."""
681  if battery_level := BATTERY_LEVELS.get(self.devicedevice.battery):
682  return icon_for_battery_level(battery_level=battery_level)
683  return self.entity_descriptionentity_description.icon
684 
685  @property
686  def extra_state_attributes(self) -> dict[str, str | None]:
687  """Return the state attributes."""
688  return {
689  "model": self.devicedevice.device_version,
690  "type": self.devicedevice.type.lower() if self.devicedevice.type is not None else None,
691  }
692 
693  async def async_added_to_hass(self) -> None:
694  """When entity is added to hass update state from existing coordinator data."""
695  await super().async_added_to_hass()
696  self._handle_coordinator_update_handle_coordinator_update()
697 
698  @callback
699  def _handle_coordinator_update(self) -> None:
700  """Handle updated data from the coordinator."""
701  self.devicedevice = self.coordinator.data[self.devicedevice.id]
702  self._attr_native_value_attr_native_value = self.devicedevice.battery
703  self.async_write_ha_stateasync_write_ha_state()
704 
705 
707  CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity
708 ):
709  """Implementation of a Fitbit battery level sensor."""
710 
711  entity_description: FitbitSensorEntityDescription
712  _attr_attribution = ATTRIBUTION
713 
714  def __init__(
715  self,
716  coordinator: FitbitDeviceCoordinator,
717  user_profile_id: str,
718  description: FitbitSensorEntityDescription,
719  device: FitbitDevice,
720  ) -> None:
721  """Initialize the Fitbit sensor."""
722  super().__init__(coordinator)
723  self.entity_descriptionentity_description = description
724  self.devicedevice = device
725  self._attr_unique_id_attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
726  self._attr_device_info_attr_device_info = DeviceInfo(
727  identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
728  name=device.device_version,
729  model=device.device_version,
730  )
731 
732  async def async_added_to_hass(self) -> None:
733  """When entity is added to hass update state from existing coordinator data."""
734  await super().async_added_to_hass()
735  self._handle_coordinator_update_handle_coordinator_update()
736 
737  @callback
738  def _handle_coordinator_update(self) -> None:
739  """Handle updated data from the coordinator."""
740  self.devicedevice = self.coordinator.data[self.devicedevice.id]
741  self._attr_native_value_attr_native_value = self.devicedevice.battery_level
742  self.async_write_ha_stateasync_write_ha_state()
None __init__(self, FitbitDeviceCoordinator coordinator, str user_profile_id, FitbitSensorEntityDescription description, FitbitDevice device)
Definition: sensor.py:720
None __init__(self, FitbitDeviceCoordinator coordinator, str user_profile_id, FitbitSensorEntityDescription description, FitbitDevice device, bool enable_default_override)
Definition: sensor.py:663
None __init__(self, ConfigEntry config_entry, FitbitApi api, str user_profile_id, FitbitSensorEntityDescription description, str|None units, bool enable_default_override, DeviceInfo device_info)
Definition: sensor.py:611
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
FitbitConfig config_from_entry_data(Mapping[str, Any] data)
Definition: model.py:65
UnitOfVolume _water_unit(FitbitUnitSystem unit_system)
Definition: sensor.py:100
int|str _distance_value_fn(dict[str, Any] result)
Definition: sensor.py:52
DeviceInfo _build_device_info(ConfigEntry config_entry, FitbitSensorEntityDescription entity_description)
Definition: sensor.py:135
UnitOfMass _weight_unit(FitbitUnitSystem unit_system)
Definition: sensor.py:77
str _clock_format_12h(dict[str, Any] result)
Definition: sensor.py:62
int|str _body_value_fn(dict[str, Any] result)
Definition: sensor.py:57
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:529
str _default_value_fn(dict[str, Any] result)
Definition: sensor.py:47
UnitOfLength _elevation_unit(FitbitUnitSystem unit_system)
Definition: sensor.py:93
Callable[[dict[str, Any]], int|None] _int_value_or_none(str field)
Definition: sensor.py:107
UnitOfLength _distance_unit(FitbitUnitSystem unit_system)
Definition: sensor.py:86
str icon_for_battery_level(int|None battery_level=None, bool charging=False)
Definition: icon.py:169