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):