diff --git a/.coveragerc b/.coveragerc index cc2402e480b..8fe53ec8282 100644 --- a/.coveragerc +++ b/.coveragerc @@ -413,7 +413,10 @@ omit = homeassistant/components/mediaroom/media_player.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py - homeassistant/components/meteo_france/* + homeassistant/components/meteo_france/__init__.py + homeassistant/components/meteo_france/const.py + homeassistant/components/meteo_france/sensor.py + homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py diff --git a/CODEOWNERS b/CODEOWNERS index 5a66f80a1d0..4df7f250d60 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,7 +206,7 @@ homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen -homeassistant/components/meteo_france/* @victorcerutti @oncleben31 +homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mikrotik/* @engrbm87 diff --git a/homeassistant/components/meteo_france/.translations/en.json b/homeassistant/components/meteo_france/.translations/en.json new file mode 100644 index 00000000000..804ad9d67b1 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "City already configured", + "unknown": "Unknown error: please retry later" + }, + "step": { + "user": { + "data": { + "city": "City" + }, + "description": "Enter the postal code (only for France, recommended) or city name", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 73b8dbb0e39..b7eda51b955 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,4 +1,5 @@ """Support for Meteo-France weather data.""" +import asyncio import datetime import logging @@ -6,116 +7,96 @@ from meteofrance.client import meteofranceClient, meteofranceError from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle -from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES +from .const import CONF_CITY, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=5) -def has_all_unique_cities(value): - """Validate that all cities are unique.""" - cities = [location[CONF_CITY] for location in value] - vol.Schema(vol.Unique())(cities) - return value - +CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string}) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_CITY): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } - ) - ], - has_all_unique_cities, - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): - """Set up the Meteo-France component.""" - hass.data[DATA_METEO_FRANCE] = {} +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up Meteo-France from legacy config file.""" - # Check if at least weather alert have to be monitored for one location. - need_weather_alert_watcher = False - for location in config[DOMAIN]: - if ( - CONF_MONITORED_CONDITIONS in location - and "weather_alert" in location[CONF_MONITORED_CONDITIONS] - ): - need_weather_alert_watcher = True + conf = config.get(DOMAIN) + if conf is None: + return True - # If weather alert monitoring is expected initiate a client to be used by - # all weather_alert entities. - if need_weather_alert_watcher: - _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") - - weather_alert_client = VigilanceMeteoFranceProxy() - try: - weather_alert_client.update_data() - except VigilanceMeteoError as exp: - _LOGGER.error( - "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", - exp, + for city_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=city_conf.copy() ) - else: - weather_alert_client = None - hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client - - for location in config[DOMAIN]: - - city = location[CONF_CITY] - - try: - client = meteofranceClient(city) - except meteofranceError as exp: - _LOGGER.error( - "Unexpected error when creating the meteofrance proxy: %s", exp - ) - return - - client.need_rain_forecast = bool( - CONF_MONITORED_CONDITIONS in location - and "next_rain" in location[CONF_MONITORED_CONDITIONS] ) - hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) - hass.data[DATA_METEO_FRANCE][city].update() - - if CONF_MONITORED_CONDITIONS in location: - monitored_conditions = location[CONF_MONITORED_CONDITIONS] - _LOGGER.debug("meteo_france sensor platform loaded for %s", city) - load_platform( - hass, - "sensor", - DOMAIN, - {CONF_CITY: city, CONF_MONITORED_CONDITIONS: monitored_conditions}, - config, - ) - - load_platform(hass, "weather", DOMAIN, {CONF_CITY: city}, config) - return True +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an Meteo-France account from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # Weather alert + weather_alert_client = VigilanceMeteoFranceProxy() + try: + await hass.async_add_executor_job(weather_alert_client.update_data) + except VigilanceMeteoError as exp: + _LOGGER.error( + "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp + ) + return False + hass.data[DOMAIN]["weather_alert_client"] = weather_alert_client + + # Weather + city = entry.data[CONF_CITY] + try: + client = await hass.async_add_executor_job(meteofranceClient, city) + except meteofranceError as exp: + _LOGGER.error("Unexpected error when creating the meteofrance proxy: %s", exp) + return False + + hass.data[DOMAIN][city] = MeteoFranceUpdater(client) + await hass.async_add_executor_job(hass.data[DOMAIN][city].update) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + _LOGGER.debug("meteo_france sensor platform loaded for %s", city) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.data[CONF_CITY]) + + return unload_ok + + class MeteoFranceUpdater: """Update data from Meteo-France.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialize the data object.""" self._client = client diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py new file mode 100644 index 00000000000..c7673020360 --- /dev/null +++ b/homeassistant/components/meteo_france/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow to configure the Meteo-France integration.""" +import logging + +from meteofrance.client import meteofranceClient, meteofranceError +import voluptuous as vol + +from homeassistant import config_entries + +from .const import CONF_CITY +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Meteo-France config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_CITY, default=user_input.get(CONF_CITY, "")): str} + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, errors) + + city = user_input[CONF_CITY] # Might be a city name or a postal code + city_name = None + + try: + client = await self.hass.async_add_executor_job(meteofranceClient, city) + city_name = client.get_data()["name"] + except meteofranceError as exp: + _LOGGER.error( + "Unexpected error when creating the meteofrance proxy: %s", exp + ) + return self.async_abort(reason="unknown") + + # Check if already configured + await self.async_set_unique_id(city_name) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=city_name, data={CONF_CITY: city}) + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 223aca20bac..fae2000b19a 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -3,7 +3,7 @@ from homeassistant.const import TEMP_CELSIUS DOMAIN = "meteo_france" -DATA_METEO_FRANCE = "data_meteo_france" +PLATFORMS = ["sensor", "weather"] ATTRIBUTION = "Data provided by Météo-France" CONF_CITY = "city" diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 41a003ea4f7..77f8fca984d 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,8 +1,9 @@ { "domain": "meteo_france", "name": "Météo-France", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"], "dependencies": [], - "codeowners": ["@victorcerutti", "@oncleben31"] + "codeowners": ["@victorcerutti", "@oncleben31", "@Quentame"] } diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index f0c08ac1822..cf28b9ea558 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,15 +1,18 @@ """Support for Meteo-France raining forecast sensor.""" import logging -from vigilancemeteo import DepartmentWeatherAlert +from meteofrance.client import meteofranceClient +from vigilancemeteo import DepartmentWeatherAlert, VigilanceMeteoFranceProxy -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTRIBUTION, CONF_CITY, - DATA_METEO_FRANCE, + DOMAIN, SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, @@ -23,52 +26,47 @@ STATE_ATTR_FORECAST = "1h rain forecast" STATE_ATTR_BULLETIN_TIME = "Bulletin date" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Meteo-France sensor.""" - if discovery_info is None: - return - - city = discovery_info[CONF_CITY] - monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] - client = hass.data[DATA_METEO_FRANCE][city] - weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Meteo-France sensor platform.""" + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] + weather_alert_client = hass.data[DOMAIN]["weather_alert_client"] alert_watcher = None - if "weather_alert" in monitored_conditions: - datas = hass.data[DATA_METEO_FRANCE][city].get_data() - # Check if a department code is available for this city. - if "dept" in datas: - try: - # If yes create the watcher DepartmentWeatherAlert object. - alert_watcher = DepartmentWeatherAlert( - datas["dept"], weather_alert_client - ) - except ValueError as exp: - _LOGGER.error( - "Unexpected error when creating the weather alert sensor for %s in department %s: %s", - city, - datas["dept"], - exp, - ) - alert_watcher = None - else: - _LOGGER.info( - "Weather alert watcher added for %s in department %s", - city, - datas["dept"], - ) - else: - _LOGGER.warning( - "No 'dept' key found for '%s'. So weather alert information won't be available", - city, + datas = client.get_data() + # Check if a department code is available for this city. + if "dept" in datas: + try: + # If yes create the watcher DepartmentWeatherAlert object. + alert_watcher = await hass.async_add_executor_job( + DepartmentWeatherAlert, datas["dept"], weather_alert_client ) - # Exit and don't create the sensor if no department code available. - return + _LOGGER.info( + "Weather alert watcher added for %s in department %s", + city, + datas["dept"], + ) + except ValueError as exp: + _LOGGER.error( + "Unexpected error when creating the weather alert sensor for %s in department %s: %s", + city, + datas["dept"], + exp, + ) + else: + _LOGGER.warning( + "No 'dept' key found for '%s'. So weather alert information won't be available", + city, + ) + # Exit and don't create the sensor if no department code available. + return - add_entities( + async_add_entities( [ - MeteoFranceSensor(variable, client, alert_watcher) - for variable in monitored_conditions + MeteoFranceSensor(sensor_type, client, alert_watcher) + for sensor_type in SENSOR_TYPES ], True, ) @@ -77,9 +75,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MeteoFranceSensor(Entity): """Representation of a Meteo-France sensor.""" - def __init__(self, condition, client, alert_watcher): + def __init__( + self, + sensor_type: str, + client: meteofranceClient, + alert_watcher: VigilanceMeteoFranceProxy, + ): """Initialize the Meteo-France sensor.""" - self._condition = condition + self._type = sensor_type self._client = client self._alert_watcher = alert_watcher self._state = None @@ -88,7 +91,12 @@ class MeteoFranceSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}" + return f"{self._data['name']} {SENSOR_TYPES[self._type][SENSOR_TYPE_NAME]}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name @property def state(self): @@ -99,7 +107,7 @@ class MeteoFranceSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" # Attributes for next_rain sensor. - if self._condition == "next_rain" and "rain_forecast" in self._data: + if self._type == "next_rain" and "rain_forecast" in self._data: return { **{STATE_ATTR_FORECAST: self._data["rain_forecast"]}, **self._data["next_rain_intervals"], @@ -107,7 +115,7 @@ class MeteoFranceSensor(Entity): } # Attributes for weather_alert sensor. - if self._condition == "weather_alert" and self._alert_watcher is not None: + if self._type == "weather_alert" and self._alert_watcher is not None: return { **{STATE_ATTR_BULLETIN_TIME: self._alert_watcher.bulletin_date}, **self._alert_watcher.alerts_list, @@ -120,17 +128,17 @@ class MeteoFranceSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT] + return SENSOR_TYPES[self._type][SENSOR_TYPE_UNIT] @property def icon(self): """Return the icon.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON] + return SENSOR_TYPES[self._type][SENSOR_TYPE_ICON] @property def device_class(self): """Return the device class of the sensor.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS] + return SENSOR_TYPES[self._type][SENSOR_TYPE_CLASS] def update(self): """Fetch new state data for the sensor.""" @@ -138,13 +146,12 @@ class MeteoFranceSensor(Entity): self._client.update() self._data = self._client.get_data() - if self._condition == "weather_alert": + if self._type == "weather_alert": if self._alert_watcher is not None: self._alert_watcher.update_department_status() self._state = self._alert_watcher.department_color _LOGGER.debug( - "weather alert watcher for %s updated. Proxy" - " have the status: %s", + "weather alert watcher for %s updated. Proxy have the status: %s", self._data["name"], self._alert_watcher.proxy.status, ) @@ -153,9 +160,9 @@ class MeteoFranceSensor(Entity): "No weather alert data for location %s", self._data["name"] ) else: - self._state = self._data[self._condition] + self._state = self._data[self._type] except KeyError: _LOGGER.error( - "No condition %s for location %s", self._condition, self._data["name"] + "No condition %s for location %s", self._type, self._data["name"] ) self._state = None diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json new file mode 100644 index 00000000000..8bb02f28bd0 --- /dev/null +++ b/homeassistant/components/meteo_france/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Météo-France", + "step": { + "user": { + "title": "Météo-France", + "description": "Enter the postal code (only for France, recommended) or city name", + "data": { + "city": "City" + } + } + }, + "abort":{ + "already_configured": "City already configured", + "unknown": "Unknown error: please retry later" + } + } +} diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index c96080808e9..1bdea073aae 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from meteofrance.client import meteofranceClient + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -9,29 +11,30 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util -from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE +from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the Meteo-France weather platform.""" - if discovery_info is None: - return + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] - city = discovery_info[CONF_CITY] - client = hass.data[DATA_METEO_FRANCE][city] - - add_entities([MeteoFranceWeather(client)], True) + async_add_entities([MeteoFranceWeather(client)], True) class MeteoFranceWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialise the platform with a data instance and station name.""" self._client = client self._data = {} @@ -46,6 +49,11 @@ class MeteoFranceWeather(WeatherEntity): """Return the name of the sensor.""" return self._data["name"] + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name + @property def condition(self): """Return the current condition.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf77dae7fb2..ea8a0a4e82d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "luftdaten", "mailgun", "met", + "meteo_france", "mikrotik", "mobile_app", "mqtt", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 527a0eae504..904df51f68f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,6 +298,9 @@ luftdaten==0.6.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 +# homeassistant.components.meteo_france +meteofrance==0.3.7 + # homeassistant.components.mfi mficlient==0.3.0 @@ -663,6 +666,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.meteo_france +vigilancemeteo==3.0.0 + # homeassistant.components.verisure vsure==1.5.4 diff --git a/tests/components/meteo_france/__init__.py b/tests/components/meteo_france/__init__.py new file mode 100644 index 00000000000..c4d4c446574 --- /dev/null +++ b/tests/components/meteo_france/__init__.py @@ -0,0 +1 @@ +"""Tests for the Meteo-France component.""" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py new file mode 100644 index 00000000000..f9ead2c1ef3 --- /dev/null +++ b/tests/components/meteo_france/test_config_flow.py @@ -0,0 +1,128 @@ +"""Tests for the Meteo-France config flow.""" +from unittest.mock import patch + +from meteofrance.client import meteofranceError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER + +from tests.common import MockConfigEntry + +CITY_1_POSTAL = "74220" +CITY_1_NAME = "La Clusaz" +CITY_2_POSTAL_DISTRICT_1 = "69001" +CITY_2_POSTAL_DISTRICT_4 = "69004" +CITY_2_NAME = "Lyon" + + +@pytest.fixture(name="client_1") +def mock_controller_client_1(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = {"name": CITY_1_NAME} + yield service_mock + + +@pytest.fixture(name="client_2") +def mock_controller_client_2(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = {"name": CITY_2_NAME} + yield service_mock + + +async def test_user(hass, client_1): + """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == CITY_1_NAME + assert result["title"] == CITY_1_NAME + assert result["data"][CONF_CITY] == CITY_1_POSTAL + + +async def test_import(hass, client_1): + """Test import step.""" + # import with all + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == CITY_1_NAME + assert result["title"] == CITY_1_NAME + assert result["data"][CONF_CITY] == CITY_1_POSTAL + + +async def test_abort_if_already_setup(hass, client_1): + """Test we abort if already setup.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_CITY: CITY_1_POSTAL}, unique_id=CITY_1_NAME + ).add_to_hass(hass) + + # Should fail, same CITY same postal code (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same CITY same postal code (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_if_already_setup_district(hass, client_2): + """Test we abort if already setup.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_CITY: CITY_2_POSTAL_DISTRICT_1}, unique_id=CITY_2_NAME + ).add_to_hass(hass) + + # Should fail, same CITY different postal code (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same CITY different postal code (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_2_POSTAL_DISTRICT_4}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_client_failed(hass): + """Test when we have errors during client fetch.""" + with patch( + "homeassistant.components.meteo_france.config_flow.meteofranceClient", + side_effect=meteofranceError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown"