From 0dcd8b32abaf87245ddf67996944c2e657ecba48 Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Mon, 27 Sep 2021 19:40:55 +0200
Subject: [PATCH] Use EntityDescription - meteo_france (#55677)

---
 .../components/meteo_france/const.py          | 248 +++++++++---------
 .../components/meteo_france/sensor.py         | 134 ++++------
 2 files changed, 179 insertions(+), 203 deletions(-)

diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
index a2e9eeb2799..09b48bc1b3e 100644
--- a/homeassistant/components/meteo_france/const.py
+++ b/homeassistant/components/meteo_france/const.py
@@ -1,5 +1,9 @@
 """Meteo-France component constants."""
+from __future__ import annotations
 
+from dataclasses import dataclass
+
+from homeassistant.components.sensor import SensorEntityDescription
 from homeassistant.components.weather import (
     ATTR_CONDITION_CLEAR_NIGHT,
     ATTR_CONDITION_CLOUDY,
@@ -47,127 +51,131 @@ FORECAST_MODE = [FORECAST_MODE_HOURLY, FORECAST_MODE_DAILY]
 ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast"
 ATTR_NEXT_RAIN_DT_REF = "forecast_time_ref"
 
-ENTITY_NAME = "name"
-ENTITY_UNIT = "unit"
-ENTITY_ICON = "icon"
-ENTITY_DEVICE_CLASS = "device_class"
-ENTITY_ENABLE = "enable"
-ENTITY_API_DATA_PATH = "data_path"
 
-SENSOR_TYPES = {
-    "pressure": {
-        ENTITY_NAME: "Pressure",
-        ENTITY_UNIT: PRESSURE_HPA,
-        ENTITY_ICON: None,
-        ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "current_forecast:sea_level",
-    },
-    "rain_chance": {
-        ENTITY_NAME: "Rain chance",
-        ENTITY_UNIT: PERCENTAGE,
-        ENTITY_ICON: "mdi:weather-rainy",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "probability_forecast:rain:3h",
-    },
-    "snow_chance": {
-        ENTITY_NAME: "Snow chance",
-        ENTITY_UNIT: PERCENTAGE,
-        ENTITY_ICON: "mdi:weather-snowy",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "probability_forecast:snow:3h",
-    },
-    "freeze_chance": {
-        ENTITY_NAME: "Freeze chance",
-        ENTITY_UNIT: PERCENTAGE,
-        ENTITY_ICON: "mdi:snowflake",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "probability_forecast:freezing",
-    },
-    "wind_gust": {
-        ENTITY_NAME: "Wind gust",
-        ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
-        ENTITY_ICON: "mdi:weather-windy-variant",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "current_forecast:wind:gust",
-    },
-    "wind_speed": {
-        ENTITY_NAME: "Wind speed",
-        ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
-        ENTITY_ICON: "mdi:weather-windy",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "current_forecast:wind:speed",
-    },
-    "next_rain": {
-        ENTITY_NAME: "Next rain",
-        ENTITY_UNIT: None,
-        ENTITY_ICON: None,
-        ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: None,
-    },
-    "temperature": {
-        ENTITY_NAME: "Temperature",
-        ENTITY_UNIT: TEMP_CELSIUS,
-        ENTITY_ICON: None,
-        ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "current_forecast:T:value",
-    },
-    "uv": {
-        ENTITY_NAME: "UV",
-        ENTITY_UNIT: UV_INDEX,
-        ENTITY_ICON: "mdi:sunglasses",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "today_forecast:uv",
-    },
-    "weather_alert": {
-        ENTITY_NAME: "Weather alert",
-        ENTITY_UNIT: None,
-        ENTITY_ICON: "mdi:weather-cloudy-alert",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: None,
-    },
-    "precipitation": {
-        ENTITY_NAME: "Daily precipitation",
-        ENTITY_UNIT: LENGTH_MILLIMETERS,
-        ENTITY_ICON: "mdi:cup-water",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h",
-    },
-    "cloud": {
-        ENTITY_NAME: "Cloud cover",
-        ENTITY_UNIT: PERCENTAGE,
-        ENTITY_ICON: "mdi:weather-partly-cloudy",
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: True,
-        ENTITY_API_DATA_PATH: "current_forecast:clouds",
-    },
-    "original_condition": {
-        ENTITY_NAME: "Original condition",
-        ENTITY_UNIT: None,
-        ENTITY_ICON: None,
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "current_forecast:weather:desc",
-    },
-    "daily_original_condition": {
-        ENTITY_NAME: "Daily original condition",
-        ENTITY_UNIT: None,
-        ENTITY_ICON: None,
-        ENTITY_DEVICE_CLASS: None,
-        ENTITY_ENABLE: False,
-        ENTITY_API_DATA_PATH: "today_forecast:weather12H:desc",
-    },
-}
+@dataclass
+class MeteoFranceRequiredKeysMixin:
+    """Mixin for required keys."""
+
+    data_path: str
+
+
+@dataclass
+class MeteoFranceSensorEntityDescription(
+    SensorEntityDescription, MeteoFranceRequiredKeysMixin
+):
+    """Describes Meteo-France sensor entity."""
+
+
+SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = (
+    MeteoFranceSensorEntityDescription(
+        key="pressure",
+        name="Pressure",
+        native_unit_of_measurement=PRESSURE_HPA,
+        device_class=DEVICE_CLASS_PRESSURE,
+        entity_registry_enabled_default=False,
+        data_path="current_forecast:sea_level",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="wind_gust",
+        name="Wind gust",
+        native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
+        icon="mdi:weather-windy-variant",
+        entity_registry_enabled_default=False,
+        data_path="current_forecast:wind:gust",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="wind_speed",
+        name="Wind speed",
+        native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
+        icon="mdi:weather-windy",
+        entity_registry_enabled_default=False,
+        data_path="current_forecast:wind:speed",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="temperature",
+        name="Temperature",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=DEVICE_CLASS_TEMPERATURE,
+        entity_registry_enabled_default=False,
+        data_path="current_forecast:T:value",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="uv",
+        name="UV",
+        native_unit_of_measurement=UV_INDEX,
+        icon="mdi:sunglasses",
+        data_path="today_forecast:uv",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="precipitation",
+        name="Daily precipitation",
+        native_unit_of_measurement=LENGTH_MILLIMETERS,
+        icon="mdi:cup-water",
+        data_path="today_forecast:precipitation:24h",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="cloud",
+        name="Cloud cover",
+        native_unit_of_measurement=PERCENTAGE,
+        icon="mdi:weather-partly-cloudy",
+        data_path="current_forecast:clouds",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="original_condition",
+        name="Original condition",
+        entity_registry_enabled_default=False,
+        data_path="current_forecast:weather:desc",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="daily_original_condition",
+        name="Daily original condition",
+        entity_registry_enabled_default=False,
+        data_path="today_forecast:weather12H:desc",
+    ),
+)
+
+SENSOR_TYPES_RAIN: tuple[MeteoFranceSensorEntityDescription, ...] = (
+    MeteoFranceSensorEntityDescription(
+        key="next_rain",
+        name="Next rain",
+        device_class=DEVICE_CLASS_TIMESTAMP,
+        data_path="",
+    ),
+)
+
+SENSOR_TYPES_ALERT: tuple[MeteoFranceSensorEntityDescription, ...] = (
+    MeteoFranceSensorEntityDescription(
+        key="weather_alert",
+        name="Weather alert",
+        icon="mdi:weather-cloudy-alert",
+        data_path="",
+    ),
+)
+
+SENSOR_TYPES_PROBABILITY: tuple[MeteoFranceSensorEntityDescription, ...] = (
+    MeteoFranceSensorEntityDescription(
+        key="rain_chance",
+        name="Rain chance",
+        native_unit_of_measurement=PERCENTAGE,
+        icon="mdi:weather-rainy",
+        data_path="probability_forecast:rain:3h",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="snow_chance",
+        name="Snow chance",
+        native_unit_of_measurement=PERCENTAGE,
+        icon="mdi:weather-snowy",
+        data_path="probability_forecast:snow:3h",
+    ),
+    MeteoFranceSensorEntityDescription(
+        key="freeze_chance",
+        name="Freeze chance",
+        native_unit_of_measurement=PERCENTAGE,
+        icon="mdi:snowflake",
+        data_path="probability_forecast:freezing",
+    ),
+)
+
 
 CONDITION_CLASSES = {
     ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"],
diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py
index df006c78194..9f24cf02a2c 100644
--- a/homeassistant/components/meteo_france/sensor.py
+++ b/homeassistant/components/meteo_france/sensor.py
@@ -1,6 +1,4 @@
 """Support for Meteo-France raining forecast sensor."""
-import logging
-
 from meteofrance_api.helpers import (
     get_warning_text_status_from_indice_color,
     readeable_phenomenoms_dict,
@@ -24,19 +22,15 @@ from .const import (
     COORDINATOR_FORECAST,
     COORDINATOR_RAIN,
     DOMAIN,
-    ENTITY_API_DATA_PATH,
-    ENTITY_DEVICE_CLASS,
-    ENTITY_ENABLE,
-    ENTITY_ICON,
-    ENTITY_NAME,
-    ENTITY_UNIT,
     MANUFACTURER,
     MODEL,
     SENSOR_TYPES,
+    SENSOR_TYPES_ALERT,
+    SENSOR_TYPES_PROBABILITY,
+    SENSOR_TYPES_RAIN,
+    MeteoFranceSensorEntityDescription,
 )
 
-_LOGGER = logging.getLogger(__name__)
-
 
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities
@@ -46,56 +40,51 @@ async def async_setup_entry(
     coordinator_rain = hass.data[DOMAIN][entry.entry_id][COORDINATOR_RAIN]
     coordinator_alert = hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT]
 
-    entities = []
-    for sensor_type in SENSOR_TYPES:
-        if sensor_type == "next_rain":
-            if coordinator_rain:
-                entities.append(MeteoFranceRainSensor(sensor_type, coordinator_rain))
-
-        elif sensor_type == "weather_alert":
-            if coordinator_alert:
-                entities.append(MeteoFranceAlertSensor(sensor_type, coordinator_alert))
-
-        elif sensor_type in ("rain_chance", "freeze_chance", "snow_chance"):
-            if coordinator_forecast.data.probability_forecast:
-                entities.append(MeteoFranceSensor(sensor_type, coordinator_forecast))
-            else:
-                _LOGGER.warning(
-                    "Sensor %s skipped for %s as data is missing in the API",
-                    sensor_type,
-                    coordinator_forecast.data.position["name"],
-                )
-
-        else:
-            entities.append(MeteoFranceSensor(sensor_type, coordinator_forecast))
-
-    async_add_entities(
-        entities,
-        False,
+    entities = [
+        MeteoFranceSensor(coordinator_forecast, description)
+        for description in SENSOR_TYPES
+    ]
+    entities.extend(
+        [
+            MeteoFranceRainSensor(coordinator_rain, description)
+            for description in SENSOR_TYPES_RAIN
+        ]
     )
+    entities.extend(
+        [
+            MeteoFranceAlertSensor(coordinator_alert, description)
+            for description in SENSOR_TYPES_ALERT
+        ]
+    )
+    if coordinator_forecast.data.probability_forecast:
+        entities.extend(
+            [
+                MeteoFranceSensor(coordinator_forecast, description)
+                for description in SENSOR_TYPES_PROBABILITY
+            ]
+        )
+
+    async_add_entities(entities, False)
 
 
 class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
     """Representation of a Meteo-France sensor."""
 
-    def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None:
+    entity_description: MeteoFranceSensorEntityDescription
+
+    def __init__(
+        self,
+        coordinator: DataUpdateCoordinator,
+        description: MeteoFranceSensorEntityDescription,
+    ) -> None:
         """Initialize the Meteo-France sensor."""
         super().__init__(coordinator)
-        self._type = sensor_type
-        if hasattr(self.coordinator.data, "position"):
-            city_name = self.coordinator.data.position["name"]
-            self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
-            self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
-
-    @property
-    def unique_id(self):
-        """Return the unique id."""
-        return self._unique_id
-
-    @property
-    def name(self):
-        """Return the name."""
-        return self._name
+        self.entity_description = description
+        if hasattr(coordinator.data, "position"):
+            city_name = coordinator.data.position["name"]
+            self._attr_name = f"{city_name} {description.name}"
+            self._attr_unique_id = f"{coordinator.data.position['lat']},{coordinator.data.position['lon']}_{description.key}"
+        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
 
     @property
     def device_info(self):
@@ -111,7 +100,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
     @property
     def native_value(self):
         """Return the state."""
-        path = SENSOR_TYPES[self._type][ENTITY_API_DATA_PATH].split(":")
+        path = self.entity_description.data_path.split(":")
         data = getattr(self.coordinator.data, path[0])
 
         # Specific case for probability forecast
@@ -129,36 +118,11 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
             else:
                 value = data[path[1]]
 
-        if self._type in ("wind_speed", "wind_gust"):
+        if self.entity_description.key in ("wind_speed", "wind_gust"):
             # convert API wind speed from m/s to km/h
             value = round(value * 3.6)
         return value
 
-    @property
-    def native_unit_of_measurement(self):
-        """Return the unit of measurement."""
-        return SENSOR_TYPES[self._type][ENTITY_UNIT]
-
-    @property
-    def icon(self):
-        """Return the icon."""
-        return SENSOR_TYPES[self._type][ENTITY_ICON]
-
-    @property
-    def device_class(self):
-        """Return the device class."""
-        return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS]
-
-    @property
-    def entity_registry_enabled_default(self) -> bool:
-        """Return if the entity should be enabled when first added to the entity registry."""
-        return SENSOR_TYPES[self._type][ENTITY_ENABLE]
-
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
 
 class MeteoFranceRainSensor(MeteoFranceSensor):
     """Representation of a Meteo-France rain sensor."""
@@ -194,12 +158,16 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
 class MeteoFranceAlertSensor(MeteoFranceSensor):
     """Representation of a Meteo-France alert sensor."""
 
-    def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator) -> None:
+    def __init__(
+        self,
+        coordinator: DataUpdateCoordinator,
+        description: MeteoFranceSensorEntityDescription,
+    ) -> None:
         """Initialize the Meteo-France sensor."""
-        super().__init__(sensor_type, coordinator)
+        super().__init__(coordinator, description)
         dept_code = self.coordinator.data.domain_id
-        self._name = f"{dept_code} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
-        self._unique_id = self._name
+        self._attr_name = f"{dept_code} {description.name}"
+        self._attr_unique_id = self._attr_name
 
     @property
     def native_value(self):