core/homeassistant/components/fitbit/sensor.py

743 lines
26 KiB
Python

"""Support for the Fitbit API."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import datetime
import logging
from typing import Any, Final, cast
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfLength,
UnitOfMass,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .api import FitbitApi
from .const import ATTRIBUTION, BATTERY_LEVELS, DOMAIN, FitbitScope, FitbitUnitSystem
from .coordinator import FitbitData, FitbitDeviceCoordinator
from .exceptions import FitbitApiException, FitbitAuthException
from .model import FitbitDevice, config_from_entry_data
_LOGGER: Final = logging.getLogger(__name__)
_CONFIGURING: dict[str, str] = {}
SCAN_INTERVAL: Final = datetime.timedelta(minutes=30)
FITBIT_TRACKER_SUBSTRING = "/tracker/"
def _default_value_fn(result: dict[str, Any]) -> str:
"""Parse a Fitbit timeseries API responses."""
return cast(str, result["value"])
def _distance_value_fn(result: dict[str, Any]) -> int | str:
"""Format function for distance values."""
return format(float(_default_value_fn(result)), ".2f")
def _body_value_fn(result: dict[str, Any]) -> int | str:
"""Format function for body values."""
return format(float(_default_value_fn(result)), ".1f")
def _clock_format_12h(result: dict[str, Any]) -> str:
raw_state = result["value"]
if raw_state == "":
return "-"
hours_str, minutes_str = raw_state.split(":")
hours, minutes = int(hours_str), int(minutes_str)
setting = "AM"
if hours > 12:
setting = "PM"
hours -= 12
elif hours == 0:
hours = 12
return f"{hours}:{minutes:02d} {setting}"
def _weight_unit(unit_system: FitbitUnitSystem) -> UnitOfMass:
"""Determine the weight unit."""
if unit_system == FitbitUnitSystem.EN_US:
return UnitOfMass.POUNDS
if unit_system == FitbitUnitSystem.EN_GB:
return UnitOfMass.STONES
return UnitOfMass.KILOGRAMS
def _distance_unit(unit_system: FitbitUnitSystem) -> UnitOfLength:
"""Determine the distance unit."""
if unit_system == FitbitUnitSystem.EN_US:
return UnitOfLength.MILES
return UnitOfLength.KILOMETERS
def _elevation_unit(unit_system: FitbitUnitSystem) -> UnitOfLength:
"""Determine the elevation unit."""
if unit_system == FitbitUnitSystem.EN_US:
return UnitOfLength.FEET
return UnitOfLength.METERS
def _water_unit(unit_system: FitbitUnitSystem) -> UnitOfVolume:
"""Determine the water unit."""
if unit_system == FitbitUnitSystem.EN_US:
return UnitOfVolume.FLUID_OUNCES
return UnitOfVolume.MILLILITERS
def _int_value_or_none(field: str) -> Callable[[dict[str, Any]], int | None]:
"""Value function that will parse the specified field if present."""
def convert(result: dict[str, Any]) -> int | None:
if (value := result["value"].get(field)) is not None:
return int(value)
return None
return convert
@dataclass(frozen=True)
class FitbitSensorEntityDescription(SensorEntityDescription):
"""Describes Fitbit sensor entity."""
unit_type: str | None = None
value_fn: Callable[[dict[str, Any]], Any] = _default_value_fn
unit_fn: Callable[[FitbitUnitSystem], str | None] = lambda x: None
scope: FitbitScope | None = None
@property
def is_tracker(self) -> bool:
"""Return if the entity is a tracker."""
return FITBIT_TRACKER_SUBSTRING in self.key
def _build_device_info(
config_entry: ConfigEntry, entity_description: FitbitSensorEntityDescription
) -> DeviceInfo:
"""Build device info for sensor entities info across devices."""
unique_id = cast(str, config_entry.unique_id)
if entity_description.is_tracker:
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, f"{unique_id}_tracker")},
translation_key="tracker",
translation_placeholders={"display_name": config_entry.title},
)
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, unique_id)},
)
FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
FitbitSensorEntityDescription(
key="activities/activityCalories",
translation_key="activity_calories",
native_unit_of_measurement="cal",
icon="mdi:fire",
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/calories",
translation_key="calories",
native_unit_of_measurement="cal",
icon="mdi:fire",
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
FitbitSensorEntityDescription(
key="activities/caloriesBMR",
translation_key="calories_bmr",
native_unit_of_measurement="cal",
icon="mdi:fire",
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/distance",
icon="mdi:map-marker",
device_class=SensorDeviceClass.DISTANCE,
value_fn=_distance_value_fn,
unit_fn=_distance_unit,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
FitbitSensorEntityDescription(
key="activities/elevation",
translation_key="elevation",
icon="mdi:walk",
device_class=SensorDeviceClass.DISTANCE,
unit_fn=_elevation_unit,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/floors",
translation_key="floors",
native_unit_of_measurement="floors",
icon="mdi:walk",
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/heart",
translation_key="resting_heart_rate",
native_unit_of_measurement="bpm",
icon="mdi:heart-pulse",
value_fn=_int_value_or_none("restingHeartRate"),
scope=FitbitScope.HEART_RATE,
state_class=SensorStateClass.MEASUREMENT,
),
FitbitSensorEntityDescription(
key="activities/minutesFairlyActive",
translation_key="minutes_fairly_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:walk",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/minutesLightlyActive",
translation_key="minutes_lightly_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:walk",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/minutesSedentary",
translation_key="minutes_sedentary",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:seat-recline-normal",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/minutesVeryActive",
translation_key="minutes_very_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:run",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/steps",
translation_key="steps",
native_unit_of_measurement="steps",
icon="mdi:walk",
scope=FitbitScope.ACTIVITY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
FitbitSensorEntityDescription(
key="activities/tracker/activityCalories",
translation_key="activity_calories",
native_unit_of_measurement="cal",
icon="mdi:fire",
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/calories",
translation_key="calories",
native_unit_of_measurement="cal",
icon="mdi:fire",
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/distance",
icon="mdi:map-marker",
device_class=SensorDeviceClass.DISTANCE,
value_fn=_distance_value_fn,
unit_fn=_distance_unit,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/elevation",
translation_key="elevation",
icon="mdi:walk",
device_class=SensorDeviceClass.DISTANCE,
unit_fn=_elevation_unit,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/floors",
translation_key="floors",
native_unit_of_measurement="floors",
icon="mdi:walk",
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesFairlyActive",
translation_key="minutes_fairly_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:walk",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesLightlyActive",
translation_key="minutes_lightly_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:walk",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesSedentary",
translation_key="minutes_sedentary",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:seat-recline-normal",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesVeryActive",
translation_key="minutes_very_active",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:run",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="activities/tracker/steps",
translation_key="steps",
native_unit_of_measurement="steps",
icon="mdi:walk",
scope=FitbitScope.ACTIVITY,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="body/bmi",
translation_key="bmi",
native_unit_of_measurement="BMI",
icon="mdi:human",
state_class=SensorStateClass.MEASUREMENT,
value_fn=_body_value_fn,
scope=FitbitScope.WEIGHT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="body/fat",
translation_key="body_fat",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:human",
state_class=SensorStateClass.MEASUREMENT,
value_fn=_body_value_fn,
scope=FitbitScope.WEIGHT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="body/weight",
icon="mdi:human",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WEIGHT,
value_fn=_body_value_fn,
unit_fn=_weight_unit,
scope=FitbitScope.WEIGHT,
),
FitbitSensorEntityDescription(
key="sleep/awakeningsCount",
translation_key="awakenings_count",
native_unit_of_measurement="times awaken",
icon="mdi:sleep",
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/efficiency",
translation_key="sleep_efficiency",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:sleep",
state_class=SensorStateClass.MEASUREMENT,
scope=FitbitScope.SLEEP,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/minutesAfterWakeup",
translation_key="minutes_after_wakeup",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/minutesAsleep",
translation_key="sleep_minutes_asleep",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/minutesAwake",
translation_key="sleep_minutes_awake",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/minutesToFallAsleep",
translation_key="sleep_minutes_to_fall_asleep",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:sleep",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="sleep/timeInBed",
translation_key="sleep_time_in_bed",
native_unit_of_measurement=UnitOfTime.MINUTES,
icon="mdi:hotel",
device_class=SensorDeviceClass.DURATION,
scope=FitbitScope.SLEEP,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="foods/log/caloriesIn",
translation_key="calories_in",
native_unit_of_measurement="cal",
icon="mdi:food-apple",
state_class=SensorStateClass.TOTAL_INCREASING,
scope=FitbitScope.NUTRITION,
entity_category=EntityCategory.DIAGNOSTIC,
),
FitbitSensorEntityDescription(
key="foods/log/water",
translation_key="water",
icon="mdi:cup-water",
unit_fn=_water_unit,
state_class=SensorStateClass.TOTAL_INCREASING,
scope=FitbitScope.NUTRITION,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
# Different description depending on clock format
SLEEP_START_TIME = FitbitSensorEntityDescription(
key="sleep/startTime",
translation_key="sleep_start_time",
icon="mdi:clock",
scope=FitbitScope.SLEEP,
entity_category=EntityCategory.DIAGNOSTIC,
)
SLEEP_START_TIME_12HR = FitbitSensorEntityDescription(
key="sleep/startTime",
translation_key="sleep_start_time",
icon="mdi:clock",
value_fn=_clock_format_12h,
scope=FitbitScope.SLEEP,
entity_category=EntityCategory.DIAGNOSTIC,
)
FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
key="devices/battery",
translation_key="battery",
icon="mdi:battery",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
)
FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
key="devices/battery_level",
translation_key="battery_level",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Fitbit sensor platform."""
data: FitbitData = hass.data[DOMAIN][entry.entry_id]
api = data.api
# These are run serially to reuse the cached user profile, not gathered
# to avoid two racing requests.
user_profile = await api.async_get_user_profile()
unit_system = await api.async_get_unit_system()
fitbit_config = config_from_entry_data(entry.data)
def is_explicit_enable(description: FitbitSensorEntityDescription) -> bool:
"""Determine if entity is enabled by default."""
return fitbit_config.is_explicit_enable(description.key)
def is_allowed_resource(description: FitbitSensorEntityDescription) -> bool:
"""Determine if an entity is allowed to be created."""
return fitbit_config.is_allowed_resource(description.scope, description.key)
resource_list = [
*FITBIT_RESOURCES_LIST,
SLEEP_START_TIME_12HR
if fitbit_config.clock_format == "12H"
else SLEEP_START_TIME,
]
entities = [
FitbitSensor(
entry,
api,
user_profile.encoded_id,
description,
units=description.unit_fn(unit_system),
enable_default_override=is_explicit_enable(description),
device_info=_build_device_info(entry, description),
)
for description in resource_list
if is_allowed_resource(description)
]
async_add_entities(entities)
if data.device_coordinator and is_allowed_resource(FITBIT_RESOURCE_BATTERY):
battery_entities: list[SensorEntity] = [
FitbitBatterySensor(
data.device_coordinator,
user_profile.encoded_id,
FITBIT_RESOURCE_BATTERY,
device=device,
enable_default_override=is_explicit_enable(FITBIT_RESOURCE_BATTERY),
)
for device in data.device_coordinator.data.values()
]
battery_entities.extend(
FitbitBatteryLevelSensor(
data.device_coordinator,
user_profile.encoded_id,
FITBIT_RESOURCE_BATTERY_LEVEL,
device=device,
)
for device in data.device_coordinator.data.values()
)
async_add_entities(battery_entities)
class FitbitSensor(SensorEntity):
"""Implementation of a Fitbit sensor."""
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(
self,
config_entry: ConfigEntry,
api: FitbitApi,
user_profile_id: str,
description: FitbitSensorEntityDescription,
units: str | None,
enable_default_override: bool,
device_info: DeviceInfo,
) -> None:
"""Initialize the Fitbit sensor."""
self.config_entry = config_entry
self.entity_description = description
self.api = api
self._attr_unique_id = f"{user_profile_id}_{description.key}"
self._attr_device_info = device_info
if units is not None:
self._attr_native_unit_of_measurement = units
if enable_default_override:
self._attr_entity_registry_enabled_default = True
async def async_update(self) -> None:
"""Get the latest data from the Fitbit API and update the states."""
try:
result = await self.api.async_get_latest_time_series(
self.entity_description.key
)
except FitbitAuthException:
self._attr_available = False
self.config_entry.async_start_reauth(self.hass)
except FitbitApiException:
self._attr_available = False
else:
self._attr_available = True
self._attr_native_value = self.entity_description.value_fn(result)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
# We do not ask for an update with async_add_entities()
# because it will update disabled entities.
self.async_schedule_update_ha_state(force_refresh=True)
class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity):
"""Implementation of a Fitbit battery sensor."""
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
def __init__(
self,
coordinator: FitbitDeviceCoordinator,
user_profile_id: str,
description: FitbitSensorEntityDescription,
device: FitbitDevice,
enable_default_override: bool,
) -> None:
"""Initialize the Fitbit sensor."""
super().__init__(coordinator)
self.entity_description = description
self.device = device
self._attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
name=device.device_version,
model=device.device_version,
)
if enable_default_override:
self._attr_entity_registry_enabled_default = True
@property
def icon(self) -> str | None:
"""Icon to use in the frontend, if any."""
if battery_level := BATTERY_LEVELS.get(self.device.battery):
return icon_for_battery_level(battery_level=battery_level)
return self.entity_description.icon
@property
def extra_state_attributes(self) -> dict[str, str | None]:
"""Return the state attributes."""
return {
"model": self.device.device_version,
"type": self.device.type.lower() if self.device.type is not None else None,
}
async def async_added_to_hass(self) -> None:
"""When entity is added to hass update state from existing coordinator data."""
await super().async_added_to_hass()
self._handle_coordinator_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.device = self.coordinator.data[self.device.id]
self._attr_native_value = self.device.battery
self.async_write_ha_state()
class FitbitBatteryLevelSensor(
CoordinatorEntity[FitbitDeviceCoordinator], SensorEntity
):
"""Implementation of a Fitbit battery level sensor."""
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
def __init__(
self,
coordinator: FitbitDeviceCoordinator,
user_profile_id: str,
description: FitbitSensorEntityDescription,
device: FitbitDevice,
) -> None:
"""Initialize the Fitbit sensor."""
super().__init__(coordinator)
self.entity_description = description
self.device = device
self._attr_unique_id = f"{user_profile_id}_{description.key}_{device.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{user_profile_id}_{device.id}")},
name=device.device_version,
model=device.device_version,
)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass update state from existing coordinator data."""
await super().async_added_to_hass()
self._handle_coordinator_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.device = self.coordinator.data[self.device.id]
self._attr_native_value = self.device.battery_level
self.async_write_ha_state()