Add configflow to Proximity integration (#103894)

* add config flow

* fix tests

* adjust and fix tests

* fix tests

* config_zones as fixture

* add config flow tests

* use coordinator.async_config_entry_first_refresh

* use entry.entry_id for hass.data

* fix doc string

* remove unused unit_of_measurement string key

* don't store friendly_name, just use self.name

* abort on matching entiry

* break out legacy setup into seperate function

* make tracked entites required

* move _asnyc_setup_legacy to module level

* use zone name as config entry title

* add entity_used_in helper

* check entry source if imported

* create repair issue for removed tracked entities

* separate state change from registry change event handling

* migrate unique ids after tracked entity renamed

* use full words for the variable names

* use defaultdict

* add test

* remove unnecessary if not in check

* use unique_id of tracked entity

* use the entity registry entry id

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Michael 2024-01-31 12:47:23 +01:00 committed by GitHub
parent c587c69915
commit 30c5baf522
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 919 additions and 207 deletions

View file

@ -5,18 +5,20 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.automation import automations_with_entity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICES, CONF_DEVICES,
CONF_NAME, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE, CONF_ZONE,
Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.event import (
from homeassistant.helpers.event import async_track_state_change async_track_entity_registry_updated_event,
async_track_state_change,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -27,12 +29,14 @@ from .const import (
ATTR_NEAREST, ATTR_NEAREST,
CONF_IGNORED_ZONES, CONF_IGNORED_ZONES,
CONF_TOLERANCE, CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DEFAULT_PROXIMITY_ZONE, DEFAULT_PROXIMITY_ZONE,
DEFAULT_TOLERANCE, DEFAULT_TOLERANCE,
DOMAIN, DOMAIN,
UNITS, UNITS,
) )
from .coordinator import ProximityDataUpdateCoordinator from .coordinator import ProximityDataUpdateCoordinator
from .helpers import entity_used_in
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -49,45 +53,24 @@ ZONE_SCHEMA = vol.Schema(
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA)}, extra=vol.ALLOW_EXTRA vol.All(
cv.deprecated(DOMAIN),
{DOMAIN: cv.schema_with_slug_keys(ZONE_SCHEMA)},
),
extra=vol.ALLOW_EXTRA,
) )
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_setup_legacy(
"""Get the zones and offsets from configuration.yaml.""" hass: HomeAssistant, entry: ConfigEntry, coordinator: ProximityDataUpdateCoordinator
hass.data.setdefault(DOMAIN, {}) ) -> None:
for friendly_name, proximity_config in config[DOMAIN].items(): """Legacy proximity entity handling, can be removed in 2024.8."""
_LOGGER.debug("setup %s with config:%s", friendly_name, proximity_config) friendly_name = entry.data[CONF_NAME]
coordinator = ProximityDataUpdateCoordinator(
hass, friendly_name, proximity_config
)
async_track_state_change(
hass,
proximity_config[CONF_DEVICES],
coordinator.async_check_proximity_state_change,
)
await coordinator.async_refresh()
hass.data[DOMAIN][friendly_name] = coordinator
proximity = Proximity(hass, friendly_name, coordinator) proximity = Proximity(hass, friendly_name, coordinator)
await proximity.async_added_to_hass() await proximity.async_added_to_hass()
proximity.async_write_ha_state() proximity.async_write_ha_state()
await async_load_platform( if used_in := entity_used_in(hass, f"{DOMAIN}.{friendly_name}"):
hass,
"sensor",
DOMAIN,
{CONF_NAME: friendly_name, **proximity_config},
config,
)
# deprecate proximity entity - can be removed in 2024.8
used_in = automations_with_entity(hass, f"{DOMAIN}.{friendly_name}")
used_in += scripts_with_entity(hass, f"{DOMAIN}.{friendly_name}")
if used_in:
async_create_issue( async_create_issue(
hass, hass,
DOMAIN, DOMAIN,
@ -103,9 +86,101 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}, },
) )
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Get the zones and offsets from configuration.yaml."""
if DOMAIN in config:
for friendly_name, proximity_config in config[DOMAIN].items():
_LOGGER.debug("import %s with config:%s", friendly_name, proximity_config)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_NAME: friendly_name,
CONF_ZONE: f"zone.{proximity_config[CONF_ZONE]}",
CONF_TRACKED_ENTITIES: proximity_config[CONF_DEVICES],
CONF_IGNORED_ZONES: [
f"zone.{zone}"
for zone in proximity_config[CONF_IGNORED_ZONES]
],
CONF_TOLERANCE: proximity_config[CONF_TOLERANCE],
CONF_UNIT_OF_MEASUREMENT: proximity_config.get(
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
),
},
)
)
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.8.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Proximity",
},
)
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Proximity from a config entry."""
_LOGGER.debug("setup %s with config:%s", entry.title, entry.data)
hass.data.setdefault(DOMAIN, {})
coordinator = ProximityDataUpdateCoordinator(hass, entry.title, dict(entry.data))
entry.async_on_unload(
async_track_state_change(
hass,
entry.data[CONF_TRACKED_ENTITIES],
coordinator.async_check_proximity_state_change,
)
)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass,
entry.data[CONF_TRACKED_ENTITIES],
coordinator.async_check_tracked_entity_change,
)
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
if entry.source == SOURCE_IMPORT:
await _async_setup_legacy(hass, entry, coordinator)
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, [Platform.SENSOR]
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]): class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]):
"""Representation of a Proximity.""" """Representation of a Proximity."""

View file

@ -0,0 +1,133 @@
"""Config flow for proximity."""
from __future__ import annotations
from typing import Any, cast
import voluptuous as vol
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_ZONE
from homeassistant.core import State, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
EntitySelector,
EntitySelectorConfig,
NumberSelector,
NumberSelectorConfig,
)
from .const import (
CONF_IGNORED_ZONES,
CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DEFAULT_PROXIMITY_ZONE,
DEFAULT_TOLERANCE,
DOMAIN,
)
RESULT_SUCCESS = "success"
def _base_schema(user_input: dict[str, Any]) -> vol.Schema:
return {
vol.Required(
CONF_TRACKED_ENTITIES, default=user_input.get(CONF_TRACKED_ENTITIES, [])
): EntitySelector(
EntitySelectorConfig(
domain=[DEVICE_TRACKER_DOMAIN, PERSON_DOMAIN], multiple=True
),
),
vol.Optional(
CONF_IGNORED_ZONES, default=user_input.get(CONF_IGNORED_ZONES, [])
): EntitySelector(
EntitySelectorConfig(domain=ZONE_DOMAIN, multiple=True),
),
vol.Required(
CONF_TOLERANCE,
default=user_input.get(CONF_TOLERANCE, DEFAULT_TOLERANCE),
): NumberSelector(
NumberSelectorConfig(min=1, max=100, step=1),
),
}
class ProximityConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a proximity config flow."""
VERSION = 1
def _user_form_schema(self, user_input: dict[str, Any] | None = None) -> vol.Schema:
if user_input is None:
user_input = {}
return vol.Schema(
{
vol.Required(
CONF_ZONE,
default=user_input.get(
CONF_ZONE, f"{ZONE_DOMAIN}.{DEFAULT_PROXIMITY_ZONE}"
),
): EntitySelector(
EntitySelectorConfig(domain=ZONE_DOMAIN),
),
**_base_schema(user_input),
}
)
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler."""
return ProximityOptionsFlow(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if user_input is not None:
self._async_abort_entries_match(user_input)
zone = self.hass.states.get(user_input[CONF_ZONE])
return self.async_create_entry(
title=cast(State, zone).name, data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=self._user_form_schema(user_input),
)
async def async_step_import(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Import a yaml config entry."""
return await self.async_step_user(user_input)
class ProximityOptionsFlow(OptionsFlow):
"""Handle a option flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
def _user_form_schema(self, user_input: dict[str, Any]) -> vol.Schema:
return vol.Schema(_base_schema(user_input))
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle options flow."""
if user_input is not None:
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, **user_input}
)
return self.async_create_entry(title=self.config_entry.title, data={})
return self.async_show_form(
step_id="init",
data_schema=self._user_form_schema(dict(self.config_entry.data)),
)

View file

@ -13,6 +13,7 @@ ATTR_PROXIMITY_DATA: Final = "proximity_data"
CONF_IGNORED_ZONES = "ignored_zones" CONF_IGNORED_ZONES = "ignored_zones"
CONF_TOLERANCE = "tolerance" CONF_TOLERANCE = "tolerance"
CONF_TRACKED_ENTITIES = "tracked_entities"
DEFAULT_DIR_OF_TRAVEL = "not set" DEFAULT_DIR_OF_TRAVEL = "not set"
DEFAULT_DIST_TO_ZONE = "not set" DEFAULT_DIST_TO_ZONE = "not set"

View file

@ -1,19 +1,23 @@
"""Data update coordinator for the Proximity integration.""" """Data update coordinator for the Proximity integration."""
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
ATTR_NAME, ATTR_NAME,
CONF_DEVICES,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE, CONF_ZONE,
UnitOfLength, UnitOfLength,
) )
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, EventType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.location import distance from homeassistant.util.location import distance
from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_conversion import DistanceConverter
@ -25,9 +29,11 @@ from .const import (
ATTR_NEAREST, ATTR_NEAREST,
CONF_IGNORED_ZONES, CONF_IGNORED_ZONES,
CONF_TOLERANCE, CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DEFAULT_DIR_OF_TRAVEL, DEFAULT_DIR_OF_TRAVEL,
DEFAULT_DIST_TO_ZONE, DEFAULT_DIST_TO_ZONE,
DEFAULT_NEAREST, DEFAULT_NEAREST,
DOMAIN,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,18 +69,21 @@ DEFAULT_DATA = ProximityData(
class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
"""Proximity data update coordinator.""" """Proximity data update coordinator."""
config_entry: ConfigEntry
def __init__( def __init__(
self, hass: HomeAssistant, friendly_name: str, config: ConfigType self, hass: HomeAssistant, friendly_name: str, config: ConfigType
) -> None: ) -> None:
"""Initialize the Proximity coordinator.""" """Initialize the Proximity coordinator."""
self.ignored_zones: list[str] = config[CONF_IGNORED_ZONES] self.ignored_zone_ids: list[str] = config[CONF_IGNORED_ZONES]
self.tracked_entities: list[str] = config[CONF_DEVICES] self.tracked_entities: list[str] = config[CONF_TRACKED_ENTITIES]
self.tolerance: int = config[CONF_TOLERANCE] self.tolerance: int = config[CONF_TOLERANCE]
self.proximity_zone: str = config[CONF_ZONE] self.proximity_zone_id: str = config[CONF_ZONE]
self.proximity_zone_name: str = self.proximity_zone_id.split(".")[-1]
self.unit_of_measurement: str = config.get( self.unit_of_measurement: str = config.get(
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
) )
self.friendly_name = friendly_name self.entity_mapping: dict[str, list[str]] = defaultdict(list)
super().__init__( super().__init__(
hass, hass,
@ -87,6 +96,11 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
self.state_change_data: StateChangedData | None = None self.state_change_data: StateChangedData | None = None
@callback
def async_add_entity_mapping(self, tracked_entity_id: str, entity_id: str) -> None:
"""Add an tracked entity to proximity entity mapping."""
self.entity_mapping[tracked_entity_id].append(entity_id)
async def async_check_proximity_state_change( async def async_check_proximity_state_change(
self, entity: str, old_state: State | None, new_state: State | None self, entity: str, old_state: State | None, new_state: State | None
) -> None: ) -> None:
@ -94,6 +108,31 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
self.state_change_data = StateChangedData(entity, old_state, new_state) self.state_change_data = StateChangedData(entity, old_state, new_state)
await self.async_refresh() await self.async_refresh()
async def async_check_tracked_entity_change(
self, event: EventType[er.EventEntityRegistryUpdatedData]
) -> None:
"""Fetch and process tracked entity change event."""
data = event.data
if data["action"] == "remove":
self._create_removed_tracked_entity_issue(data["entity_id"])
if data["action"] == "update" and "entity_id" in data["changes"]:
old_tracked_entity_id = data["old_entity_id"]
new_tracked_entity_id = data["entity_id"]
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONF_TRACKED_ENTITIES: [
tracked_entity
for tracked_entity in self.tracked_entities
+ [new_tracked_entity_id]
if tracked_entity != old_tracked_entity_id
],
},
)
def _convert(self, value: float | str) -> float | str: def _convert(self, value: float | str) -> float | str:
"""Round and convert given distance value.""" """Round and convert given distance value."""
if isinstance(value, str): if isinstance(value, str):
@ -113,10 +152,10 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
latitude: float | None, latitude: float | None,
longitude: float | None, longitude: float | None,
) -> int | None: ) -> int | None:
if device.state.lower() == self.proximity_zone.lower(): if device.state.lower() == self.proximity_zone_name.lower():
_LOGGER.debug( _LOGGER.debug(
"%s: %s in zone -> distance=0", "%s: %s in zone -> distance=0",
self.friendly_name, self.name,
device.entity_id, device.entity_id,
) )
return 0 return 0
@ -124,7 +163,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if latitude is None or longitude is None: if latitude is None or longitude is None:
_LOGGER.debug( _LOGGER.debug(
"%s: %s has no coordinates -> distance=None", "%s: %s has no coordinates -> distance=None",
self.friendly_name, self.name,
device.entity_id, device.entity_id,
) )
return None return None
@ -149,10 +188,10 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
new_latitude: float | None, new_latitude: float | None,
new_longitude: float | None, new_longitude: float | None,
) -> str | None: ) -> str | None:
if device.state.lower() == self.proximity_zone.lower(): if device.state.lower() == self.proximity_zone_name.lower():
_LOGGER.debug( _LOGGER.debug(
"%s: %s in zone -> direction_of_travel=arrived", "%s: %s in zone -> direction_of_travel=arrived",
self.friendly_name, self.name,
device.entity_id, device.entity_id,
) )
return "arrived" return "arrived"
@ -193,11 +232,11 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
async def _async_update_data(self) -> ProximityData: async def _async_update_data(self) -> ProximityData:
"""Calculate Proximity data.""" """Calculate Proximity data."""
if (zone_state := self.hass.states.get(f"zone.{self.proximity_zone}")) is None: if (zone_state := self.hass.states.get(self.proximity_zone_id)) is None:
_LOGGER.debug( _LOGGER.debug(
"%s: zone %s does not exist -> reset", "%s: zone %s does not exist -> reset",
self.friendly_name, self.name,
self.proximity_zone, self.proximity_zone_id,
) )
return DEFAULT_DATA return DEFAULT_DATA
@ -208,12 +247,12 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if (tracked_entity_state := self.hass.states.get(entity_id)) is None: if (tracked_entity_state := self.hass.states.get(entity_id)) is None:
if entities_data.pop(entity_id, None) is not None: if entities_data.pop(entity_id, None) is not None:
_LOGGER.debug( _LOGGER.debug(
"%s: %s does not exist -> remove", self.friendly_name, entity_id "%s: %s does not exist -> remove", self.name, entity_id
) )
continue continue
if entity_id not in entities_data: if entity_id not in entities_data:
_LOGGER.debug("%s: %s is new -> add", self.friendly_name, entity_id) _LOGGER.debug("%s: %s is new -> add", self.name, entity_id)
entities_data[entity_id] = { entities_data[entity_id] = {
ATTR_DIST_TO: None, ATTR_DIST_TO: None,
ATTR_DIR_OF_TRAVEL: None, ATTR_DIR_OF_TRAVEL: None,
@ -221,7 +260,8 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
ATTR_IN_IGNORED_ZONE: False, ATTR_IN_IGNORED_ZONE: False,
} }
entities_data[entity_id][ATTR_IN_IGNORED_ZONE] = ( entities_data[entity_id][ATTR_IN_IGNORED_ZONE] = (
tracked_entity_state.state.lower() in self.ignored_zones f"{ZONE_DOMAIN}.{tracked_entity_state.state.lower()}"
in self.ignored_zone_ids
) )
entities_data[entity_id][ATTR_DIST_TO] = self._calc_distance_to_zone( entities_data[entity_id][ATTR_DIST_TO] = self._calc_distance_to_zone(
zone_state, zone_state,
@ -232,7 +272,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if entities_data[entity_id][ATTR_DIST_TO] is None: if entities_data[entity_id][ATTR_DIST_TO] is None:
_LOGGER.debug( _LOGGER.debug(
"%s: %s has unknown distance got -> direction_of_travel=None", "%s: %s has unknown distance got -> direction_of_travel=None",
self.friendly_name, self.name,
entity_id, entity_id,
) )
entities_data[entity_id][ATTR_DIR_OF_TRAVEL] = None entities_data[entity_id][ATTR_DIR_OF_TRAVEL] = None
@ -243,7 +283,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
) is not None: ) is not None:
_LOGGER.debug( _LOGGER.debug(
"%s: calculate direction of travel for %s", "%s: calculate direction of travel for %s",
self.friendly_name, self.name,
state_change_data.entity_id, state_change_data.entity_id,
) )
@ -304,3 +344,16 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
proximity_data[ATTR_DIST_TO] = self._convert(proximity_data[ATTR_DIST_TO]) proximity_data[ATTR_DIST_TO] = self._convert(proximity_data[ATTR_DIST_TO])
return ProximityData(proximity_data, entities_data) return ProximityData(proximity_data, entities_data)
def _create_removed_tracked_entity_issue(self, entity_id: str) -> None:
"""Create a repair issue for a removed tracked entity."""
async_create_issue(
self.hass,
DOMAIN,
f"tracked_entity_removed_{entity_id}",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="tracked_entity_removed",
translation_placeholders={"entity_id": entity_id, "name": self.name},
)

View file

@ -0,0 +1,11 @@
"""Helper functions for proximity."""
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.core import HomeAssistant
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get list of related automations and scripts."""
used_in = automations_with_entity(hass, entity_id)
used_in += scripts_with_entity(hass, entity_id)
return used_in

View file

@ -2,6 +2,7 @@
"domain": "proximity", "domain": "proximity",
"name": "Proximity", "name": "Proximity",
"codeowners": ["@mib1185"], "codeowners": ["@mib1185"],
"config_flow": true,
"dependencies": ["device_tracker", "zone"], "dependencies": ["device_tracker", "zone"],
"documentation": "https://www.home-assistant.io/integrations/proximity", "documentation": "https://www.home-assistant.io/integrations/proximity",
"iot_class": "calculated", "iot_class": "calculated",

View file

@ -7,10 +7,12 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.const import CONF_NAME, UnitOfLength from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfLength
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_DIR_OF_TRAVEL, ATTR_DIST_TO, ATTR_NEAREST, DOMAIN from .const import ATTR_DIR_OF_TRAVEL, ATTR_DIST_TO, ATTR_NEAREST, DOMAIN
@ -48,29 +50,51 @@ SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [
] ]
async def async_setup_platform( def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo:
hass: HomeAssistant, return DeviceInfo(
config: ConfigType, identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
async_add_entities: AddEntitiesCallback, name=coordinator.config_entry.title,
discovery_info: DiscoveryInfoType | None = None, entry_type=DeviceEntryType.SERVICE,
) -> None: )
"""Set up the Proximity sensor platform."""
if discovery_info is None:
return
coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][
discovery_info[CONF_NAME] async def async_setup_entry(
] hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the proximity sensors."""
coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[ProximitySensor | ProximityTrackedEntitySensor] = [ entities: list[ProximitySensor | ProximityTrackedEntitySensor] = [
ProximitySensor(description, coordinator) ProximitySensor(description, coordinator)
for description in SENSORS_PER_PROXIMITY for description in SENSORS_PER_PROXIMITY
] ]
tracked_entity_descriptors = []
entity_reg = er.async_get(hass)
for tracked_entity_id in coordinator.tracked_entities:
if (entity_entry := entity_reg.async_get(tracked_entity_id)) is not None:
tracked_entity_descriptors.append(
{
"entity_id": tracked_entity_id,
"identifier": entity_entry.id,
}
)
else:
tracked_entity_descriptors.append(
{
"entity_id": tracked_entity_id,
"identifier": tracked_entity_id,
}
)
entities += [ entities += [
ProximityTrackedEntitySensor(description, coordinator, tracked_entity_id) ProximityTrackedEntitySensor(
description, coordinator, tracked_entity_descriptor
)
for description in SENSORS_PER_ENTITY for description in SENSORS_PER_ENTITY
for tracked_entity_id in coordinator.tracked_entities for tracked_entity_descriptor in tracked_entity_descriptors
] ]
async_add_entities(entities) async_add_entities(entities)
@ -91,9 +115,8 @@ class ProximitySensor(CoordinatorEntity[ProximityDataUpdateCoordinator], SensorE
self.entity_description = description self.entity_description = description
# entity name will be removed as soon as we have a config entry self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
# and can follow the entity naming guidelines self._attr_device_info = _device_info(coordinator)
self._attr_name = f"{coordinator.friendly_name} {description.name}"
@property @property
def native_value(self) -> str | float | None: def native_value(self) -> str | float | None:
@ -116,23 +139,38 @@ class ProximityTrackedEntitySensor(
self, self,
description: SensorEntityDescription, description: SensorEntityDescription,
coordinator: ProximityDataUpdateCoordinator, coordinator: ProximityDataUpdateCoordinator,
tracked_entity_id: str, tracked_entity_descriptor: dict[str, str],
) -> None: ) -> None:
"""Initialize the proximity.""" """Initialize the proximity."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self.tracked_entity_id = tracked_entity_id self.tracked_entity_id = tracked_entity_descriptor["entity_id"]
# entity name will be removed as soon as we have a config entry self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor['identifier']}_{description.key}"
# and can follow the entity naming guidelines self._attr_name = f"{self.tracked_entity_id.split('.')[-1]} {description.name}"
self._attr_name = ( self._attr_device_info = _device_info(coordinator)
f"{coordinator.friendly_name} {tracked_entity_id} {description.name}"
async def async_added_to_hass(self) -> None:
"""Register entity mapping."""
await super().async_added_to_hass()
self.coordinator.async_add_entity_mapping(
self.tracked_entity_id, self.entity_id
) )
@property
def data(self) -> dict[str, str | int | None] | None:
"""Get data from coordinator."""
return self.coordinator.data.entities.get(self.tracked_entity_id)
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.data is not None
@property @property
def native_value(self) -> str | float | None: def native_value(self) -> str | float | None:
"""Return native sensor value.""" """Return native sensor value."""
if (data := self.coordinator.data.entities.get(self.tracked_entity_id)) is None: if self.data is None:
return None return None
return data.get(self.entity_description.key) return self.data.get(self.entity_description.key)

View file

@ -1,5 +1,34 @@
{ {
"title": "Proximity", "title": "Proximity",
"config": {
"flow_title": "Proximity",
"step": {
"user": {
"data": {
"zone": "Zone to track distance to",
"ignored_zones": "Zones to ignore",
"tracked_entities": "Devices or Persons to track",
"tolerance": "Tolerance distance"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"success": "Changes saved"
}
},
"options": {
"step": {
"init": {
"data": {
"zone": "Zone to track distance to",
"ignored_zones": "Zones to ignore",
"tracked_entities": "Devices or Persons to track",
"tolerance": "Tolerance distance"
}
}
}
},
"entity": { "entity": {
"sensor": { "sensor": {
"dir_of_travel": { "dir_of_travel": {
@ -25,6 +54,17 @@
} }
} }
} }
},
"tracked_entity_removed": {
"title": "Tracked entity has been removed",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::proximity::issues::tracked_entity_removed::title%]",
"description": "The entity `{entity_id}` has been removed from HA, but is used in proximity {name}. Please remove `{entity_id}` from the list of tracked entities. Related proximity sensor entites were set to unavailable and can be removed."
}
}
}
} }
} }
} }

