diff --git a/.coveragerc b/.coveragerc index 2845a1768a8..624037946c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1102,6 +1102,8 @@ omit = homeassistant/components/waterfurnace/* homeassistant/components/watson_iot/* homeassistant/components/watson_tts/tts.py + homeassistant/components/waze_travel_time/__init__.py + homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/whois/sensor.py diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 9674bd9850e..20a0c01c642 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1 +1,29 @@ """The waze_travel_time component.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Load the saved entities.""" + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + ) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py new file mode 100644 index 00000000000..05dd372f9d9 --- /dev/null +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -0,0 +1,149 @@ +"""Config flow for Waze Travel Time integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_REGION +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify + +from .const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DEFAULT_NAME, + DOMAIN, + REGIONS, + UNITS, + VEHICLE_TYPES, +) +from .helpers import is_valid_config_entry + +_LOGGER = logging.getLogger(__name__) + + +class WazeOptionsFlow(config_entries.OptionsFlow): + """Handle an options flow for Waze Travel Time.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize waze options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_INCL_FILTER, + default=self.config_entry.options.get(CONF_INCL_FILTER), + ): cv.string, + vol.Optional( + CONF_EXCL_FILTER, + default=self.config_entry.options.get(CONF_EXCL_FILTER), + ): cv.string, + vol.Optional( + CONF_REALTIME, + default=self.config_entry.options[CONF_REALTIME], + ): cv.boolean, + vol.Optional( + CONF_VEHICLE_TYPE, + default=self.config_entry.options[CONF_VEHICLE_TYPE], + ): vol.In(VEHICLE_TYPES), + vol.Optional( + CONF_UNITS, + default=self.config_entry.options[CONF_UNITS], + ): vol.In(UNITS), + vol.Optional( + CONF_AVOID_TOLL_ROADS, + default=self.config_entry.options[CONF_AVOID_TOLL_ROADS], + ): cv.boolean, + vol.Optional( + CONF_AVOID_SUBSCRIPTION_ROADS, + default=self.config_entry.options[ + CONF_AVOID_SUBSCRIPTION_ROADS + ], + ): cv.boolean, + vol.Optional( + CONF_AVOID_FERRIES, + default=self.config_entry.options[CONF_AVOID_FERRIES], + ): cv.boolean, + } + ), + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Waze Travel Time.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> WazeOptionsFlow: + """Get the options flow for this handler.""" + return WazeOptionsFlow(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + user_input[CONF_REGION], + ): + await self.async_set_unique_id( + slugify( + f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" + ) + ) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=( + user_input.get( + CONF_NAME, + ( + f"{DEFAULT_NAME}: {user_input[CONF_ORIGIN]} -> " + f"{user_input[CONF_DESTINATION]}" + ), + ) + ), + data=user_input, + ) + + # If we get here, it's because we couldn't connect + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_REGION): vol.In(REGIONS), + } + ), + errors=errors, + ) + + async_step_import = async_step_user diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py new file mode 100644 index 00000000000..1b89fd5e282 --- /dev/null +++ b/homeassistant/components/waze_travel_time/const.py @@ -0,0 +1,40 @@ +"""Constants for waze_travel_time.""" +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC + +DOMAIN = "waze_travel_time" + +ATTR_DESTINATION = "destination" +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ORIGIN = "origin" +ATTR_ROUTE = "route" + +ATTRIBUTION = "Powered by Waze" + +CONF_DESTINATION = "destination" +CONF_ORIGIN = "origin" +CONF_INCL_FILTER = "incl_filter" +CONF_EXCL_FILTER = "excl_filter" +CONF_REALTIME = "realtime" +CONF_UNITS = "units" +CONF_VEHICLE_TYPE = "vehicle_type" +CONF_AVOID_TOLL_ROADS = "avoid_toll_roads" +CONF_AVOID_SUBSCRIPTION_ROADS = "avoid_subscription_roads" +CONF_AVOID_FERRIES = "avoid_ferries" + +DEFAULT_NAME = "Waze Travel Time" +DEFAULT_REALTIME = True +DEFAULT_VEHICLE_TYPE = "car" +DEFAULT_AVOID_TOLL_ROADS = False +DEFAULT_AVOID_SUBSCRIPTION_ROADS = False +DEFAULT_AVOID_FERRIES = False + +ICON = "mdi:car" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +REGIONS = ["US", "NA", "EU", "IL", "AU"] +VEHICLE_TYPES = ["car", "taxi", "motorcycle"] + +# Attempt to find entity_id without finding address with period. +ENTITY_ID_PATTERN = "(? None: + """Set up a Waze travel time sensor entry.""" + defaults = { + CONF_REALTIME: DEFAULT_REALTIME, + CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE, + CONF_UNITS: hass.config.units.name, + CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, + } + name = None + if not config_entry.options: + new_data = config_entry.data.copy() + name = new_data.pop(CONF_NAME, None) + options = {} + for key in [ + CONF_INCL_FILTER, + CONF_EXCL_FILTER, + CONF_REALTIME, + CONF_VEHICLE_TYPE, + CONF_AVOID_TOLL_ROADS, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_FERRIES, + CONF_UNITS, + ]: + if key in new_data: + options[key] = new_data.pop(key) + elif key in defaults: + options[key] = defaults[key] + + hass.config_entries.async_update_entry( + config_entry, data=new_data, options=options + ) + + destination = config_entry.data[CONF_DESTINATION] + origin = config_entry.data[CONF_ORIGIN] + region = config_entry.data[CONF_REGION] + name = name or f"{DEFAULT_NAME}: {origin} -> {destination}" + + if not await hass.async_add_executor_job( + is_valid_config_entry, hass, _LOGGER, origin, destination, region + ): + raise ConfigEntryNotReady data = WazeTravelTimeData( None, None, region, - incl_filter, - excl_filter, - realtime, - units, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, + config_entry, ) - sensor = WazeTravelTime(name, origin, destination, data) + sensor = WazeTravelTime(config_entry.unique_id, name, origin, destination, data) - add_entities([sensor]) - - # Wait until start event is sent to load this component. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: sensor.update()) - - -def _get_location_from_attributes(state): - """Get the lat/long string from an states attributes.""" - attr = state.attributes - return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + async_add_entities([sensor], False) class WazeTravelTime(SensorEntity): """Representation of a Waze travel time sensor.""" - def __init__(self, name, origin, destination, waze_data): + def __init__(self, unique_id, name, origin, destination, waze_data): """Initialize the Waze travel time sensor.""" - self._name = name + self._unique_id = unique_id self._waze_data = waze_data + self._name = name self._state = None self._origin_entity_id = None self._destination_entity_id = None - - # Attempt to find entity_id without finding address with period. - pattern = "(? None: + """Handle when entity is added.""" + if self.hass.state != CoreState.running: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self.first_update + ) + else: + await self.first_update() + @property def name(self): """Return the name of the sensor.""" @@ -188,150 +233,118 @@ class WazeTravelTime(SensorEntity): res[ATTR_DESTINATION] = self._waze_data.destination return res - def _get_location_from_entity(self, entity_id): - """Get the location from the entity_id.""" - state = self.hass.states.get(entity_id) - - if state is None: - _LOGGER.error("Unable to find entity %s", entity_id) - return None - - # Check if the entity has location attributes. - if location.has_location(state): - _LOGGER.debug("Getting %s location", entity_id) - return _get_location_from_attributes(state) - - # Check if device is inside a zone. - zone_state = self.hass.states.get(f"zone.{state.state}") - if location.has_location(zone_state): - _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_state.entity_id - ) - return _get_location_from_attributes(zone_state) - - # If zone was not found in state then use the state as the location. - if entity_id.startswith("sensor."): - return state.state - - # When everything fails just return nothing. - return None - - def _resolve_zone(self, friendly_name): - """Get a lat/long from a zones friendly_name.""" - states = self.hass.states.all() - for state in states: - if state.domain == "zone" and state.name == friendly_name: - return _get_location_from_attributes(state) - - return friendly_name + async def first_update(self, _=None): + """Run first update and write state.""" + await self.hass.async_add_executor_job(self.update) + self.async_write_ha_state() def update(self): """Fetch new state data for the sensor.""" _LOGGER.debug("Fetching Route for %s", self._name) # Get origin latitude and longitude from entity_id. if self._origin_entity_id is not None: - self._waze_data.origin = self._get_location_from_entity( - self._origin_entity_id + self._waze_data.origin = get_location_from_entity( + self.hass, _LOGGER, self._origin_entity_id ) # Get destination latitude and longitude from entity_id. if self._destination_entity_id is not None: - self._waze_data.destination = self._get_location_from_entity( - self._destination_entity_id + self._waze_data.destination = get_location_from_entity( + self.hass, _LOGGER, self._destination_entity_id ) # Get origin from zone name. - self._waze_data.origin = self._resolve_zone(self._waze_data.origin) + self._waze_data.origin = resolve_zone(self.hass, self._waze_data.origin) # Get destination from zone name. - self._waze_data.destination = self._resolve_zone(self._waze_data.destination) + self._waze_data.destination = resolve_zone( + self.hass, self._waze_data.destination + ) self._waze_data.update() + @property + def device_info(self) -> dict[str, Any] | None: + """Return device specific attributes.""" + return { + "name": "Waze", + "identifiers": {(DOMAIN, DOMAIN)}, + "entry_type": "service", + } + + @property + def unique_id(self) -> str: + """Return unique ID of entity.""" + return self._unique_id + class WazeTravelTimeData: """WazeTravelTime Data object.""" - def __init__( - self, - origin, - destination, - region, - include, - exclude, - realtime, - units, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, - ): + def __init__(self, origin, destination, region, config_entry): """Set up WazeRouteCalculator.""" - - self._calc = WazeRouteCalculator - self.origin = origin self.destination = destination self.region = region - self.include = include - self.exclude = exclude - self.realtime = realtime - self.units = units + self.config_entry = config_entry self.duration = None self.distance = None self.route = None - self.avoid_toll_roads = avoid_toll_roads - self.avoid_subscription_roads = avoid_subscription_roads - self.avoid_ferries = avoid_ferries - - # Currently WazeRouteCalc only supports PRIVATE, TAXI, MOTORCYCLE. - if vehicle_type.upper() == "CAR": - # Empty means PRIVATE for waze which translates to car. - self.vehicle_type = "" - else: - self.vehicle_type = vehicle_type.upper() def update(self): """Update WazeRouteCalculator Sensor.""" if self.origin is not None and self.destination is not None: + # Grab options on every update + incl_filter = self.config_entry.options.get(CONF_INCL_FILTER) + excl_filter = self.config_entry.options.get(CONF_EXCL_FILTER) + realtime = self.config_entry.options[CONF_REALTIME] + vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE] + vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() + avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS] + avoid_subscription_roads = self.config_entry.options[ + CONF_AVOID_SUBSCRIPTION_ROADS + ] + avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] + units = self.config_entry.options[CONF_UNITS] + try: - params = self._calc.WazeRouteCalculator( + params = WazeRouteCalculator( self.origin, self.destination, self.region, - self.vehicle_type, - self.avoid_toll_roads, - self.avoid_subscription_roads, - self.avoid_ferries, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ) - routes = params.calc_all_routes_info(real_time=self.realtime) + routes = params.calc_all_routes_info(real_time=realtime) - if self.include is not None: + if incl_filter is not None: routes = { k: v for k, v in routes.items() - if self.include.lower() in k.lower() + if incl_filter.lower() in k.lower() } - if self.exclude is not None: + if excl_filter is not None: routes = { k: v for k, v in routes.items() - if self.exclude.lower() not in k.lower() + if excl_filter.lower() not in k.lower() } route = list(routes)[0] self.duration, distance = routes[route] - if self.units == CONF_UNIT_SYSTEM_IMPERIAL: + if units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. self.distance = distance / 1.609 else: self.distance = distance self.route = route - except self._calc.WRCError as exp: + except WRCError as exp: _LOGGER.warning("Error on retrieving data: %s", exp) return except KeyError: diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json new file mode 100644 index 00000000000..082ee31db73 --- /dev/null +++ b/homeassistant/components/waze_travel_time/strings.json @@ -0,0 +1,38 @@ +{ + "title": "Waze Travel Time", + "config": { + "step": { + "user": { + "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name.", + "data": { + "origin": "Origin", + "destination": "Destination", + "region": "Region" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation.", + "data": { + "units": "Units", + "vehicle_type": "Vehicle Type", + "incl_filter": "Substring in Description of Selected Route", + "excl_filter": "Substring NOT in Description of Selected Route", + "realtime": "Realtime Travel Time?", + "avoid_toll_roads": "Avoid Toll Roads?", + "avoid_ferries": "Avoid Ferries?", + "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/en.json b/homeassistant/components/waze_travel_time/translations/en.json new file mode 100644 index 00000000000..4b113302cda --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Origin", + "region": "Region" + }, + "description": "For Origin and Destination, enter the address or the GPS coordinates of the location (GPS coordinates has to be separated by a comma). You can also enter an entity id which provides this information in its state, an entity id with latitude and longitude attributes, or zone friendly name." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Avoid Ferries?", + "avoid_subscription_roads": "Avoid Roads Needing a Vignette / Subscription?", + "avoid_toll_roads": "Avoid Toll Roads?", + "excl_filter": "Substring NOT in Description of Selected Route", + "incl_filter": "Substring in Description of Selected Route", + "realtime": "Realtime Travel Time?", + "units": "Units", + "vehicle_type": "Vehicle Type" + }, + "description": "The `substring` inputs will allow you to force the integration to use a particular route or avoid a particular route in its time travel calculation." + } + } + }, + "title": "Waze Travel Time" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 26c1b55c923..e9eece903fc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -258,6 +258,7 @@ FLOWS = [ "vilfo", "vizio", "volumio", + "waze_travel_time", "wemo", "wiffi", "wilight", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44090efa0b0..64239d08288 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -38,6 +38,9 @@ RtmAPI==0.7.2 # homeassistant.components.onvif WSDiscovery==2.0.0 +# homeassistant.components.waze_travel_time +WazeRouteCalculator==0.12 + # homeassistant.components.abode abodepy==1.2.0 diff --git a/tests/components/waze_travel_time/__init__.py b/tests/components/waze_travel_time/__init__.py new file mode 100644 index 00000000000..1df3d9314d0 --- /dev/null +++ b/tests/components/waze_travel_time/__init__.py @@ -0,0 +1 @@ +"""Tests for the Waze Travel Time integration.""" diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py new file mode 100644 index 00000000000..dd5b343cc16 --- /dev/null +++ b/tests/components/waze_travel_time/conftest.py @@ -0,0 +1,57 @@ +"""Fixtures for Waze Travel Time tests.""" +from unittest.mock import patch + +from WazeRouteCalculator import WRCError +import pytest + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="validate_config_entry") +def validate_config_entry_fixture(): + """Return valid config entry.""" + with patch( + "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" + ) as mock_wrc: + obj = mock_wrc.return_value + obj.calc_all_routes_info.return_value = None + yield + + +@pytest.fixture(name="bypass_setup") +def bypass_setup_fixture(): + """Bypass entry setup.""" + with patch( + "homeassistant.components.waze_travel_time.async_setup_entry", + return_value=True, + ): + yield + + +@pytest.fixture(name="mock_update") +def mock_update_fixture(): + """Mock an update to the sensor.""" + with patch( + "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator.calc_all_routes_info", + return_value={"My route": (150, 300)}, + ): + yield + + +@pytest.fixture(name="invalidate_config_entry") +def invalidate_config_entry_fixture(): + """Return invalid config entry.""" + with patch( + "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" + ) as mock_wrc: + obj = mock_wrc.return_value + obj.calc_all_routes_info.return_value = {} + obj.calc_all_routes_info.side_effect = WRCError("test") + yield diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py new file mode 100644 index 00000000000..f6f1614ca25 --- /dev/null +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -0,0 +1,205 @@ +"""Test the Waze Travel Time config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.waze_travel_time.const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DEFAULT_NAME, + DOMAIN, +) +from homeassistant.const import CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL + +from tests.common import MockConfigEntry + + +async def test_minimum_fields(hass, validate_config_entry, bypass_setup): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{DEFAULT_NAME}: location1 -> location2" + assert result2["data"] == { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + } + + +async def test_options(hass, validate_config_entry, mock_update): + """Test options flow.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"] == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + assert entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + +async def test_import(hass, validate_config_entry, mock_update): + """Test import for config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + } + assert entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + } + + +async def test_dupe_id(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry twice fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_invalid_config_entry(hass, invalidate_config_entry): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"}