diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 275d47d15ca..9cf73a90a73 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -2,23 +2,16 @@ from __future__ import annotations -from dwdwfsapi import DwdWeatherWarningsAPI - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_REGION_IDENTIFIER, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .coordinator import DwdWeatherWarningsCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - region_identifier: str = entry.data[CONF_REGION_IDENTIFIER] - - # Initialize the API and coordinator. - api = await hass.async_add_executor_job(DwdWeatherWarningsAPI, region_identifier) - coordinator = DwdWeatherWarningsCoordinator(hass, api) - + coordinator = DwdWeatherWarningsCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/dwd_weather_warnings/config_flow.py b/homeassistant/components/dwd_weather_warnings/config_flow.py index 5076dbae187..f148f4e05ac 100644 --- a/homeassistant/components/dwd_weather_warnings/config_flow.py +++ b/homeassistant/components/dwd_weather_warnings/config_flow.py @@ -8,9 +8,15 @@ from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig -from .const import CONF_REGION_IDENTIFIER, DOMAIN +from .const import CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, DOMAIN +from .exceptions import EntityNotFoundError +from .util import get_position_data + +EXCLUSIVE_OPTIONS = (CONF_REGION_IDENTIFIER, CONF_REGION_DEVICE_TRACKER) class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): @@ -25,27 +31,70 @@ class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict = {} if user_input is not None: - region_identifier = user_input[CONF_REGION_IDENTIFIER] + # Check, if either CONF_REGION_IDENTIFIER or CONF_GPS_TRACKER has been set. + if all(k not in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "no_identifier" + elif all(k in user_input for k in EXCLUSIVE_OPTIONS): + errors["base"] = "ambiguous_identifier" + elif CONF_REGION_IDENTIFIER in user_input: + # Validate region identifier using the API + identifier = user_input[CONF_REGION_IDENTIFIER] - # Validate region identifier using the API - if not await self.hass.async_add_executor_job( - DwdWeatherWarningsAPI, region_identifier - ): - errors["base"] = "invalid_identifier" + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, identifier + ): + errors["base"] = "invalid_identifier" - if not errors: - # Set the unique ID for this config entry. - await self.async_set_unique_id(region_identifier) - self._abort_if_unique_id_configured() + if not errors: + # Set the unique ID for this config entry. + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=region_identifier, data=user_input) + return self.async_create_entry(title=identifier, data=user_input) + else: # CONF_REGION_DEVICE_TRACKER + device_tracker = user_input[CONF_REGION_DEVICE_TRACKER] + registry = er.async_get(self.hass) + entity_entry = registry.async_get(device_tracker) + + if entity_entry is None: + errors["base"] = "entity_not_found" + else: + try: + position = get_position_data(self.hass, entity_entry.id) + except EntityNotFoundError: + errors["base"] = "entity_not_found" + except AttributeError: + errors["base"] = "attribute_not_found" + else: + # Validate position using the API + if not await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ): + errors["base"] = "invalid_identifier" + + # Position is valid here, because the API call was successful. + if not errors and position is not None and entity_entry is not None: + # Set the unique ID for this config entry. + await self.async_set_unique_id(entity_entry.id) + self._abort_if_unique_id_configured() + + # Replace entity ID with registry ID for more stability. + user_input[CONF_REGION_DEVICE_TRACKER] = entity_entry.id + + return self.async_create_entry( + title=device_tracker.removeprefix("device_tracker."), + data=user_input, + ) return self.async_show_form( step_id="user", errors=errors, data_schema=vol.Schema( { - vol.Required(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_IDENTIFIER): cv.string, + vol.Optional(CONF_REGION_DEVICE_TRACKER): EntitySelector( + EntitySelectorConfig(domain="device_tracker") + ), } ), ) diff --git a/homeassistant/components/dwd_weather_warnings/const.py b/homeassistant/components/dwd_weather_warnings/const.py index 75969dee119..4f0a6767660 100644 --- a/homeassistant/components/dwd_weather_warnings/const.py +++ b/homeassistant/components/dwd_weather_warnings/const.py @@ -14,6 +14,7 @@ DOMAIN: Final = "dwd_weather_warnings" CONF_REGION_NAME: Final = "region_name" CONF_REGION_IDENTIFIER: Final = "region_identifier" +CONF_REGION_DEVICE_TRACKER: Final = "region_device_tracker" ATTR_REGION_NAME: Final = "region_name" ATTR_REGION_ID: Final = "region_id" diff --git a/homeassistant/components/dwd_weather_warnings/coordinator.py b/homeassistant/components/dwd_weather_warnings/coordinator.py index a1232697130..465a7c09750 100644 --- a/homeassistant/components/dwd_weather_warnings/coordinator.py +++ b/homeassistant/components/dwd_weather_warnings/coordinator.py @@ -4,23 +4,79 @@ from __future__ import annotations from dwdwfsapi import DwdWeatherWarningsAPI +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import location -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import ( + CONF_REGION_DEVICE_TRACKER, + CONF_REGION_IDENTIFIER, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, +) +from .exceptions import EntityNotFoundError +from .util import get_position_data class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]): """Custom coordinator for the dwd_weather_warnings integration.""" - def __init__(self, hass: HomeAssistant, api: DwdWeatherWarningsAPI) -> None: + config_entry: ConfigEntry + api: DwdWeatherWarningsAPI + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the dwd_weather_warnings coordinator.""" super().__init__( hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL ) - self.api = api + self._device_tracker = None + self._previous_position = None + + async def async_config_entry_first_refresh(self) -> None: + """Perform first refresh.""" + if region_identifier := self.config_entry.data.get(CONF_REGION_IDENTIFIER): + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, region_identifier + ) + else: + self._device_tracker = self.config_entry.data.get( + CONF_REGION_DEVICE_TRACKER + ) + + await super().async_config_entry_first_refresh() async def _async_update_data(self) -> None: """Get the latest data from the DWD Weather Warnings API.""" - await self.hass.async_add_executor_job(self.api.update) + if self._device_tracker: + try: + position = get_position_data(self.hass, self._device_tracker) + except (EntityNotFoundError, AttributeError) as err: + raise UpdateFailed(f"Error fetching position: {repr(err)}") from err + + distance = None + if self._previous_position is not None: + distance = location.distance( + self._previous_position[0], + self._previous_position[1], + position[0], + position[1], + ) + + if distance is None or distance > 50: + # Only create a new object on the first update + # or when the distance to the previous position + # changes by more than 50 meters (to take GPS + # inaccuracy into account). + self.api = await self.hass.async_add_executor_job( + DwdWeatherWarningsAPI, position + ) + else: + # Otherwise update the API to check for new warnings. + await self.hass.async_add_executor_job(self.api.update) + + self._previous_position = position + else: + await self.hass.async_add_executor_job(self.api.update) diff --git a/homeassistant/components/dwd_weather_warnings/exceptions.py b/homeassistant/components/dwd_weather_warnings/exceptions.py new file mode 100644 index 00000000000..cd61cfa6bae --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/exceptions.py @@ -0,0 +1,7 @@ +"""Exceptions for the dwd_weather_warnings integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class EntityNotFoundError(HomeAssistantError): + """When a referenced entity was not found.""" diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index d3e3b4a3772..d62c0f4f192 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -11,6 +11,8 @@ Wetterwarnungen (Stufe 1) from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -93,29 +95,27 @@ class DwdWeatherWarningsSensor( entry_type=DeviceEntryType.SERVICE, ) - self.api = coordinator.api - @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" if self.entity_description.key == CURRENT_WARNING_SENSOR: - return self.api.current_warning_level + return self.coordinator.api.current_warning_level - return self.api.expected_warning_level + return self.coordinator.api.expected_warning_level @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" data = { - ATTR_REGION_NAME: self.api.warncell_name, - ATTR_REGION_ID: self.api.warncell_id, - ATTR_LAST_UPDATE: self.api.last_update, + ATTR_REGION_NAME: self.coordinator.api.warncell_name, + ATTR_REGION_ID: self.coordinator.api.warncell_id, + ATTR_LAST_UPDATE: self.coordinator.api.last_update, } if self.entity_description.key == CURRENT_WARNING_SENSOR: - searched_warnings = self.api.current_warnings + searched_warnings = self.coordinator.api.current_warnings else: - searched_warnings = self.api.expected_warnings + searched_warnings = self.coordinator.api.expected_warnings data[ATTR_WARNING_COUNT] = len(searched_warnings) @@ -142,4 +142,4 @@ class DwdWeatherWarningsSensor( @property def available(self) -> bool: """Could the device be accessed during the last update call.""" - return self.api.data_valid + return self.coordinator.api.data_valid diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index aa460dcc6d5..3f421d338a7 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -2,17 +2,22 @@ "config": { "step": { "user": { - "description": "To identify the desired region, the warncell ID / name is required.", + "description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'latitude' and 'longitude'.", "data": { - "region_identifier": "Warncell ID or name" + "region_identifier": "Warncell ID or name", + "region_device_tracker": "Device tracker entity" } } }, "error": { - "invalid_identifier": "The specified region identifier is invalid." + "no_identifier": "Either the region identifier or device tracker is required.", + "ambiguous_identifier": "The region identifier and device tracker can not be specified together.", + "invalid_identifier": "The specified region identifier / device tracker is invalid.", + "entity_not_found": "The specified device tracker entity was not found.", + "attribute_not_found": "The required `latitude` or `longitude` attribute was not found in the specified device tracker." }, "abort": { - "already_configured": "Warncell ID / name is already configured.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } }, diff --git a/homeassistant/components/dwd_weather_warnings/util.py b/homeassistant/components/dwd_weather_warnings/util.py new file mode 100644 index 00000000000..730ebf4b71e --- /dev/null +++ b/homeassistant/components/dwd_weather_warnings/util.py @@ -0,0 +1,39 @@ +"""Util functions for the dwd_weather_warnings integration.""" + +from __future__ import annotations + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .exceptions import EntityNotFoundError + + +def get_position_data( + hass: HomeAssistant, registry_id: str +) -> tuple[float, float] | None: + """Extract longitude and latitude from a device tracker.""" + registry = er.async_get(hass) + registry_entry = registry.async_get(registry_id) + if registry_entry is None: + raise EntityNotFoundError(f"Failed to find registry entry {registry_id}") + + entity = hass.states.get(registry_entry.entity_id) + if entity is None: + raise EntityNotFoundError(f"Failed to find entity {registry_entry.entity_id}") + + latitude = entity.attributes.get(ATTR_LATITUDE) + if not latitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LATITUDE}' in {registry_entry.entity_id}", + ATTR_LATITUDE, + ) + + longitude = entity.attributes.get(ATTR_LONGITUDE) + if not longitude: + raise AttributeError( + f"Failed to find attribute '{ATTR_LONGITUDE}' in {registry_entry.entity_id}", + ATTR_LONGITUDE, + ) + + return (latitude, longitude) diff --git a/tests/components/dwd_weather_warnings/test_config_flow.py b/tests/components/dwd_weather_warnings/test_config_flow.py index 3558ff5ed93..119c029767a 100644 --- a/tests/components/dwd_weather_warnings/test_config_flow.py +++ b/tests/components/dwd_weather_warnings/test_config_flow.py @@ -6,34 +6,31 @@ from unittest.mock import patch import pytest from homeassistant.components.dwd_weather_warnings.const import ( - ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, - CONF_REGION_NAME, - CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_CONFIG_ENTRY_REGION: Final = { CONF_REGION_IDENTIFIER: "807111000", } -DEMO_YAML_CONFIGURATION: Final = { - CONF_NAME: "Unit Test", - CONF_REGION_NAME: "807111000", - CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +DEMO_CONFIG_ENTRY_GPS: Final = { + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", } pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_create_entry(hass: HomeAssistant) -> None: - """Test that the full config flow works.""" +async def test_create_entry_region(hass: HomeAssistant) -> None: + """Test that the full config flow works for a region identifier.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -45,7 +42,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: return_value=False, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for invalid region identifier. @@ -58,7 +55,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) # Test for successfully created entry. @@ -70,12 +67,95 @@ async def test_create_entry(hass: HomeAssistant) -> None: } +async def test_create_entry_gps( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test that the full config flow works for a device tracker.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test for missing registry entry error. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing device tracker error. + registry_entry = entity_registry.async_get_or_create( + "device_tracker", DOMAIN, "uuid", suggested_object_id="test_gps" + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "entity_not_found"} + + # Test for missing attribute error. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "attribute_not_found"} + + # Test for invalid provided identifier. + hass.states.async_set( + DEMO_CONFIG_ENTRY_GPS[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_identifier"} + + # Test for successfully created entry. + with patch( + "homeassistant.components.dwd_weather_warnings.config_flow.DwdWeatherWarningsAPI", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_GPS + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test_gps" + assert result["data"] == { + CONF_REGION_DEVICE_TRACKER: registry_entry.id, + } + + async def test_config_flow_already_configured(hass: HomeAssistant) -> None: """Test aborting, if the warncell ID / name is already configured during the config.""" entry = MockConfigEntry( domain=DOMAIN, - data=DEMO_CONFIG_ENTRY.copy(), - unique_id=DEMO_CONFIG_ENTRY[CONF_REGION_IDENTIFIER], + data=DEMO_CONFIG_ENTRY_REGION.copy(), + unique_id=DEMO_CONFIG_ENTRY_REGION[CONF_REGION_IDENTIFIER], ) entry.add_to_hass(hass) @@ -92,9 +172,40 @@ async def test_config_flow_already_configured(hass: HomeAssistant) -> None: return_value=True, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_CONFIG_ENTRY + result["flow_id"], user_input=DEMO_CONFIG_ENTRY_REGION ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_config_flow_with_errors(hass: HomeAssistant) -> None: + """Test error scenarios during the configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + # Test error for empty input data. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_identifier"} + + # Test error for setting both options during configuration. + demo_input = DEMO_CONFIG_ENTRY_REGION.copy() + demo_input.update(DEMO_CONFIG_ENTRY_GPS.copy()) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=demo_input, + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "ambiguous_identifier"} diff --git a/tests/components/dwd_weather_warnings/test_init.py b/tests/components/dwd_weather_warnings/test_init.py index db7afaadec9..bfd03b2fdd4 100644 --- a/tests/components/dwd_weather_warnings/test_init.py +++ b/tests/components/dwd_weather_warnings/test_init.py @@ -4,26 +4,40 @@ from typing import Final from homeassistant.components.dwd_weather_warnings.const import ( ADVANCE_WARNING_SENSOR, + CONF_REGION_DEVICE_TRACKER, CONF_REGION_IDENTIFIER, CURRENT_WARNING_SENSOR, DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + STATE_HOME, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -DEMO_CONFIG_ENTRY: Final = { +DEMO_IDENTIFIER_CONFIG_ENTRY: Final = { CONF_NAME: "Unit Test", CONF_REGION_IDENTIFIER: "807111000", CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], } +DEMO_TRACKER_CONFIG_ENTRY: Final = { + CONF_NAME: "Unit Test", + CONF_REGION_DEVICE_TRACKER: "device_tracker.test_gps", + CONF_MONITORED_CONDITIONS: [CURRENT_WARNING_SENSOR, ADVANCE_WARNING_SENSOR], +} + async def test_load_unload_entry(hass: HomeAssistant) -> None: - """Test loading and unloading the integration.""" - entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) + """Test loading and unloading the integration with a region identifier based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_IDENTIFIER_CONFIG_ENTRY) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -36,3 +50,68 @@ async def test_load_unload_entry(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED assert entry.entry_id not in hass.data[DOMAIN] + + +async def test_load_invalid_registry_entry(hass: HomeAssistant) -> None: + """Test loading the integration with an invalid registry entry ID.""" + INVALID_DATA = DEMO_TRACKER_CONFIG_ENTRY.copy() + INVALID_DATA[CONF_REGION_DEVICE_TRACKER] = "invalid_registry_id" + entry = MockConfigEntry(domain=DOMAIN, data=INVALID_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_device_tracker(hass: HomeAssistant) -> None: + """Test loading the integration with a missing device tracker.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_missing_required_attribute(hass: HomeAssistant) -> None: + """Test loading the integration with a device tracker missing a required attribute.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_valid_device_tracker( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test loading the integration with a valid device tracker based entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_TRACKER_CONFIG_ENTRY) + entry.add_to_hass(hass) + entity_registry.async_get_or_create( + "device_tracker", + entry.domain, + "uuid", + suggested_object_id="test_gps", + config_entry=entry, + ) + + hass.states.async_set( + DEMO_TRACKER_CONFIG_ENTRY[CONF_REGION_DEVICE_TRACKER], + STATE_HOME, + {ATTR_LATITUDE: "50.180454", ATTR_LONGITUDE: "7.610263"}, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN]