From fed5f5e3b96c97df8a7c051efaa28ff4e1984367 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 23 Sep 2021 20:08:47 +0200 Subject: [PATCH] Use EntityDescription - fitbit (#55925) --- homeassistant/components/fitbit/const.py | 274 +++++++++++++++++----- homeassistant/components/fitbit/sensor.py | 146 ++++++------ 2 files changed, 286 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py index e5891758f60..1da3058c790 100644 --- a/homeassistant/components/fitbit/const.py +++ b/homeassistant/components/fitbit/const.py @@ -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": { diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0bd4ed36199..34c4f61f554 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -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 = {