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
parent
c587c69915
commit
30c5baf522
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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)),
|
||||
)
|
|
@ -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"
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -393,6 +393,7 @@ FLOWS = {
|
|||
"profiler",
|
||||
"progettihwsw",
|
||||
"prosegur",
|
||||
"proximity",
|
||||
"prusalink",
|
||||
"ps4",
|
||||
"pure_energie",
|
||||
|
|
|
@ -4562,7 +4562,7 @@
|
|||
},
|
||||
"proximity": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"proxmoxve": {
|
||||
|
|
|
@ -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},
|
||||
)
|
|
@ -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()
|
|
@ -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"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue