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

View File

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