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>
pull/107460/head^2
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
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_DEVICES,
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.event import (
async_track_entity_registry_updated_event,
async_track_state_change,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -27,12 +29,14 @@ from .const import (
ATTR_NEAREST,
CONF_IGNORED_ZONES,
CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DEFAULT_PROXIMITY_ZONE,
DEFAULT_TOLERANCE,
DOMAIN,
UNITS,
)
from .coordinator import ProximityDataUpdateCoordinator
from .helpers import entity_used_in
_LOGGER = logging.getLogger(__name__)
@ -49,63 +53,134 @@ ZONE_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_legacy(
hass: HomeAssistant, entry: ConfigEntry, coordinator: ProximityDataUpdateCoordinator
) -> None:
"""Legacy proximity entity handling, can be removed in 2024.8."""
friendly_name = entry.data[CONF_NAME]
proximity = Proximity(hass, friendly_name, coordinator)
await proximity.async_added_to_hass()
proximity.async_write_ha_state()
if used_in := entity_used_in(hass, f"{DOMAIN}.{friendly_name}"):
async_create_issue(
hass,
DOMAIN,
f"deprecated_proximity_entity_{friendly_name}",
breaks_in_ha_version="2024.8.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_proximity_entity",
translation_placeholders={
"entity": f"{DOMAIN}.{friendly_name}",
"used_in": "\n- ".join([f"`{x}`" for x in used_in]),
},
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Get the zones and offsets from configuration.yaml."""
hass.data.setdefault(DOMAIN, {})
for friendly_name, proximity_config in config[DOMAIN].items():
_LOGGER.debug("setup %s with config:%s", friendly_name, proximity_config)
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)
await proximity.async_added_to_hass()
proximity.async_write_ha_state()
await async_load_platform(
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(
hass,
DOMAIN,
f"deprecated_proximity_entity_{friendly_name}",
breaks_in_ha_version="2024.8.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_proximity_entity",
translation_placeholders={
"entity": f"{DOMAIN}.{friendly_name}",
"used_in": "\n- ".join([f"`{x}`" for x in used_in]),
},
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
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]):
"""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_TOLERANCE = "tolerance"
CONF_TRACKED_ENTITIES = "tracked_entities"
DEFAULT_DIR_OF_TRAVEL = "not set"
DEFAULT_DIST_TO_ZONE = "not set"

View File

@ -1,19 +1,23 @@
"""Data update coordinator for the Proximity integration."""
from collections import defaultdict
from dataclasses import dataclass
import logging
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_NAME,
CONF_DEVICES,
CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE,
UnitOfLength,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.typing import ConfigType
from homeassistant.core import HomeAssistant, State, callback
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.util.location import distance
from homeassistant.util.unit_conversion import DistanceConverter
@ -25,9 +29,11 @@ from .const import (
ATTR_NEAREST,
CONF_IGNORED_ZONES,
CONF_TOLERANCE,
CONF_TRACKED_ENTITIES,
DEFAULT_DIR_OF_TRAVEL,
DEFAULT_DIST_TO_ZONE,
DEFAULT_NEAREST,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@ -63,18 +69,21 @@ DEFAULT_DATA = ProximityData(
class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
"""Proximity data update coordinator."""
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, friendly_name: str, config: ConfigType
) -> None:
"""Initialize the Proximity coordinator."""
self.ignored_zones: list[str] = config[CONF_IGNORED_ZONES]
self.tracked_entities: list[str] = config[CONF_DEVICES]
self.ignored_zone_ids: list[str] = config[CONF_IGNORED_ZONES]
self.tracked_entities: list[str] = config[CONF_TRACKED_ENTITIES]
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(
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__(
hass,
@ -87,6 +96,11 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
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(
self, entity: str, old_state: State | None, new_state: State | None
) -> None:
@ -94,6 +108,31 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
self.state_change_data = StateChangedData(entity, old_state, new_state)
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:
"""Round and convert given distance value."""
if isinstance(value, str):
@ -113,10 +152,10 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
latitude: float | None,
longitude: float | None,
) -> int | None:
if device.state.lower() == self.proximity_zone.lower():
if device.state.lower() == self.proximity_zone_name.lower():
_LOGGER.debug(
"%s: %s in zone -> distance=0",
self.friendly_name,
self.name,
device.entity_id,
)
return 0
@ -124,7 +163,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if latitude is None or longitude is None:
_LOGGER.debug(
"%s: %s has no coordinates -> distance=None",
self.friendly_name,
self.name,
device.entity_id,
)
return None
@ -149,10 +188,10 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
new_latitude: float | None,
new_longitude: float | None,
) -> str | None:
if device.state.lower() == self.proximity_zone.lower():
if device.state.lower() == self.proximity_zone_name.lower():
_LOGGER.debug(
"%s: %s in zone -> direction_of_travel=arrived",
self.friendly_name,
self.name,
device.entity_id,
)
return "arrived"
@ -193,11 +232,11 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
async def _async_update_data(self) -> ProximityData:
"""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(
"%s: zone %s does not exist -> reset",
self.friendly_name,
self.proximity_zone,
self.name,
self.proximity_zone_id,
)
return DEFAULT_DATA
@ -208,12 +247,12 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if (tracked_entity_state := self.hass.states.get(entity_id)) is None:
if entities_data.pop(entity_id, None) is not None:
_LOGGER.debug(
"%s: %s does not exist -> remove", self.friendly_name, entity_id
"%s: %s does not exist -> remove", self.name, entity_id
)
continue
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] = {
ATTR_DIST_TO: None,
ATTR_DIR_OF_TRAVEL: None,
@ -221,7 +260,8 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
ATTR_IN_IGNORED_ZONE: False,
}
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(
zone_state,
@ -232,7 +272,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
if entities_data[entity_id][ATTR_DIST_TO] is None:
_LOGGER.debug(
"%s: %s has unknown distance got -> direction_of_travel=None",
self.friendly_name,
self.name,
entity_id,
)
entities_data[entity_id][ATTR_DIR_OF_TRAVEL] = None
@ -243,7 +283,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
) is not None:
_LOGGER.debug(
"%s: calculate direction of travel for %s",
self.friendly_name,
self.name,
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])
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",
"name": "Proximity",
"codeowners": ["@mib1185"],
"config_flow": true,
"dependencies": ["device_tracker", "zone"],
"documentation": "https://www.home-assistant.io/integrations/proximity",
"iot_class": "calculated",

View File

@ -7,10 +7,12 @@ from homeassistant.components.sensor import (
SensorEntity,
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.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
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(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Proximity sensor platform."""
if discovery_info is None:
return
def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name=coordinator.config_entry.title,
entry_type=DeviceEntryType.SERVICE,
)
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] = [
ProximitySensor(description, coordinator)
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 += [
ProximityTrackedEntitySensor(description, coordinator, tracked_entity_id)
ProximityTrackedEntitySensor(
description, coordinator, tracked_entity_descriptor
)
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)
@ -91,9 +115,8 @@ class ProximitySensor(CoordinatorEntity[ProximityDataUpdateCoordinator], SensorE
self.entity_description = description
# entity name will be removed as soon as we have a config entry
# and can follow the entity naming guidelines
self._attr_name = f"{coordinator.friendly_name} {description.name}"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
self._attr_device_info = _device_info(coordinator)
@property
def native_value(self) -> str | float | None:
@ -116,23 +139,38 @@ class ProximityTrackedEntitySensor(
self,
description: SensorEntityDescription,
coordinator: ProximityDataUpdateCoordinator,
tracked_entity_id: str,
tracked_entity_descriptor: dict[str, str],
) -> None:
"""Initialize the proximity."""
super().__init__(coordinator)
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
# and can follow the entity naming guidelines
self._attr_name = (
f"{coordinator.friendly_name} {tracked_entity_id} {description.name}"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor['identifier']}_{description.key}"
self._attr_name = f"{self.tracked_entity_id.split('.')[-1]} {description.name}"
self._attr_device_info = _device_info(coordinator)
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
def native_value(self) -> str | float | None:
"""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 data.get(self.entity_description.key)
return self.data.get(self.entity_description.key)

View File

@ -1,5 +1,34 @@
{
"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": {
"sensor": {
"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",
"progettihwsw",
"prosegur",
"proximity",
"prusalink",
"ps4",
"pure_energie",

View File

@ -4562,7 +4562,7 @@
},
"proximity": {
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "calculated"
},
"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.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.const import STATE_UNKNOWN
from homeassistant.const import CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
from homeassistant.setup import async_setup_component
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:
"""Test a list of proximities."""
config = {
"proximity": {
"home": {
@pytest.mark.parametrize(
("friendly_name", "config"),
[
(
"home",
{
"ignored_zones": ["work"],
"devices": ["device_tracker.test1", "device_tracker.test2"],
"tolerance": "1",
},
"home_test2": {
"ignored_zones": ["work"],
"devices": ["device_tracker.test1", "device_tracker.test2"],
"tolerance": "1",
},
"work": {
),
(
"work",
{
"devices": ["device_tracker.test1"],
"tolerance": "1",
"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()
# proximity entity
@ -50,31 +62,47 @@ async def test_proximities(hass: HomeAssistant, friendly_name: str) -> None:
assert state.state == "0"
# 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
for device in config["proximity"][friendly_name]["devices"]:
entity_base_name = f"sensor.{friendly_name}_{slugify(device)}"
for device in config["devices"]:
entity_base_name = f"sensor.{friendly_name}_{slugify(device.split('.')[-1])}"
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")
assert state.state == STATE_UNKNOWN
assert state.state == STATE_UNAVAILABLE
async def test_proximities_setup(hass: HomeAssistant) -> None:
"""Test a list of proximities with missing devices."""
async def test_legacy_setup(hass: HomeAssistant) -> None:
"""Test legacy setup only on imported entries."""
config = {
"proximity": {
"home": {
"ignored_zones": ["work"],
"devices": ["device_tracker.test1", "device_tracker.test2"],
"devices": ["device_tracker.test1"],
"tolerance": "1",
},
"work": {"tolerance": "1", "zone": "work"},
}
}
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:
@ -105,10 +133,10 @@ async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "arrived"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "0"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "11912010"
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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."""
config_zones(hass)
await hass.async_block_till_done()
config = {
@ -184,10 +213,10 @@ async def test_device_tracker_test1_awayfurther(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "4625264"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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."""
config_zones(hass)
await hass.async_block_till_done()
config = {
@ -246,10 +276,10 @@ async def test_device_tracker_test1_awaycloser(hass: HomeAssistant) -> None:
assert state.attributes.get("dir_of_travel") == "unknown"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "4625264"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == STATE_UNKNOWN
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == STATE_UNKNOWN
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "11912010"
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "11912010"
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
assert state.state == "test1, test2"
for device in ["device_tracker.test1", "device_tracker.test2"]:
entity_base_name = f"sensor.home_{slugify(device)}"
for device in ["test1", "test2"]:
entity_base_name = f"sensor.home_{device}"
state = hass.states.get(f"{entity_base_name}_distance")
assert state.state == "0"
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(
hass: HomeAssistant,
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker ordering."""
config_zones(hass)
await hass.async_block_till_done()
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == STATE_UNKNOWN
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "4625264"
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(
hass: HomeAssistant,
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker ordering."""
config_zones(hass)
await hass.async_block_till_done()
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "4625264"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "4625264"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "11912010"
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == STATE_UNKNOWN
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(
hass: HomeAssistant,
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker state."""
config_zones(hass)
await hass.async_block_till_done()
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == STATE_UNKNOWN
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "2218752"
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(
hass: HomeAssistant,
hass: HomeAssistant, config_zones
) -> None:
"""Test for tracker states."""
config_zones(hass)
await hass.async_block_till_done()
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == STATE_UNKNOWN
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "989156"
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"
# sensor entities
state = hass.states.get("sensor.home_nearest")
state = hass.states.get("sensor.home_nearest_device")
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")
assert state.state == "2218752"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
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")
assert state.state == "1364567"
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
assert state.state == "away_from"
async def test_create_issue(
async def test_create_deprecated_proximity_issue(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
@ -946,16 +972,142 @@ async def test_create_issue(
)
def config_zones(hass):
"""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},
async def test_create_removed_tracked_entity_issue(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test we create an issue for removed tracked entities."""
t1 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test1"
)
hass.states.async_set(
"zone.work",
"zoning",
{"name": "work", "latitude": 2.3, "longitude": 1.3, "radius": 10},
t2 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test2"
)
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"
)