Add optional location based region to dwd_weather_warnings (#96027)

* Add device tracker option

* Update const name to be more understandable

* Clean up sensor code

* Clean up init and coordinator

* Add tests and update util function and it's usage

* Switch to using the registry entry and add tests

* Clean up code

* Consolidate duplicate code and adjust tests

* Fix runtime error

* Fix blocking of the event loop

* Adjust API object handling

* Update homeassistant/components/dwd_weather_warnings/exceptions.py

* Optimize coordinator data update

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
andarotajo 2024-04-22 09:13:09 +02:00 committed by GitHub
parent e29b301dd1
commit 70d4b4d20d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 403 additions and 63 deletions

View File

@ -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

View File

@ -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")
),
}
),
)

View File

@ -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"

View File

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

View File

@ -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."""

View File

@ -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

View File

@ -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%]"
}
},

View File

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

View File

@ -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"}

View File

@ -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]