1 """Support for the Fitbit API."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from dataclasses
import dataclass
9 from typing
import Any, Final, cast
14 SensorEntityDescription,
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
38 _LOGGER: Final = logging.getLogger(__name__)
40 _CONFIGURING: dict[str, str] = {}
42 SCAN_INTERVAL: Final = datetime.timedelta(minutes=30)
44 FITBIT_TRACKER_SUBSTRING =
"/tracker/"
48 """Parse a Fitbit timeseries API responses."""
49 return cast(str, result[
"value"])
53 """Format function for distance values."""
58 """Format function for body values."""
63 raw_state = result[
"value"]
66 hours_str, minutes_str = raw_state.split(
":")
67 hours, minutes =
int(hours_str),
int(minutes_str)
74 return f
"{hours}:{minutes:02d} {setting}"
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
87 """Determine the distance unit."""
88 if unit_system == FitbitUnitSystem.EN_US:
89 return UnitOfLength.MILES
90 return UnitOfLength.KILOMETERS
94 """Determine the elevation unit."""
95 if unit_system == FitbitUnitSystem.EN_US:
96 return UnitOfLength.FEET
97 return UnitOfLength.METERS
101 """Determine the water unit."""
102 if unit_system == FitbitUnitSystem.EN_US:
103 return UnitOfVolume.FLUID_OUNCES
104 return UnitOfVolume.MILLILITERS
108 """Value function that will parse the specified field if present."""
110 def convert(result: dict[str, Any]) -> int |
None:
111 if (value := result[
"value"].
get(field))
is not None:
118 @dataclass(frozen=True)
120 """Describes Fitbit sensor entity."""
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
129 """Return if the entity is a tracker."""
130 return FITBIT_TRACKER_SUBSTRING
in self.key
134 config_entry: ConfigEntry, entity_description: FitbitSensorEntityDescription
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:
140 entry_type=DeviceEntryType.SERVICE,
141 identifiers={(DOMAIN, f
"{unique_id}_tracker")},
142 translation_key=
"tracker",
143 translation_placeholders={
"display_name": config_entry.title},
146 entry_type=DeviceEntryType.SERVICE,
147 identifiers={(DOMAIN, unique_id)},
151 FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
153 key=
"activities/activityCalories",
154 translation_key=
"activity_calories",
155 native_unit_of_measurement=
"cal",
157 scope=FitbitScope.ACTIVITY,
158 state_class=SensorStateClass.TOTAL_INCREASING,
159 entity_category=EntityCategory.DIAGNOSTIC,
162 key=
"activities/calories",
163 translation_key=
"calories",
164 native_unit_of_measurement=
"cal",
166 scope=FitbitScope.ACTIVITY,
167 state_class=SensorStateClass.TOTAL_INCREASING,
170 key=
"activities/caloriesBMR",
171 translation_key=
"calories_bmr",
172 native_unit_of_measurement=
"cal",
174 scope=FitbitScope.ACTIVITY,
175 entity_registry_enabled_default=
False,
176 state_class=SensorStateClass.TOTAL_INCREASING,
177 entity_category=EntityCategory.DIAGNOSTIC,
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,
189 key=
"activities/elevation",
190 translation_key=
"elevation",
192 device_class=SensorDeviceClass.DISTANCE,
193 unit_fn=_elevation_unit,
194 scope=FitbitScope.ACTIVITY,
195 state_class=SensorStateClass.MEASUREMENT,
196 entity_category=EntityCategory.DIAGNOSTIC,
199 key=
"activities/floors",
200 translation_key=
"floors",
201 native_unit_of_measurement=
"floors",
203 scope=FitbitScope.ACTIVITY,
204 state_class=SensorStateClass.TOTAL_INCREASING,
205 entity_category=EntityCategory.DIAGNOSTIC,
208 key=
"activities/heart",
209 translation_key=
"resting_heart_rate",
210 native_unit_of_measurement=
"bpm",
211 icon=
"mdi:heart-pulse",
213 scope=FitbitScope.HEART_RATE,
214 state_class=SensorStateClass.MEASUREMENT,
217 key=
"activities/minutesFairlyActive",
218 translation_key=
"minutes_fairly_active",
219 native_unit_of_measurement=UnitOfTime.MINUTES,
221 device_class=SensorDeviceClass.DURATION,
222 scope=FitbitScope.ACTIVITY,
223 state_class=SensorStateClass.MEASUREMENT,
224 entity_category=EntityCategory.DIAGNOSTIC,
227 key=
"activities/minutesLightlyActive",
228 translation_key=
"minutes_lightly_active",
229 native_unit_of_measurement=UnitOfTime.MINUTES,
231 device_class=SensorDeviceClass.DURATION,
232 scope=FitbitScope.ACTIVITY,
233 state_class=SensorStateClass.MEASUREMENT,
234 entity_category=EntityCategory.DIAGNOSTIC,
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,
247 key=
"activities/minutesVeryActive",
248 translation_key=
"minutes_very_active",
249 native_unit_of_measurement=UnitOfTime.MINUTES,
251 device_class=SensorDeviceClass.DURATION,
252 scope=FitbitScope.ACTIVITY,
253 state_class=SensorStateClass.MEASUREMENT,
254 entity_category=EntityCategory.DIAGNOSTIC,
257 key=
"activities/steps",
258 translation_key=
"steps",
259 native_unit_of_measurement=
"steps",
261 scope=FitbitScope.ACTIVITY,
262 state_class=SensorStateClass.TOTAL_INCREASING,
265 key=
"activities/tracker/activityCalories",
266 translation_key=
"activity_calories",
267 native_unit_of_measurement=
"cal",
269 scope=FitbitScope.ACTIVITY,
270 entity_registry_enabled_default=
False,
271 state_class=SensorStateClass.TOTAL_INCREASING,
272 entity_category=EntityCategory.DIAGNOSTIC,
275 key=
"activities/tracker/calories",
276 translation_key=
"calories",
277 native_unit_of_measurement=
"cal",
279 scope=FitbitScope.ACTIVITY,
280 entity_registry_enabled_default=
False,
281 state_class=SensorStateClass.TOTAL_INCREASING,
282 entity_category=EntityCategory.DIAGNOSTIC,
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,
296 key=
"activities/tracker/elevation",
297 translation_key=
"elevation",
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,
307 key=
"activities/tracker/floors",
308 translation_key=
"floors",
309 native_unit_of_measurement=
"floors",
311 scope=FitbitScope.ACTIVITY,
312 entity_registry_enabled_default=
False,
313 state_class=SensorStateClass.TOTAL_INCREASING,
314 entity_category=EntityCategory.DIAGNOSTIC,
317 key=
"activities/tracker/minutesFairlyActive",
318 translation_key=
"minutes_fairly_active",
319 native_unit_of_measurement=UnitOfTime.MINUTES,
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,
328 key=
"activities/tracker/minutesLightlyActive",
329 translation_key=
"minutes_lightly_active",
330 native_unit_of_measurement=UnitOfTime.MINUTES,
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,
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,
350 key=
"activities/tracker/minutesVeryActive",
351 translation_key=
"minutes_very_active",
352 native_unit_of_measurement=UnitOfTime.MINUTES,
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,
361 key=
"activities/tracker/steps",
362 translation_key=
"steps",
363 native_unit_of_measurement=
"steps",
365 scope=FitbitScope.ACTIVITY,
366 entity_registry_enabled_default=
False,
367 state_class=SensorStateClass.TOTAL_INCREASING,
368 entity_category=EntityCategory.DIAGNOSTIC,
372 translation_key=
"bmi",
373 native_unit_of_measurement=
"BMI",
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,
383 translation_key=
"body_fat",
384 native_unit_of_measurement=PERCENTAGE,
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,
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,
402 key=
"sleep/awakeningsCount",
403 translation_key=
"awakenings_count",
404 native_unit_of_measurement=
"times awaken",
406 scope=FitbitScope.SLEEP,
407 state_class=SensorStateClass.TOTAL_INCREASING,
408 entity_category=EntityCategory.DIAGNOSTIC,
411 key=
"sleep/efficiency",
412 translation_key=
"sleep_efficiency",
413 native_unit_of_measurement=PERCENTAGE,
415 state_class=SensorStateClass.MEASUREMENT,
416 scope=FitbitScope.SLEEP,
417 entity_category=EntityCategory.DIAGNOSTIC,
420 key=
"sleep/minutesAfterWakeup",
421 translation_key=
"minutes_after_wakeup",
422 native_unit_of_measurement=UnitOfTime.MINUTES,
424 device_class=SensorDeviceClass.DURATION,
425 scope=FitbitScope.SLEEP,
426 state_class=SensorStateClass.MEASUREMENT,
427 entity_category=EntityCategory.DIAGNOSTIC,
430 key=
"sleep/minutesAsleep",
431 translation_key=
"sleep_minutes_asleep",
432 native_unit_of_measurement=UnitOfTime.MINUTES,
434 device_class=SensorDeviceClass.DURATION,
435 scope=FitbitScope.SLEEP,
436 state_class=SensorStateClass.TOTAL_INCREASING,
437 entity_category=EntityCategory.DIAGNOSTIC,
440 key=
"sleep/minutesAwake",
441 translation_key=
"sleep_minutes_awake",
442 native_unit_of_measurement=UnitOfTime.MINUTES,
444 device_class=SensorDeviceClass.DURATION,
445 scope=FitbitScope.SLEEP,
446 state_class=SensorStateClass.TOTAL_INCREASING,
447 entity_category=EntityCategory.DIAGNOSTIC,
450 key=
"sleep/minutesToFallAsleep",
451 translation_key=
"sleep_minutes_to_fall_asleep",
452 native_unit_of_measurement=UnitOfTime.MINUTES,
454 device_class=SensorDeviceClass.DURATION,
455 scope=FitbitScope.SLEEP,
456 state_class=SensorStateClass.TOTAL_INCREASING,
457 entity_category=EntityCategory.DIAGNOSTIC,
460 key=
"sleep/timeInBed",
461 translation_key=
"sleep_time_in_bed",
462 native_unit_of_measurement=UnitOfTime.MINUTES,
464 device_class=SensorDeviceClass.DURATION,
465 scope=FitbitScope.SLEEP,
466 state_class=SensorStateClass.TOTAL_INCREASING,
467 entity_category=EntityCategory.DIAGNOSTIC,
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,
479 key=
"foods/log/water",
480 translation_key=
"water",
481 icon=
"mdi:cup-water",
483 state_class=SensorStateClass.TOTAL_INCREASING,
484 scope=FitbitScope.NUTRITION,
485 entity_category=EntityCategory.DIAGNOSTIC,
491 key=
"sleep/startTime",
492 translation_key=
"sleep_start_time",
494 scope=FitbitScope.SLEEP,
495 entity_category=EntityCategory.DIAGNOSTIC,
498 key=
"sleep/startTime",
499 translation_key=
"sleep_start_time",
501 value_fn=_clock_format_12h,
502 scope=FitbitScope.SLEEP,
503 entity_category=EntityCategory.DIAGNOSTIC,
507 key=
"devices/battery",
508 translation_key=
"battery",
510 scope=FitbitScope.DEVICE,
511 entity_category=EntityCategory.DIAGNOSTIC,
512 has_entity_name=
True,
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,
528 async_add_entities: AddEntitiesCallback,
530 """Set up the Fitbit sensor platform."""
532 data: FitbitData = hass.data[DOMAIN][entry.entry_id]
537 user_profile = await api.async_get_user_profile()
538 unit_system = await api.async_get_unit_system()
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)
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)
551 *FITBIT_RESOURCES_LIST,
552 SLEEP_START_TIME_12HR
553 if fitbit_config.clock_format ==
"12H"
554 else SLEEP_START_TIME,
561 user_profile.encoded_id,
563 units=description.unit_fn(unit_system),
564 enable_default_override=is_explicit_enable(description),
567 for description
in resource_list
568 if is_allowed_resource(description)
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,
579 enable_default_override=is_explicit_enable(FITBIT_RESOURCE_BATTERY),
581 for device
in data.device_coordinator.data.values()
583 battery_entities.extend(
585 data.device_coordinator,
586 user_profile.encoded_id,
587 FITBIT_RESOURCE_BATTERY_LEVEL,
590 for device
in data.device_coordinator.data.values()
596 """Implementation of a Fitbit sensor."""
598 entity_description: FitbitSensorEntityDescription
599 _attr_attribution = ATTRIBUTION
600 _attr_has_entity_name =
True
604 config_entry: ConfigEntry,
606 user_profile_id: str,
607 description: FitbitSensorEntityDescription,
609 enable_default_override: bool,
610 device_info: DeviceInfo,
612 """Initialize the Fitbit sensor."""
620 if units
is not None:
623 if enable_default_override:
627 """Get the latest data from the Fitbit API and update the states."""
629 result = await self.
apiapi.async_get_latest_time_series(
632 except FitbitAuthException:
635 except FitbitApiException:
642 """When entity is added to hass."""
651 """Implementation of a Fitbit battery sensor."""
653 entity_description: FitbitSensorEntityDescription
654 _attr_attribution = ATTRIBUTION
658 coordinator: FitbitDeviceCoordinator,
659 user_profile_id: str,
660 description: FitbitSensorEntityDescription,
661 device: FitbitDevice,
662 enable_default_override: bool,
664 """Initialize the Fitbit sensor."""
668 self.
_attr_unique_id_attr_unique_id = f
"{user_profile_id}_{description.key}_{device.id}"
670 identifiers={(DOMAIN, f
"{user_profile_id}_{device.id}")},
671 name=device.device_version,
672 model=device.device_version,
675 if enable_default_override:
680 """Icon to use in the frontend, if any."""
681 if battery_level := BATTERY_LEVELS.get(self.
devicedevice.battery):
687 """Return the state attributes."""
689 "model": self.
devicedevice.device_version,
690 "type": self.
devicedevice.type.lower()
if self.
devicedevice.type
is not None else None,
694 """When entity is added to hass update state from existing coordinator data."""
700 """Handle updated data from the coordinator."""
701 self.
devicedevice = self.coordinator.data[self.
devicedevice.id]
707 CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity
709 """Implementation of a Fitbit battery level sensor."""
711 entity_description: FitbitSensorEntityDescription
712 _attr_attribution = ATTRIBUTION
716 coordinator: FitbitDeviceCoordinator,
717 user_profile_id: str,
718 description: FitbitSensorEntityDescription,
719 device: FitbitDevice,
721 """Initialize the Fitbit sensor."""
725 self.
_attr_unique_id_attr_unique_id = f
"{user_profile_id}_{description.key}_{device.id}"
727 identifiers={(DOMAIN, f
"{user_profile_id}_{device.id}")},
728 name=device.device_version,
729 model=device.device_version,
733 """When entity is added to hass update state from existing coordinator data."""
739 """Handle updated data from the coordinator."""
740 self.
devicedevice = self.coordinator.data[self.
devicedevice.id]
None __init__(self, FitbitDeviceCoordinator coordinator, str user_profile_id, FitbitSensorEntityDescription description, FitbitDevice device)
None async_added_to_hass(self)
None _handle_coordinator_update(self)
None __init__(self, FitbitDeviceCoordinator coordinator, str user_profile_id, FitbitSensorEntityDescription description, FitbitDevice device, bool enable_default_override)
None _handle_coordinator_update(self)
_attr_entity_registry_enabled_default
dict[str, str|None] extra_state_attributes(self)
None async_added_to_hass(self)
_attr_native_unit_of_measurement
_attr_entity_registry_enabled_default
None __init__(self, ConfigEntry config_entry, FitbitApi api, str user_profile_id, FitbitSensorEntityDescription description, str|None units, bool enable_default_override, DeviceInfo device_info)
None async_added_to_hass(self)
None async_schedule_update_ha_state(self, bool force_refresh=False)
None async_write_ha_state(self)
web.Response get(self, web.Request request, str config_key)
FitbitConfig config_from_entry_data(Mapping[str, Any] data)
UnitOfVolume _water_unit(FitbitUnitSystem unit_system)
int|str _distance_value_fn(dict[str, Any] result)
DeviceInfo _build_device_info(ConfigEntry config_entry, FitbitSensorEntityDescription entity_description)
UnitOfMass _weight_unit(FitbitUnitSystem unit_system)
str _clock_format_12h(dict[str, Any] result)
int|str _body_value_fn(dict[str, Any] result)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
str _default_value_fn(dict[str, Any] result)
UnitOfLength _elevation_unit(FitbitUnitSystem unit_system)
Callable[[dict[str, Any]], int|None] _int_value_or_none(str field)
UnitOfLength _distance_unit(FitbitUnitSystem unit_system)
str icon_for_battery_level(int|None battery_level=None, bool charging=False)