View file

@ -393,6 +393,7 @@ FLOWS = {
"profiler", "profiler",
"progettihwsw", "progettihwsw",
"prosegur", "prosegur",
"proximity",
"prusalink", "prusalink",
"ps4", "ps4",
"pure_energie", "pure_energie",

View file

@ -4562,7 +4562,7 @@
}, },
"proximity": { "proximity": {
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "calculated" "iot_class": "calculated"
}, },
"proxmoxve": { "proxmoxve": {

View file

@ -0,0 +1,20 @@
"""Config test for proximity."""
import pytest
from homeassistant.core import HomeAssistant
@pytest.fixture(autouse=True)
def config_zones(hass: HomeAssistant):
"""Set up zones for test."""
hass.config.components.add("zone")
hass.states.async_set(
"zone.home",
"zoning",
{"name": "Home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
)
hass.states.async_set(
"zone.work",
"zoning",
{"name": "Work", "latitude": 2.3, "longitude": 1.3, "radius": 10},
)

View file

@ -0,0 +1,187 @@
"""Test proximity config flow."""
from unittest.mock import patch
import pytest
from homeassistant.components.proximity.const import (
CONF_IGNORED_ZONES,
CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("user_input", "expected_result"),
[
(
{
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
},
{
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
),
(
{
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
},
{
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
},
),
],
)
async def test_user_flow(
hass: HomeAssistant, user_input: dict, expected_result: dict
) -> None:
"""Test starting a flow by user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.proximity.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=user_input,
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == expected_result
zone = hass.states.get(user_input[CONF_ZONE])
assert result["title"] == zone.name
await hass.async_block_till_done()
assert mock_setup_entry.called
async def test_options_flow(hass: HomeAssistant) -> None:
"""Test options flow."""
mock_config = MockConfigEntry(
domain=DOMAIN,
title="home",
data={
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
},
unique_id=f"{DOMAIN}_home",
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.proximity.async_setup_entry", return_value=True
) as mock_setup_entry:
await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
assert mock_setup_entry.called
result = await hass.config_entries.options.async_init(mock_config.entry_id)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_TRACKED_ENTITIES: ["device_tracker.test2"],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert mock_config.data == {
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test2"],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
}
async def test_import_flow(hass: HomeAssistant) -> None:
"""Test import of yaml configuration."""
with patch(
"homeassistant.components.proximity.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_NAME: "home",
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
CONF_UNIT_OF_MEASUREMENT: "km",
},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_NAME: "home",
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
CONF_UNIT_OF_MEASUREMENT: "km",
}
zone = hass.states.get("zone.home")
assert result["title"] == zone.name
await hass.async_block_till_done()
assert mock_setup_entry.called
async def test_abort_duplicated_entry(hass: HomeAssistant) -> None:
"""Test if we abort on duplicate user input data."""
DATA = {
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: ["device_tracker.test1"],
CONF_IGNORED_ZONES: ["zone.work"],
CONF_TOLERANCE: 10,
}
mock_config = MockConfigEntry(
domain=DOMAIN,
title="home",
data=DATA,
unique_id=f"{DOMAIN}_home",
)
mock_config.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
with patch(
"homeassistant.components.proximity.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=DATA,
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
await hass.async_block_till_done()

View file

@ -4,39 +4,51 @@ import pytest
from homeassistant.components import automation, script from homeassistant.components import automation, script
from homeassistant.components.automation import automations_with_entity from homeassistant.components.automation import automations_with_entity
from homeassistant.components.proximity import DOMAIN from homeassistant.components.proximity.const import (
CONF_IGNORED_ZONES,
CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DOMAIN,
)
from homeassistant.components.script import scripts_with_entity from homeassistant.components.script import scripts_with_entity
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir import homeassistant.helpers.issue_registry as ir
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import slugify from homeassistant.util import slugify
from tests.common import MockConfigEntry
@pytest.mark.parametrize(("friendly_name"), ["home", "home_test2", "work"])
async def test_proximities(hass: HomeAssistant, friendly_name: str) -> None: @pytest.mark.parametrize(
"""Test a list of proximities.""" ("friendly_name", "config"),
config = { [
"proximity": { (
"home": { "home",
{
"ignored_zones": ["work"], "ignored_zones": ["work"],
"devices": ["device_tracker.test1", "device_tracker.test2"], "devices": ["device_tracker.test1", "device_tracker.test2"],
"tolerance": "1", "tolerance": "1",
}, },
"home_test2": { ),
"ignored_zones": ["work"], (
"devices": ["device_tracker.test1", "device_tracker.test2"], "work",
"tolerance": "1", {
},
"work": {
"devices": ["device_tracker.test1"], "devices": ["device_tracker.test1"],
"tolerance": "1", "tolerance": "1",
"zone": "work", "zone": "work",
}, },
} ),
} ],
)
assert await async_setup_component(hass, DOMAIN, config) async def test_proximities(
hass: HomeAssistant, friendly_name: str, config: dict
) -> None:
"""Test a list of proximities."""
assert await async_setup_component(
hass, DOMAIN, {"proximity": {friendly_name: config}}
)
await hass.async_block_till_done() await hass.async_block_till_done()
# proximity entity # proximity entity
@ -50,31 +62,47 @@ async def test_proximities(hass: HomeAssistant, friendly_name: str) -> None:
assert state.state == "0" assert state.state == "0"
# sensor entities # sensor entities
state = hass.states.get(f"sensor.{friendly_name}_nearest") state = hass.states.get(f"sensor.{friendly_name}_nearest_device")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
for device in config["proximity"][friendly_name]["devices"]: for device in config["devices"]:
entity_base_name = f"sensor.{friendly_name}_{slugify(device)}" entity_base_name = f"sensor.{friendly_name}_{slugify(device.split('.')[-1])}"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNAVAILABLE
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNAVAILABLE
async def test_proximities_setup(hass: HomeAssistant) -> None: async def test_legacy_setup(hass: HomeAssistant) -> None:
"""Test a list of proximities with missing devices.""" """Test legacy setup only on imported entries."""
config = { config = {
"proximity": { "proximity": {
"home": { "home": {
"ignored_zones": ["work"], "devices": ["device_tracker.test1"],
"devices": ["device_tracker.test1", "device_tracker.test2"],
"tolerance": "1", "tolerance": "1",
}, },
"work": {"tolerance": "1", "zone": "work"},
} }
} }
assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get("proximity.home")
mock_config = MockConfigEntry(
domain=DOMAIN,
title="work",
data={
CONF_ZONE: "zone.work",
CONF_TRACKED_ENTITIES: ["device_tracker.test2"],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
unique_id=f"{DOMAIN}_work",
)
mock_config.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
assert not hass.states.get("proximity.work")
async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None: async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None:
@ -105,10 +133,10 @@ async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "arrived" assert state.attributes.get("dir_of_travel") == "arrived"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "0" assert state.state == "0"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -143,20 +171,21 @@ async def test_device_tracker_test1_away(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "11912010" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
async def test_device_tracker_test1_awayfurther(hass: HomeAssistant) -> None: async def test_device_tracker_test1_awayfurther(
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker state away further.""" """Test for tracker state away further."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
config = { config = {
@ -184,10 +213,10 @@ async def test_device_tracker_test1_awayfurther(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -206,19 +235,20 @@ async def test_device_tracker_test1_awayfurther(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "away_from" assert state.attributes.get("dir_of_travel") == "away_from"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "4625264" assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == "away_from" assert state.state == "away_from"
async def test_device_tracker_test1_awaycloser(hass: HomeAssistant) -> None: async def test_device_tracker_test1_awaycloser(
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker state away closer.""" """Test for tracker state away closer."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
config = { config = {
@ -246,10 +276,10 @@ async def test_device_tracker_test1_awaycloser(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "4625264" assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -268,10 +298,10 @@ async def test_device_tracker_test1_awaycloser(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "towards" assert state.attributes.get("dir_of_travel") == "towards"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -302,10 +332,10 @@ async def test_all_device_trackers_in_ignored_zone(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "not set" assert state.attributes.get("dir_of_travel") == "not set"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -337,10 +367,10 @@ async def test_device_tracker_test1_no_coordinates(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "not set" assert state.attributes.get("dir_of_travel") == "not set"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -377,12 +407,12 @@ async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> No
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "11912010" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
@ -399,12 +429,12 @@ async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> No
assert state.attributes.get("dir_of_travel") == "stationary" assert state.attributes.get("dir_of_travel") == "stationary"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "11912010" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == "stationary" assert state.state == "stationary"
@ -445,11 +475,11 @@ async def test_device_trackers_in_zone(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "arrived" assert state.attributes.get("dir_of_travel") == "arrived"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1, test2" assert state.state == "test1, test2"
for device in ["device_tracker.test1", "device_tracker.test2"]: for device in ["test1", "test2"]:
entity_base_name = f"sensor.home_{slugify(device)}" entity_base_name = f"sensor.home_{device}"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "0" assert state.state == "0"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -457,10 +487,9 @@ async def test_device_trackers_in_zone(hass: HomeAssistant) -> None:
async def test_device_tracker_test1_awayfurther_than_test2_first_test1( async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
hass: HomeAssistant, hass: HomeAssistant, config_zones
) -> None: ) -> None:
"""Test for tracker ordering.""" """Test for tracker ordering."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
hass.states.async_set( hass.states.async_set(
@ -500,16 +529,16 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -528,16 +557,16 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "4625264" assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -545,10 +574,9 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
async def test_device_tracker_test1_awayfurther_than_test2_first_test2( async def test_device_tracker_test1_awayfurther_than_test2_first_test2(
hass: HomeAssistant, hass: HomeAssistant, config_zones
) -> None: ) -> None:
"""Test for tracker ordering.""" """Test for tracker ordering."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
hass.states.async_set( hass.states.async_set(
@ -586,16 +614,16 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test2" assert state.state == "test2"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "4625264" assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -614,16 +642,16 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "4625264" assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -667,16 +695,16 @@ async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "11912010" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -684,10 +712,9 @@ async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(
async def test_device_tracker_test1_awayfurther_test2_first( async def test_device_tracker_test1_awayfurther_test2_first(
hass: HomeAssistant, hass: HomeAssistant, config_zones
) -> None: ) -> None:
"""Test for tracker state.""" """Test for tracker state."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
hass.states.async_set( hass.states.async_set(
@ -750,16 +777,16 @@ async def test_device_tracker_test1_awayfurther_test2_first(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test2" assert state.state == "test2"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -767,10 +794,9 @@ async def test_device_tracker_test1_awayfurther_test2_first(
async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone( async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
hass: HomeAssistant, hass: HomeAssistant, config_zones
) -> None: ) -> None:
"""Test for tracker states.""" """Test for tracker states."""
config_zones(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
hass.states.async_set( hass.states.async_set(
@ -809,16 +835,16 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -837,16 +863,16 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test2" assert state.state == "test2"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "989156" assert state.state == "989156"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
@ -865,23 +891,23 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
assert state.attributes.get("dir_of_travel") == "unknown" assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities # sensor entities
state = hass.states.get("sensor.home_nearest") state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1" assert state.state == "test1"
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}" entity_base_name = "sensor.home_test1"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "2218752" assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
entity_base_name = f"sensor.home_{slugify('device_tracker.test2')}" entity_base_name = "sensor.home_test2"
state = hass.states.get(f"{entity_base_name}_distance") state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "1364567" assert state.state == "1364567"
state = hass.states.get(f"{entity_base_name}_direction_of_travel") state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == "away_from" assert state.state == "away_from"
async def test_create_issue( async def test_create_deprecated_proximity_issue(
hass: HomeAssistant, hass: HomeAssistant,
issue_registry: ir.IssueRegistry, issue_registry: ir.IssueRegistry,
) -> None: ) -> None:
@ -946,16 +972,142 @@ async def test_create_issue(
) )
def config_zones(hass): async def test_create_removed_tracked_entity_issue(
"""Set up zones for test.""" hass: HomeAssistant,
hass.config.components.add("zone") issue_registry: ir.IssueRegistry,
hass.states.async_set( entity_registry: er.EntityRegistry,
"zone.home", ) -> None:
"zoning", """Test we create an issue for removed tracked entities."""
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10}, t1 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test1"
) )
hass.states.async_set( t2 = entity_registry.async_get_or_create(
"zone.work", "device_tracker", "device_tracker", "test2"
"zoning", )
{"name": "work", "latitude": 2.3, "longitude": 1.3, "radius": 10},
hass.states.async_set(t1.entity_id, "not_home")
hass.states.async_set(t2.entity_id, "not_home")
mock_config = MockConfigEntry(
domain=DOMAIN,
title="home",
data={
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: [t1.entity_id, t2.entity_id],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
unique_id=f"{DOMAIN}_home",
)
mock_config.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance"
sensor_t2 = f"sensor.home_{t2.entity_id.split('.')[-1]}_distance"
state = hass.states.get(sensor_t1)
assert state.state == STATE_UNKNOWN
state = hass.states.get(sensor_t2)
assert state.state == STATE_UNKNOWN
hass.states.async_remove(t2.entity_id)
entity_registry.async_remove(t2.entity_id)
await hass.async_block_till_done()
state = hass.states.get(sensor_t1)
assert state.state == STATE_UNKNOWN
state = hass.states.get(sensor_t2)
assert state.state == STATE_UNAVAILABLE
assert issue_registry.async_get_issue(
DOMAIN, f"tracked_entity_removed_{t2.entity_id}"
)
async def test_track_renamed_tracked_entity(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that when tracked entity is renamed."""
t1 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test1"
)
hass.states.async_set(t1.entity_id, "not_home")
mock_config = MockConfigEntry(
domain=DOMAIN,
title="home",
data={
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: [t1.entity_id],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
unique_id=f"{DOMAIN}_home",
)
mock_config.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance"
entity = entity_registry.async_get(sensor_t1)
assert entity
assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone"
entity_registry.async_update_entity(
t1.entity_id, new_entity_id=f"{t1.entity_id}_renamed"
)
await hass.async_block_till_done()
entity = entity_registry.async_get(sensor_t1)
assert entity
assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone"
entry = hass.config_entries.async_get_entry(mock_config.entry_id)
assert entry
assert entry.data[CONF_TRACKED_ENTITIES] == [f"{t1.entity_id}_renamed"]
async def test_sensor_unique_ids(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that when tracked entity is renamed."""
t1 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test1"
)
hass.states.async_set(t1.entity_id, "not_home")
hass.states.async_set("device_tracker.test2", "not_home")
mock_config = MockConfigEntry(
domain=DOMAIN,
title="home",
data={
CONF_ZONE: "zone.home",
CONF_TRACKED_ENTITIES: [t1.entity_id, "device_tracker.test2"],
CONF_IGNORED_ZONES: [],
CONF_TOLERANCE: 1,
},
unique_id=f"{DOMAIN}_home",
)
mock_config.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance"
entity = entity_registry.async_get(sensor_t1)
assert entity
assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone"
entity = entity_registry.async_get("sensor.home_test2_distance")
assert entity
assert (
entity.unique_id == f"{mock_config.entry_id}_device_tracker.test2_dist_to_zone"
) )