Update meteo_france based on code review (#38789)

* Review: if not to pop

* Review: async_add_job --> async_add_executor_job

* Review: const

* Review: start logging messages with capital letter

* Review : UTC isoformated time --> fix "Invalid date""

* Fix hail forecast condition

* Review: _show_setup_form is a callback

* Fix update option

* Review: no icon for next_rain

* Review: inline cities form

* Review: store places as an instance attribute

* UNDO_UPDATE_LISTENER()
This commit is contained in:
Quentame 2020-08-14 15:10:13 +02:00 committed by GitHub
parent 91ead3be50
commit e0c0d3b53f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 57 deletions

View File

@ -20,6 +20,7 @@ from .const import (
COORDINATOR_RAIN, COORDINATOR_RAIN,
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
UNDO_UPDATE_LISTENER,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -77,15 +78,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
async def _async_update_data_forecast_forecast(): async def _async_update_data_forecast_forecast():
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
return await hass.async_add_job(client.get_forecast, latitude, longitude) return await hass.async_add_executor_job(
client.get_forecast, latitude, longitude
)
async def _async_update_data_rain(): async def _async_update_data_rain():
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
return await hass.async_add_job(client.get_rain, latitude, longitude) return await hass.async_add_executor_job(client.get_rain, latitude, longitude)
async def _async_update_data_alert(): async def _async_update_data_alert():
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
return await hass.async_add_job( return await hass.async_add_executor_job(
client.get_warning_current_phenomenoms, department, 0, True client.get_warning_current_phenomenoms, department, 0, True
) )
@ -156,10 +159,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
entry.title, entry.title,
) )
undo_listener = entry.add_update_listener(_async_update_listener)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_FORECAST: coordinator_forecast,
COORDINATOR_RAIN: coordinator_rain, COORDINATOR_RAIN: coordinator_rain,
COORDINATOR_ALERT: coordinator_alert, COORDINATOR_ALERT: coordinator_alert,
UNDO_UPDATE_LISTENER: undo_listener,
} }
for platform in PLATFORMS: for platform in PLATFORMS:
@ -192,8 +198,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
) )
) )
if unload_ok: if unload_ok:
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) == 0: if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
return unload_ok return unload_ok
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Init MeteoFranceFlowHandler."""
self.places = []
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return MeteoFranceOptionsFlowHandler(config_entry) return MeteoFranceOptionsFlowHandler(config_entry)
async def _show_setup_form(self, user_input=None, errors=None): @callback
def _show_setup_form(self, user_input=None, errors=None):
"""Show the setup form to the user.""" """Show the setup form to the user."""
if user_input is None: if user_input is None:
@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is None: if user_input is None:
return await self._show_setup_form(user_input, errors) return self._show_setup_form(user_input, errors)
city = user_input[CONF_CITY] # Might be a city name or a postal code city = user_input[CONF_CITY] # Might be a city name or a postal code
latitude = user_input.get(CONF_LATITUDE) latitude = user_input.get(CONF_LATITUDE)
@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if not latitude: if not latitude:
client = MeteoFranceClient() client = MeteoFranceClient()
places = await self.hass.async_add_executor_job(client.search_places, city) self.places = await self.hass.async_add_executor_job(
_LOGGER.debug("places search result: %s", places) client.search_places, city
if not places: )
_LOGGER.debug("Places search result: %s", self.places)
if not self.places:
errors[CONF_CITY] = "empty" errors[CONF_CITY] = "empty"
return await self._show_setup_form(user_input, errors) return self._show_setup_form(user_input, errors)
return await self.async_step_cities(places=places) return await self.async_step_cities()
# Check if already configured # Check if already configured
await self.async_set_unique_id(f"{latitude}, {longitude}") await self.async_set_unique_id(f"{latitude}, {longitude}")
@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Import a config entry.""" """Import a config entry."""
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
async def async_step_cities(self, user_input=None, places=None): async def async_step_cities(self, user_input=None):
"""Step where the user choose the city from the API search results.""" """Step where the user choose the city from the API search results."""
if places and len(places) > 1 and self.source != SOURCE_IMPORT: if not user_input:
if len(self.places) > 1 and self.source != SOURCE_IMPORT:
places_for_form = {} places_for_form = {}
for place in places: for place in self.places:
places_for_form[_build_place_key(place)] = f"{place}" places_for_form[_build_place_key(place)] = f"{place}"
return await self._show_cities_form(places_for_form) return self.async_show_form(
# for import and only 1 city in the search result step_id="cities",
if places and not user_input: data_schema=vol.Schema(
user_input = {CONF_CITY: _build_place_key(places[0])} {
vol.Required(CONF_CITY): vol.All(
vol.Coerce(str), vol.In(places_for_form)
)
}
),
)
user_input = {CONF_CITY: _build_place_key(self.places[0])}
city_infos = user_input.get(CONF_CITY).split(";") city_infos = user_input[CONF_CITY].split(";")
return await self.async_step_user( return await self.async_step_user(
{ {
CONF_CITY: city_infos[0], CONF_CITY: city_infos[0],
@ -95,15 +110,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
} }
) )
async def _show_cities_form(self, cities):
"""Show the form to choose the city."""
return self.async_show_form(
step_id="cities",
data_schema=vol.Schema(
{vol.Required(CONF_CITY): vol.All(vol.Coerce(str), vol.In(cities))}
),
)
class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow): class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow.""" """Handle a option flow."""

View File

@ -1,6 +1,9 @@
"""Meteo-France component constants.""" """Meteo-France component constants."""
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
PRESSURE_HPA, PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR, SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"]
COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_FORECAST = "coordinator_forecast"
COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_RAIN = "coordinator_rain"
COORDINATOR_ALERT = "coordinator_alert" COORDINATOR_ALERT = "coordinator_alert"
UNDO_UPDATE_LISTENER = "undo_update_listener"
ATTRIBUTION = "Data provided by Météo-France" ATTRIBUTION = "Data provided by Météo-France"
CONF_CITY = "city" CONF_CITY = "city"
@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast"
ENTITY_NAME = "name" ENTITY_NAME = "name"
ENTITY_UNIT = "unit" ENTITY_UNIT = "unit"
ENTITY_ICON = "icon" ENTITY_ICON = "icon"
ENTITY_CLASS = "device_class" ENTITY_DEVICE_CLASS = "device_class"
ENTITY_ENABLE = "enable" ENTITY_ENABLE = "enable"
ENTITY_API_DATA_PATH = "data_path" ENTITY_API_DATA_PATH = "data_path"
@ -32,8 +36,8 @@ SENSOR_TYPES = {
"pressure": { "pressure": {
ENTITY_NAME: "Pressure", ENTITY_NAME: "Pressure",
ENTITY_UNIT: PRESSURE_HPA, ENTITY_UNIT: PRESSURE_HPA,
ENTITY_ICON: "mdi:gauge", ENTITY_ICON: None,
ENTITY_CLASS: "pressure", ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:sea_level", ENTITY_API_DATA_PATH: "current_forecast:sea_level",
}, },
@ -41,7 +45,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Rain chance", ENTITY_NAME: "Rain chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-rainy", ENTITY_ICON: "mdi:weather-rainy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:rain:3h", ENTITY_API_DATA_PATH: "probability_forecast:rain:3h",
}, },
@ -49,7 +53,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Snow chance", ENTITY_NAME: "Snow chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-snowy", ENTITY_ICON: "mdi:weather-snowy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:snow:3h", ENTITY_API_DATA_PATH: "probability_forecast:snow:3h",
}, },
@ -57,7 +61,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Freeze chance", ENTITY_NAME: "Freeze chance",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:snowflake", ENTITY_ICON: "mdi:snowflake",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "probability_forecast:freezing", ENTITY_API_DATA_PATH: "probability_forecast:freezing",
}, },
@ -65,23 +69,23 @@ SENSOR_TYPES = {
ENTITY_NAME: "Wind speed", ENTITY_NAME: "Wind speed",
ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR,
ENTITY_ICON: "mdi:weather-windy", ENTITY_ICON: "mdi:weather-windy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:wind:speed", ENTITY_API_DATA_PATH: "current_forecast:wind:speed",
}, },
"next_rain": { "next_rain": {
ENTITY_NAME: "Next rain", ENTITY_NAME: "Next rain",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-pouring", ENTITY_ICON: None,
ENTITY_CLASS: "timestamp", ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None, ENTITY_API_DATA_PATH: None,
}, },
"temperature": { "temperature": {
ENTITY_NAME: "Temperature", ENTITY_NAME: "Temperature",
ENTITY_UNIT: TEMP_CELSIUS, ENTITY_UNIT: TEMP_CELSIUS,
ENTITY_ICON: "mdi:thermometer", ENTITY_ICON: None,
ENTITY_CLASS: "temperature", ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ENTITY_ENABLE: False, ENTITY_ENABLE: False,
ENTITY_API_DATA_PATH: "current_forecast:T:value", ENTITY_API_DATA_PATH: "current_forecast:T:value",
}, },
@ -89,7 +93,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "UV", ENTITY_NAME: "UV",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:sunglasses", ENTITY_ICON: "mdi:sunglasses",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:uv", ENTITY_API_DATA_PATH: "today_forecast:uv",
}, },
@ -97,7 +101,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Weather alert", ENTITY_NAME: "Weather alert",
ENTITY_UNIT: None, ENTITY_UNIT: None,
ENTITY_ICON: "mdi:weather-cloudy-alert", ENTITY_ICON: "mdi:weather-cloudy-alert",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: None, ENTITY_API_DATA_PATH: None,
}, },
@ -105,7 +109,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Daily precipitation", ENTITY_NAME: "Daily precipitation",
ENTITY_UNIT: "mm", ENTITY_UNIT: "mm",
ENTITY_ICON: "mdi:cup-water", ENTITY_ICON: "mdi:cup-water",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h", ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h",
}, },
@ -113,7 +117,7 @@ SENSOR_TYPES = {
ENTITY_NAME: "Cloud cover", ENTITY_NAME: "Cloud cover",
ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_UNIT: UNIT_PERCENTAGE,
ENTITY_ICON: "mdi:weather-partly-cloudy", ENTITY_ICON: "mdi:weather-partly-cloudy",
ENTITY_CLASS: None, ENTITY_DEVICE_CLASS: None,
ENTITY_ENABLE: True, ENTITY_ENABLE: True,
ENTITY_API_DATA_PATH: "current_forecast:clouds", ENTITY_API_DATA_PATH: "current_forecast:clouds",
}, },
@ -128,7 +132,7 @@ CONDITION_CLASSES = {
"Brouillard", "Brouillard",
"Brouillard givrant", "Brouillard givrant",
], ],
"hail": ["Risque de grêle"], "hail": ["Risque de grêle", "Risque de grèle"],
"lightning": ["Risque d'orages", "Orages"], "lightning": ["Risque d'orages", "Orages"],
"lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
"partlycloudy": [ "partlycloudy": [

View File

@ -21,7 +21,7 @@ from .const import (
COORDINATOR_RAIN, COORDINATOR_RAIN,
DOMAIN, DOMAIN,
ENTITY_API_DATA_PATH, ENTITY_API_DATA_PATH,
ENTITY_CLASS, ENTITY_DEVICE_CLASS,
ENTITY_ENABLE, ENTITY_ENABLE,
ENTITY_ICON, ENTITY_ICON,
ENTITY_NAME, ENTITY_NAME,
@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity):
@property @property
def device_class(self): def device_class(self):
"""Return the device class.""" """Return the device class."""
return SENSOR_TYPES[self._type][ENTITY_CLASS] return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS]
@property @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:
@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
@property @property
def state(self): def state(self):
"""Return the state.""" """Return the state."""
next_rain_date_locale = self.coordinator.data.next_rain_date_locale() # search first cadran with rain
next_rain = next(
(cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1),
None,
)
return ( return (
dt_util.as_local(next_rain_date_locale) if next_rain_date_locale else None dt_util.utc_from_timestamp(next_rain["dt"]).isoformat()
if next_rain
else None
) )
@property @property
@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_NEXT_RAIN_1_HOUR_FORECAST: [ ATTR_NEXT_RAIN_1_HOUR_FORECAST: [
{ {dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]}
dt_util.as_local(
self.coordinator.data.timestamp_to_locale_time(item["dt"])
).strftime("%H:%M"): item["desc"]
}
for item in self.coordinator.data.forecast for item in self.coordinator.data.forecast
], ],
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,

View File

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.const import CONF_MODE, TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTRIBUTION, ATTRIBUTION,
@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity):
continue continue
forecast_data.append( forecast_data.append(
{ {
ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( ATTR_FORECAST_TIME: dt_util.utc_from_timestamp(
forecast["dt"] forecast["dt"]
), ).isoformat(),
ATTR_FORECAST_CONDITION: format_condition( ATTR_FORECAST_CONDITION: format_condition(
forecast["weather"]["desc"] forecast["weather"]["desc"]
), ),