1
mirror of https://github.com/home-assistant/core synced 2024-09-15 17:29:45 +02:00

Use EntityDescription - fitbit (#55925)

This commit is contained in:
Marc Mueller 2021-09-23 20:08:47 +02:00 committed by GitHub
parent 60bb3121b6
commit fed5f5e3b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 286 additions and 134 deletions

View File

@ -1,8 +1,10 @@
"""Constants for the Fitbit platform."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Final
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
@ -43,66 +45,230 @@ DEFAULT_CONFIG: Final[dict[str, str]] = {
}
DEFAULT_CLOCK_FORMAT: Final = "24H"
FITBIT_RESOURCES_LIST: Final[dict[str, tuple[str, str | None, str]]] = {
"activities/activityCalories": ("Activity Calories", "cal", "fire"),
"activities/calories": ("Calories", "cal", "fire"),
"activities/caloriesBMR": ("Calories BMR", "cal", "fire"),
"activities/distance": ("Distance", "", "map-marker"),
"activities/elevation": ("Elevation", "", "walk"),
"activities/floors": ("Floors", "floors", "walk"),
"activities/heart": ("Resting Heart Rate", "bpm", "heart-pulse"),
"activities/minutesFairlyActive": ("Minutes Fairly Active", TIME_MINUTES, "walk"),
"activities/minutesLightlyActive": ("Minutes Lightly Active", TIME_MINUTES, "walk"),
"activities/minutesSedentary": (
"Minutes Sedentary",
TIME_MINUTES,
"seat-recline-normal",
@dataclass
class FitbitRequiredKeysMixin:
"""Mixin for required keys."""
unit_type: str | None
@dataclass
class FitbitSensorEntityDescription(SensorEntityDescription, FitbitRequiredKeysMixin):
"""Describes Fitbit sensor entity."""
FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = (
FitbitSensorEntityDescription(
key="activities/activityCalories",
name="Activity Calories",
unit_type="cal",
icon="mdi:fire",
),
"activities/minutesVeryActive": ("Minutes Very Active", TIME_MINUTES, "run"),
"activities/steps": ("Steps", "steps", "walk"),
"activities/tracker/activityCalories": ("Tracker Activity Calories", "cal", "fire"),
"activities/tracker/calories": ("Tracker Calories", "cal", "fire"),
"activities/tracker/distance": ("Tracker Distance", "", "map-marker"),
"activities/tracker/elevation": ("Tracker Elevation", "", "walk"),
"activities/tracker/floors": ("Tracker Floors", "floors", "walk"),
"activities/tracker/minutesFairlyActive": (
"Tracker Minutes Fairly Active",
TIME_MINUTES,
"walk",
FitbitSensorEntityDescription(
key="activities/calories",
name="Calories",
unit_type="cal",
icon="mdi:fire",
),
"activities/tracker/minutesLightlyActive": (
"Tracker Minutes Lightly Active",
TIME_MINUTES,
"walk",
FitbitSensorEntityDescription(
key="activities/caloriesBMR",
name="Calories BMR",
unit_type="cal",
icon="mdi:fire",
),
"activities/tracker/minutesSedentary": (
"Tracker Minutes Sedentary",
TIME_MINUTES,
"seat-recline-normal",
FitbitSensorEntityDescription(
key="activities/distance",
name="Distance",
unit_type="",
icon="mdi:map-marker",
),
"activities/tracker/minutesVeryActive": (
"Tracker Minutes Very Active",
TIME_MINUTES,
"run",
FitbitSensorEntityDescription(
key="activities/elevation",
name="Elevation",
unit_type="",
icon="mdi:walk",
),
"activities/tracker/steps": ("Tracker Steps", "steps", "walk"),
"body/bmi": ("BMI", "BMI", "human"),
"body/fat": ("Body Fat", PERCENTAGE, "human"),
"body/weight": ("Weight", "", "human"),
"devices/battery": ("Battery", None, "battery"),
"sleep/awakeningsCount": ("Awakenings Count", "times awaken", "sleep"),
"sleep/efficiency": ("Sleep Efficiency", PERCENTAGE, "sleep"),
"sleep/minutesAfterWakeup": ("Minutes After Wakeup", TIME_MINUTES, "sleep"),
"sleep/minutesAsleep": ("Sleep Minutes Asleep", TIME_MINUTES, "sleep"),
"sleep/minutesAwake": ("Sleep Minutes Awake", TIME_MINUTES, "sleep"),
"sleep/minutesToFallAsleep": (
"Sleep Minutes to Fall Asleep",
TIME_MINUTES,
"sleep",
FitbitSensorEntityDescription(
key="activities/floors",
name="Floors",
unit_type="floors",
icon="mdi:walk",
),
"sleep/startTime": ("Sleep Start Time", None, "clock"),
"sleep/timeInBed": ("Sleep Time in Bed", TIME_MINUTES, "hotel"),
}
FitbitSensorEntityDescription(
key="activities/heart",
name="Resting Heart Rate",
unit_type="bpm",
icon="mdi:heart-pulse",
),
FitbitSensorEntityDescription(
key="activities/minutesFairlyActive",
name="Minutes Fairly Active",
unit_type=TIME_MINUTES,
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/minutesLightlyActive",
name="Minutes Lightly Active",
unit_type=TIME_MINUTES,
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/minutesSedentary",
name="Minutes Sedentary",
unit_type=TIME_MINUTES,
icon="mdi:seat-recline-normal",
),
FitbitSensorEntityDescription(
key="activities/minutesVeryActive",
name="Minutes Very Active",
unit_type=TIME_MINUTES,
icon="mdi:run",
),
FitbitSensorEntityDescription(
key="activities/steps",
name="Steps",
unit_type="steps",
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/tracker/activityCalories",
name="Tracker Activity Calories",
unit_type="cal",
icon="mdi:fire",
),
FitbitSensorEntityDescription(
key="activities/tracker/calories",
name="Tracker Calories",
unit_type="cal",
icon="mdi:fire",
),
FitbitSensorEntityDescription(
key="activities/tracker/distance",
name="Tracker Distance",
unit_type="",
icon="mdi:map-marker",
),
FitbitSensorEntityDescription(
key="activities/tracker/elevation",
name="Tracker Elevation",
unit_type="",
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/tracker/floors",
name="Tracker Floors",
unit_type="floors",
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesFairlyActive",
name="Tracker Minutes Fairly Active",
unit_type=TIME_MINUTES,
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesLightlyActive",
name="Tracker Minutes Lightly Active",
unit_type=TIME_MINUTES,
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesSedentary",
name="Tracker Minutes Sedentary",
unit_type=TIME_MINUTES,
icon="mdi:seat-recline-normal",
),
FitbitSensorEntityDescription(
key="activities/tracker/minutesVeryActive",
name="Tracker Minutes Very Active",
unit_type=TIME_MINUTES,
icon="mdi:run",
),
FitbitSensorEntityDescription(
key="activities/tracker/steps",
name="Tracker Steps",
unit_type="steps",
icon="mdi:walk",
),
FitbitSensorEntityDescription(
key="body/bmi",
name="BMI",
unit_type="BMI",
icon="mdi:human",
),
FitbitSensorEntityDescription(
key="body/fat",
name="Body Fat",
unit_type=PERCENTAGE,
icon="mdi:human",
),
FitbitSensorEntityDescription(
key="body/weight",
name="Weight",
unit_type="",
icon="mdi:human",
),
FitbitSensorEntityDescription(
key="sleep/awakeningsCount",
name="Awakenings Count",
unit_type="times awaken",
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/efficiency",
name="Sleep Efficiency",
unit_type=PERCENTAGE,
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/minutesAfterWakeup",
name="Minutes After Wakeup",
unit_type=TIME_MINUTES,
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/minutesAsleep",
name="Sleep Minutes Asleep",
unit_type=TIME_MINUTES,
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/minutesAwake",
name="Sleep Minutes Awake",
unit_type=TIME_MINUTES,
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/minutesToFallAsleep",
name="Sleep Minutes to Fall Asleep",
unit_type=TIME_MINUTES,
icon="mdi:sleep",
),
FitbitSensorEntityDescription(
key="sleep/startTime",
name="Sleep Start Time",
unit_type=None,
icon="mdi:clock",
),
FitbitSensorEntityDescription(
key="sleep/timeInBed",
name="Sleep Time in Bed",
unit_type=TIME_MINUTES,
icon="mdi:hotel",
),
)
FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
key="devices/battery",
name="Battery",
unit_type=None,
icon="mdi:battery",
)
FITBIT_RESOURCES_KEYS: Final[list[str]] = [
desc.key for desc in (*FITBIT_RESOURCES_LIST, FITBIT_RESOURCE_BATTERY)
]
FITBIT_MEASUREMENTS: Final[dict[str, dict[str, str]]] = {
"en_US": {

View File

@ -48,7 +48,10 @@ from .const import (
FITBIT_CONFIG_FILE,
FITBIT_DEFAULT_RESOURCES,
FITBIT_MEASUREMENTS,
FITBIT_RESOURCE_BATTERY,
FITBIT_RESOURCES_KEYS,
FITBIT_RESOURCES_LIST,
FitbitSensorEntityDescription,
)
_LOGGER: Final = logging.getLogger(__name__)
@ -61,7 +64,7 @@ PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(
{
vol.Optional(
CONF_MONITORED_RESOURCES, default=FITBIT_DEFAULT_RESOURCES
): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_LIST)]),
): vol.All(cv.ensure_list, [vol.In(FITBIT_RESOURCES_KEYS)]),
vol.Optional(CONF_CLOCK_FORMAT, default=DEFAULT_CLOCK_FORMAT): vol.In(
["12H", "24H"]
),
@ -188,8 +191,7 @@ def setup_platform(
if int(time.time()) - expires_at > 3600:
authd_client.client.refresh_token()
unit_system = config.get(CONF_UNIT_SYSTEM)
if unit_system == "default":
if (unit_system := config[CONF_UNIT_SYSTEM]) == "default":
authd_client.system = authd_client.user_profile_get()["user"]["locale"]
if authd_client.system != "en_GB":
if hass.config.units.is_metric:
@ -199,35 +201,35 @@ def setup_platform(
else:
authd_client.system = unit_system
dev = []
registered_devs = authd_client.get_devices()
clock_format = config.get(CONF_CLOCK_FORMAT, DEFAULT_CLOCK_FORMAT)
for resource in config.get(CONF_MONITORED_RESOURCES, FITBIT_DEFAULT_RESOURCES):
# monitor battery for all linked FitBit devices
if resource == "devices/battery":
for dev_extra in registered_devs:
dev.append(
FitbitSensor(
authd_client,
config_path,
resource,
hass.config.units.is_metric,
clock_format,
dev_extra,
)
)
else:
dev.append(
clock_format = config[CONF_CLOCK_FORMAT]
monitored_resources = config[CONF_MONITORED_RESOURCES]
entities = [
FitbitSensor(
authd_client,
config_path,
description,
hass.config.units.is_metric,
clock_format,
)
for description in FITBIT_RESOURCES_LIST
if description.key in monitored_resources
]
if "devices/battery" in monitored_resources:
entities.extend(
[
FitbitSensor(
authd_client,
config_path,
resource,
FITBIT_RESOURCE_BATTERY,
hass.config.units.is_metric,
clock_format,
dev_extra,
)
)
add_entities(dev, True)
for dev_extra in registered_devs
]
)
add_entities(entities, True)
else:
oauth = FitbitOauth2Client(
@ -335,28 +337,28 @@ class FitbitAuthCallbackView(HomeAssistantView):
class FitbitSensor(SensorEntity):
"""Implementation of a Fitbit sensor."""
entity_description: FitbitSensorEntityDescription
def __init__(
self,
client: Fitbit,
config_path: str,
resource_type: str,
description: FitbitSensorEntityDescription,
is_metric: bool,
clock_format: str,
extra: dict[str, str] | None = None,
) -> None:
"""Initialize the Fitbit sensor."""
self.entity_description = description
self.client = client
self.config_path = config_path
self.resource_type = resource_type
self.is_metric = is_metric
self.clock_format = clock_format
self.extra = extra
self._name = FITBIT_RESOURCES_LIST[self.resource_type][0]
if self.extra is not None:
self._name = f"{self.extra.get('deviceVersion')} Battery"
unit_type = FITBIT_RESOURCES_LIST[self.resource_type][1]
if unit_type == "":
split_resource = self.resource_type.split("/")
self._attr_name = f"{self.extra.get('deviceVersion')} Battery"
if (unit_type := description.unit_type) == "":
split_resource = description.key.rsplit("/", maxsplit=1)[-1]
try:
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
except KeyError:
@ -364,43 +366,24 @@ class FitbitSensor(SensorEntity):
measurement_system = FITBIT_MEASUREMENTS["metric"]
else:
measurement_system = FITBIT_MEASUREMENTS["en_US"]
unit_type = measurement_system[split_resource[-1]]
self._unit_of_measurement = unit_type
self._state: str | None = None
unit_type = measurement_system[split_resource]
self._attr_native_unit_of_measurement = unit_type
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return self._state
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def icon(self) -> str:
def icon(self) -> str | None:
"""Icon to use in the frontend, if any."""
if self.resource_type == "devices/battery" and self.extra is not None:
if self.entity_description.key == "devices/battery" and self.extra is not None:
extra_battery = self.extra.get("battery")
if extra_battery is not None:
battery_level = BATTERY_LEVELS.get(extra_battery)
if battery_level is not None:
return icon_for_battery_level(battery_level=battery_level)
fitbit_ressource = FITBIT_RESOURCES_LIST[self.resource_type]
return f"mdi:{fitbit_ressource[2]}"
return self.entity_description.icon
@property
def extra_state_attributes(self) -> dict[str, str | None]:
"""Return the state attributes."""
attrs: dict[str, str | None] = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
attrs: dict[str, str | None] = {ATTR_ATTRIBUTION: ATTRIBUTION}
if self.extra is not None:
attrs["model"] = self.extra.get("deviceVersion")
@ -411,31 +394,32 @@ class FitbitSensor(SensorEntity):
def update(self) -> None:
"""Get the latest data from the Fitbit API and update the states."""
if self.resource_type == "devices/battery" and self.extra is not None:
resource_type = self.entity_description.key
if resource_type == "devices/battery" and self.extra is not None:
registered_devs: list[dict[str, Any]] = self.client.get_devices()
device_id = self.extra.get("id")
self.extra = list(
filter(lambda device: device.get("id") == device_id, registered_devs)
)[0]
self._state = self.extra.get("battery")
self._attr_native_value = self.extra.get("battery")
else:
container = self.resource_type.replace("/", "-")
response = self.client.time_series(self.resource_type, period="7d")
container = resource_type.replace("/", "-")
response = self.client.time_series(resource_type, period="7d")
raw_state = response[container][-1].get("value")
if self.resource_type == "activities/distance":
self._state = format(float(raw_state), ".2f")
elif self.resource_type == "activities/tracker/distance":
self._state = format(float(raw_state), ".2f")
elif self.resource_type == "body/bmi":
self._state = format(float(raw_state), ".1f")
elif self.resource_type == "body/fat":
self._state = format(float(raw_state), ".1f")
elif self.resource_type == "body/weight":
self._state = format(float(raw_state), ".1f")
elif self.resource_type == "sleep/startTime":
if resource_type == "activities/distance":
self._attr_native_value = format(float(raw_state), ".2f")
elif resource_type == "activities/tracker/distance":
self._attr_native_value = format(float(raw_state), ".2f")
elif resource_type == "body/bmi":
self._attr_native_value = format(float(raw_state), ".1f")
elif resource_type == "body/fat":
self._attr_native_value = format(float(raw_state), ".1f")
elif resource_type == "body/weight":
self._attr_native_value = format(float(raw_state), ".1f")
elif resource_type == "sleep/startTime":
if raw_state == "":
self._state = "-"
self._attr_native_value = "-"
elif self.clock_format == "12H":
hours, minutes = raw_state.split(":")
hours, minutes = int(hours), int(minutes)
@ -445,20 +429,22 @@ class FitbitSensor(SensorEntity):
hours -= 12
elif hours == 0:
hours = 12
self._state = f"{hours}:{minutes:02d} {setting}"
self._attr_native_value = f"{hours}:{minutes:02d} {setting}"
else:
self._state = raw_state
self._attr_native_value = raw_state
else:
if self.is_metric:
self._state = raw_state
self._attr_native_value = raw_state
else:
try:
self._state = f"{int(raw_state):,}"
self._attr_native_value = f"{int(raw_state):,}"
except TypeError:
self._state = raw_state
self._attr_native_value = raw_state
if self.resource_type == "activities/heart":
self._state = response[container][-1].get("value").get("restingHeartRate")
if resource_type == "activities/heart":
self._attr_native_value = (
response[container][-1].get("value").get("restingHeartRate")
)
token = self.client.client.session.token
config_contents = {