mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Add sensor platform to Proximity (#101497)
* add sensor platform * transl. of distance already covered by dev.class * add untested files to .coveragerc * add missing state translations * remove translation key for distance sensor * proximity entity do not use HA number system * fix * extend tests * make const final to be usable as key for TypedDict * remove proximity from .coveragerc * replace typeddict by simple dict definition * make black happy * rework to create proximity sensor for each tracked entity and always recalculate all entites * apply review comments * move direction of travel calc out of the loop * make direction of travel an enum sensor * remove unique_id from sensors * don't set distance=0 when in monitored zone * set None when direction is unknown * keep distance 0 in case arrived for legacy entity * exclude from nearest when in ignored zone * keep distance=0 when arrived * use description name for entity name * remove uneeded typing * uses consistent variable name * fix debug messages * use entity_id as loop var * rename device_state to tracked_entity_state * correct MRO for sensor entity classes
This commit is contained in:
parent
fa63719161
commit
eaa32146a6
@ -5,15 +5,22 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT, CONF_ZONE
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_ZONE,
|
||||
)
|
||||
from homeassistant.core import 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.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_DIR_OF_TRAVEL,
|
||||
ATTR_DIST_TO,
|
||||
ATTR_NEAREST,
|
||||
CONF_IGNORED_ZONES,
|
||||
CONF_TOLERANCE,
|
||||
@ -46,10 +53,12 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Get the zones and offsets from configuration.yaml."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
for zone, proximity_config in config[DOMAIN].items():
|
||||
_LOGGER.debug("setup %s with config:%s", zone, proximity_config)
|
||||
for friendly_name, proximity_config in config[DOMAIN].items():
|
||||
_LOGGER.debug("setup %s with config:%s", friendly_name, proximity_config)
|
||||
|
||||
coordinator = ProximityDataUpdateCoordinator(hass, zone, proximity_config)
|
||||
coordinator = ProximityDataUpdateCoordinator(
|
||||
hass, friendly_name, proximity_config
|
||||
)
|
||||
|
||||
async_track_state_change(
|
||||
hass,
|
||||
@ -58,12 +67,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
hass.data[DOMAIN][zone] = coordinator
|
||||
hass.data[DOMAIN][friendly_name] = coordinator
|
||||
|
||||
proximity = Proximity(hass, zone, 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,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@ -91,12 +107,14 @@ class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]):
|
||||
@property
|
||||
def state(self) -> str | int | float:
|
||||
"""Return the state."""
|
||||
return self.coordinator.data["dist_to_zone"]
|
||||
return self.coordinator.data.proximity[ATTR_DIST_TO]
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_DIR_OF_TRAVEL: str(self.coordinator.data["dir_of_travel"]),
|
||||
ATTR_NEAREST: str(self.coordinator.data["nearest"]),
|
||||
ATTR_DIR_OF_TRAVEL: str(
|
||||
self.coordinator.data.proximity[ATTR_DIR_OF_TRAVEL]
|
||||
),
|
||||
ATTR_NEAREST: str(self.coordinator.data.proximity[ATTR_NEAREST]),
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
"""Constants for Proximity integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import UnitOfLength
|
||||
|
||||
ATTR_DIR_OF_TRAVEL = "dir_of_travel"
|
||||
ATTR_DIST_TO = "dist_to_zone"
|
||||
ATTR_NEAREST = "nearest"
|
||||
ATTR_DIR_OF_TRAVEL: Final = "dir_of_travel"
|
||||
ATTR_DIST_TO: Final = "dist_to_zone"
|
||||
ATTR_ENTITIES_DATA: Final = "entities_data"
|
||||
ATTR_IN_IGNORED_ZONE: Final = "is_in_ignored_zone"
|
||||
ATTR_NEAREST: Final = "nearest"
|
||||
ATTR_PROXIMITY_DATA: Final = "proximity_data"
|
||||
|
||||
CONF_IGNORED_ZONES = "ignored_zones"
|
||||
CONF_TOLERANCE = "tolerance"
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TypedDict
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_NAME,
|
||||
CONF_DEVICES,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_ZONE,
|
||||
@ -19,6 +19,10 @@ from homeassistant.util.location import distance
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
|
||||
from .const import (
|
||||
ATTR_DIR_OF_TRAVEL,
|
||||
ATTR_DIST_TO,
|
||||
ATTR_IN_IGNORED_ZONE,
|
||||
ATTR_NEAREST,
|
||||
CONF_IGNORED_ZONES,
|
||||
CONF_TOLERANCE,
|
||||
DEFAULT_DIR_OF_TRAVEL,
|
||||
@ -38,12 +42,22 @@ class StateChangedData:
|
||||
new_state: State | None
|
||||
|
||||
|
||||
class ProximityData(TypedDict):
|
||||
"""ProximityData type class."""
|
||||
@dataclass
|
||||
class ProximityData:
|
||||
"""ProximityCoordinatorData class."""
|
||||
|
||||
dist_to_zone: str | float
|
||||
dir_of_travel: str | float
|
||||
nearest: str | float
|
||||
proximity: dict[str, str | float]
|
||||
entities: dict[str, dict[str, str | int | None]]
|
||||
|
||||
|
||||
DEFAULT_DATA = ProximityData(
|
||||
{
|
||||
ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
|
||||
ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
|
||||
ATTR_NEAREST: DEFAULT_NEAREST,
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
||||
@ -54,7 +68,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
||||
) -> None:
|
||||
"""Initialize the Proximity coordinator."""
|
||||
self.ignored_zones: list[str] = config[CONF_IGNORED_ZONES]
|
||||
self.proximity_devices: list[str] = config[CONF_DEVICES]
|
||||
self.tracked_entities: list[str] = config[CONF_DEVICES]
|
||||
self.tolerance: int = config[CONF_TOLERANCE]
|
||||
self.proximity_zone: str = config[CONF_ZONE]
|
||||
self.unit_of_measurement: str = config.get(
|
||||
@ -69,11 +83,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
||||
update_interval=None,
|
||||
)
|
||||
|
||||
self.data = {
|
||||
"dist_to_zone": DEFAULT_DIST_TO_ZONE,
|
||||
"dir_of_travel": DEFAULT_DIR_OF_TRAVEL,
|
||||
"nearest": DEFAULT_NEAREST,
|
||||
}
|
||||
self.data = DEFAULT_DATA
|
||||
|
||||
self.state_change_data: StateChangedData | None = None
|
||||
|
||||
@ -81,177 +91,216 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
||||
self, entity: str, old_state: State | None, new_state: State | None
|
||||
) -> None:
|
||||
"""Fetch and process state change event."""
|
||||
if new_state is None:
|
||||
_LOGGER.debug("no new_state -> abort")
|
||||
return
|
||||
|
||||
self.state_change_data = StateChangedData(entity, old_state, new_state)
|
||||
await self.async_refresh()
|
||||
|
||||
async def _async_update_data(self) -> ProximityData:
|
||||
"""Calculate Proximity data."""
|
||||
if (
|
||||
state_change_data := self.state_change_data
|
||||
) is None or state_change_data.new_state is None:
|
||||
return self.data
|
||||
|
||||
entity_name = state_change_data.new_state.name
|
||||
devices_to_calculate = False
|
||||
devices_in_zone = []
|
||||
|
||||
zone_state = self.hass.states.get(f"zone.{self.proximity_zone}")
|
||||
proximity_latitude = (
|
||||
zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None
|
||||
)
|
||||
proximity_longitude = (
|
||||
zone_state.attributes.get(ATTR_LONGITUDE) if zone_state else None
|
||||
def _convert(self, value: float | str) -> float | str:
|
||||
"""Round and convert given distance value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return round(
|
||||
DistanceConverter.convert(
|
||||
value,
|
||||
UnitOfLength.METERS,
|
||||
self.unit_of_measurement,
|
||||
)
|
||||
)
|
||||
|
||||
# Check for devices in the monitored zone.
|
||||
for device in self.proximity_devices:
|
||||
if (device_state := self.hass.states.get(device)) is None:
|
||||
devices_to_calculate = True
|
||||
continue
|
||||
|
||||
if device_state.state not in self.ignored_zones:
|
||||
devices_to_calculate = True
|
||||
|
||||
# Check the location of all devices.
|
||||
if (device_state.state).lower() == (self.proximity_zone).lower():
|
||||
device_friendly = device_state.name
|
||||
devices_in_zone.append(device_friendly)
|
||||
|
||||
# No-one to track so reset the entity.
|
||||
if not devices_to_calculate:
|
||||
_LOGGER.debug("no devices_to_calculate -> abort")
|
||||
return {
|
||||
"dist_to_zone": DEFAULT_DIST_TO_ZONE,
|
||||
"dir_of_travel": DEFAULT_DIR_OF_TRAVEL,
|
||||
"nearest": DEFAULT_NEAREST,
|
||||
}
|
||||
|
||||
# At least one device is in the monitored zone so update the entity.
|
||||
if devices_in_zone:
|
||||
_LOGGER.debug("at least one device is in zone -> arrived")
|
||||
return {
|
||||
"dist_to_zone": 0,
|
||||
"dir_of_travel": "arrived",
|
||||
"nearest": ", ".join(devices_in_zone),
|
||||
}
|
||||
|
||||
# We can't check proximity because latitude and longitude don't exist.
|
||||
if "latitude" not in state_change_data.new_state.attributes:
|
||||
_LOGGER.debug("no latitude and longitude -> reset")
|
||||
return self.data
|
||||
|
||||
# Collect distances to the zone for all devices.
|
||||
distances_to_zone: dict[str, float] = {}
|
||||
for device in self.proximity_devices:
|
||||
# Ignore devices in an ignored zone.
|
||||
device_state = self.hass.states.get(device)
|
||||
if not device_state or device_state.state in self.ignored_zones:
|
||||
continue
|
||||
|
||||
# Ignore devices if proximity cannot be calculated.
|
||||
if "latitude" not in device_state.attributes:
|
||||
continue
|
||||
|
||||
# Calculate the distance to the proximity zone.
|
||||
proximity = distance(
|
||||
proximity_latitude,
|
||||
proximity_longitude,
|
||||
device_state.attributes[ATTR_LATITUDE],
|
||||
device_state.attributes[ATTR_LONGITUDE],
|
||||
def _calc_distance_to_zone(
|
||||
self,
|
||||
zone: State,
|
||||
device: State,
|
||||
latitude: float | None,
|
||||
longitude: float | None,
|
||||
) -> int | None:
|
||||
if device.state.lower() == self.proximity_zone.lower():
|
||||
_LOGGER.debug(
|
||||
"%s: %s in zone -> distance=0",
|
||||
self.friendly_name,
|
||||
device.entity_id,
|
||||
)
|
||||
return 0
|
||||
|
||||
# Add the device and distance to a dictionary.
|
||||
if proximity is None:
|
||||
continue
|
||||
distances_to_zone[device] = round(
|
||||
DistanceConverter.convert(
|
||||
proximity, UnitOfLength.METERS, self.unit_of_measurement
|
||||
),
|
||||
1,
|
||||
if latitude is None or longitude is None:
|
||||
_LOGGER.debug(
|
||||
"%s: %s has no coordinates -> distance=None",
|
||||
self.friendly_name,
|
||||
device.entity_id,
|
||||
)
|
||||
return None
|
||||
|
||||
# Loop through each of the distances collected and work out the
|
||||
# closest.
|
||||
closest_device: str | None = None
|
||||
dist_to_zone: float | None = None
|
||||
distance_to_zone = distance(
|
||||
zone.attributes[ATTR_LATITUDE],
|
||||
zone.attributes[ATTR_LONGITUDE],
|
||||
latitude,
|
||||
longitude,
|
||||
)
|
||||
|
||||
for device, zone in distances_to_zone.items():
|
||||
if not dist_to_zone or zone < dist_to_zone:
|
||||
closest_device = device
|
||||
dist_to_zone = zone
|
||||
# it is ensured, that distance can't be None, since zones must have lat/lon coordinates
|
||||
assert distance_to_zone is not None
|
||||
return round(distance_to_zone)
|
||||
|
||||
# If the closest device is one of the other devices.
|
||||
if closest_device is not None and closest_device != state_change_data.entity_id:
|
||||
_LOGGER.debug("closest device is one of the other devices -> unknown")
|
||||
device_state = self.hass.states.get(closest_device)
|
||||
assert device_state
|
||||
return {
|
||||
"dist_to_zone": round(distances_to_zone[closest_device]),
|
||||
"dir_of_travel": "unknown",
|
||||
"nearest": device_state.name,
|
||||
}
|
||||
def _calc_direction_of_travel(
|
||||
self,
|
||||
zone: State,
|
||||
device: State,
|
||||
old_latitude: float | None,
|
||||
old_longitude: float | None,
|
||||
new_latitude: float | None,
|
||||
new_longitude: float | None,
|
||||
) -> str | None:
|
||||
if device.state.lower() == self.proximity_zone.lower():
|
||||
_LOGGER.debug(
|
||||
"%s: %s in zone -> direction_of_travel=arrived",
|
||||
self.friendly_name,
|
||||
device.entity_id,
|
||||
)
|
||||
return "arrived"
|
||||
|
||||
# Stop if we cannot calculate the direction of travel (i.e. we don't
|
||||
# have a previous state and a current LAT and LONG).
|
||||
if (
|
||||
state_change_data.old_state is None
|
||||
or "latitude" not in state_change_data.old_state.attributes
|
||||
old_latitude is None
|
||||
or old_longitude is None
|
||||
or new_latitude is None
|
||||
or new_longitude is None
|
||||
):
|
||||
_LOGGER.debug("no lat and lon in old_state -> unknown")
|
||||
return {
|
||||
"dist_to_zone": round(distances_to_zone[state_change_data.entity_id]),
|
||||
"dir_of_travel": "unknown",
|
||||
"nearest": entity_name,
|
||||
}
|
||||
return None
|
||||
|
||||
# Reset the variables
|
||||
distance_travelled: float = 0
|
||||
|
||||
# Calculate the distance travelled.
|
||||
old_distance = distance(
|
||||
proximity_latitude,
|
||||
proximity_longitude,
|
||||
state_change_data.old_state.attributes[ATTR_LATITUDE],
|
||||
state_change_data.old_state.attributes[ATTR_LONGITUDE],
|
||||
zone.attributes[ATTR_LATITUDE],
|
||||
zone.attributes[ATTR_LONGITUDE],
|
||||
old_latitude,
|
||||
old_longitude,
|
||||
)
|
||||
new_distance = distance(
|
||||
proximity_latitude,
|
||||
proximity_longitude,
|
||||
state_change_data.new_state.attributes[ATTR_LATITUDE],
|
||||
state_change_data.new_state.attributes[ATTR_LONGITUDE],
|
||||
zone.attributes[ATTR_LATITUDE],
|
||||
zone.attributes[ATTR_LONGITUDE],
|
||||
new_latitude,
|
||||
new_longitude,
|
||||
)
|
||||
assert new_distance is not None and old_distance is not None
|
||||
|
||||
# it is ensured, that distance can't be None, since zones must have lat/lon coordinates
|
||||
assert old_distance is not None
|
||||
assert new_distance is not None
|
||||
distance_travelled = round(new_distance - old_distance, 1)
|
||||
|
||||
# Check for tolerance
|
||||
if distance_travelled < self.tolerance * -1:
|
||||
direction_of_travel = "towards"
|
||||
elif distance_travelled > self.tolerance:
|
||||
direction_of_travel = "away_from"
|
||||
else:
|
||||
direction_of_travel = "stationary"
|
||||
return "towards"
|
||||
|
||||
# Update the proximity entity
|
||||
dist_to: float | str
|
||||
if dist_to_zone is not None:
|
||||
dist_to = round(dist_to_zone)
|
||||
else:
|
||||
dist_to = DEFAULT_DIST_TO_ZONE
|
||||
if distance_travelled > self.tolerance:
|
||||
return "away_from"
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s updated: distance=%s: direction=%s: device=%s",
|
||||
self.friendly_name,
|
||||
dist_to,
|
||||
direction_of_travel,
|
||||
entity_name,
|
||||
)
|
||||
return "stationary"
|
||||
|
||||
return {
|
||||
"dist_to_zone": dist_to,
|
||||
"dir_of_travel": direction_of_travel,
|
||||
"nearest": entity_name,
|
||||
async def _async_update_data(self) -> ProximityData:
|
||||
"""Calculate Proximity data."""
|
||||
if (zone_state := self.hass.states.get(f"zone.{self.proximity_zone}")) is None:
|
||||
_LOGGER.debug(
|
||||
"%s: zone %s does not exist -> reset",
|
||||
self.friendly_name,
|
||||
self.proximity_zone,
|
||||
)
|
||||
return DEFAULT_DATA
|
||||
|
||||
entities_data = self.data.entities
|
||||
|
||||
# calculate distance for all tracked entities
|
||||
for entity_id in self.tracked_entities:
|
||||
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
|
||||
)
|
||||
continue
|
||||
|
||||
if entity_id not in entities_data:
|
||||
_LOGGER.debug("%s: %s is new -> add", self.friendly_name, entity_id)
|
||||
entities_data[entity_id] = {
|
||||
ATTR_DIST_TO: None,
|
||||
ATTR_DIR_OF_TRAVEL: None,
|
||||
ATTR_NAME: tracked_entity_state.name,
|
||||
ATTR_IN_IGNORED_ZONE: False,
|
||||
}
|
||||
entities_data[entity_id][ATTR_IN_IGNORED_ZONE] = (
|
||||
tracked_entity_state.state.lower() in self.ignored_zones
|
||||
)
|
||||
entities_data[entity_id][ATTR_DIST_TO] = self._calc_distance_to_zone(
|
||||
zone_state,
|
||||
tracked_entity_state,
|
||||
tracked_entity_state.attributes.get(ATTR_LATITUDE),
|
||||
tracked_entity_state.attributes.get(ATTR_LONGITUDE),
|
||||
)
|
||||
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,
|
||||
entity_id,
|
||||
)
|
||||
entities_data[entity_id][ATTR_DIR_OF_TRAVEL] = None
|
||||
|
||||
# calculate direction of travel only for last updated tracked entity
|
||||
if (state_change_data := self.state_change_data) is not None and (
|
||||
new_state := state_change_data.new_state
|
||||
) is not None:
|
||||
_LOGGER.debug(
|
||||
"%s: calculate direction of travel for %s",
|
||||
self.friendly_name,
|
||||
state_change_data.entity_id,
|
||||
)
|
||||
|
||||
if (old_state := state_change_data.old_state) is not None:
|
||||
old_lat = old_state.attributes.get(ATTR_LATITUDE)
|
||||
old_lon = old_state.attributes.get(ATTR_LONGITUDE)
|
||||
else:
|
||||
old_lat = None
|
||||
old_lon = None
|
||||
|
||||
entities_data[state_change_data.entity_id][
|
||||
ATTR_DIR_OF_TRAVEL
|
||||
] = self._calc_direction_of_travel(
|
||||
zone_state,
|
||||
new_state,
|
||||
old_lat,
|
||||
old_lon,
|
||||
new_state.attributes.get(ATTR_LATITUDE),
|
||||
new_state.attributes.get(ATTR_LONGITUDE),
|
||||
)
|
||||
|
||||
# takeover data for legacy proximity entity
|
||||
proximity_data: dict[str, str | float] = {
|
||||
ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
|
||||
ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
|
||||
ATTR_NEAREST: DEFAULT_NEAREST,
|
||||
}
|
||||
for entity_data in entities_data.values():
|
||||
if (distance_to := entity_data[ATTR_DIST_TO]) is None or entity_data[
|
||||
ATTR_IN_IGNORED_ZONE
|
||||
]:
|
||||
continue
|
||||
|
||||
if isinstance((nearest_distance_to := proximity_data[ATTR_DIST_TO]), str):
|
||||
_LOGGER.debug("set first entity_data: %s", entity_data)
|
||||
proximity_data = {
|
||||
ATTR_DIST_TO: distance_to,
|
||||
ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL] or "unknown",
|
||||
ATTR_NEAREST: str(entity_data[ATTR_NAME]),
|
||||
}
|
||||
continue
|
||||
|
||||
if float(nearest_distance_to) > float(distance_to):
|
||||
_LOGGER.debug("set closer entity_data: %s", entity_data)
|
||||
proximity_data = {
|
||||
ATTR_DIST_TO: distance_to,
|
||||
ATTR_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL] or "unknown",
|
||||
ATTR_NEAREST: str(entity_data[ATTR_NAME]),
|
||||
}
|
||||
continue
|
||||
|
||||
if float(nearest_distance_to) == float(distance_to):
|
||||
_LOGGER.debug("set equally close entity_data: %s", entity_data)
|
||||
proximity_data[
|
||||
ATTR_NEAREST
|
||||
] = f"{proximity_data[ATTR_NEAREST]}, {str(entity_data[ATTR_NAME])}"
|
||||
|
||||
proximity_data[ATTR_DIST_TO] = self._convert(proximity_data[ATTR_DIST_TO])
|
||||
|
||||
return ProximityData(proximity_data, entities_data)
|
||||
|
138
homeassistant/components/proximity/sensor.py
Normal file
138
homeassistant/components/proximity/sensor.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""Support for Proximity sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
from .coordinator import ProximityDataUpdateCoordinator
|
||||
|
||||
SENSORS_PER_ENTITY: list[SensorEntityDescription] = [
|
||||
SensorEntityDescription(
|
||||
key=ATTR_DIST_TO,
|
||||
name="Distance",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_DIR_OF_TRAVEL,
|
||||
name="Direction of travel",
|
||||
translation_key=ATTR_DIR_OF_TRAVEL,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"arrived",
|
||||
"away_from",
|
||||
"stationary",
|
||||
"towards",
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [
|
||||
SensorEntityDescription(
|
||||
key=ATTR_NEAREST,
|
||||
name="Nearest",
|
||||
translation_key=ATTR_NEAREST,
|
||||
icon="mdi:near-me",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
coordinator: ProximityDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
discovery_info[CONF_NAME]
|
||||
]
|
||||
|
||||
entities: list[ProximitySensor | ProximityTrackedEntitySensor] = [
|
||||
ProximitySensor(description, coordinator)
|
||||
for description in SENSORS_PER_PROXIMITY
|
||||
]
|
||||
|
||||
entities += [
|
||||
ProximityTrackedEntitySensor(description, coordinator, tracked_entity_id)
|
||||
for description in SENSORS_PER_ENTITY
|
||||
for tracked_entity_id in coordinator.tracked_entities
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ProximitySensor(CoordinatorEntity[ProximityDataUpdateCoordinator], SensorEntity):
|
||||
"""Represents a Proximity sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: ProximityDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the proximity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
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}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | float | None:
|
||||
"""Return native sensor value."""
|
||||
if (
|
||||
value := self.coordinator.data.proximity[self.entity_description.key]
|
||||
) == "not set":
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
class ProximityTrackedEntitySensor(
|
||||
CoordinatorEntity[ProximityDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Represents a Proximity tracked entity sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: SensorEntityDescription,
|
||||
coordinator: ProximityDataUpdateCoordinator,
|
||||
tracked_entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the proximity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self.tracked_entity_id = tracked_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}"
|
||||
)
|
||||
|
||||
@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:
|
||||
return None
|
||||
return data.get(self.entity_description.key)
|
@ -1,3 +1,17 @@
|
||||
{
|
||||
"title": "Proximity"
|
||||
"title": "Proximity",
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"dir_of_travel": {
|
||||
"name": "Direction of travel",
|
||||
"state": {
|
||||
"arrived": "Arrived",
|
||||
"away_from": "Away from",
|
||||
"stationary": "Stationary",
|
||||
"towards": "Towards"
|
||||
}
|
||||
},
|
||||
"nearest": { "name": "Nearest device" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,16 @@
|
||||
"""The tests for the Proximity component."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.proximity import DOMAIN
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
async def test_proximities(hass: HomeAssistant) -> None:
|
||||
@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": {
|
||||
@ -27,19 +33,28 @@ async def test_proximities(hass: HomeAssistant) -> None:
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
proximities = ["home", "home_test2", "work"]
|
||||
# proximity entity
|
||||
state = hass.states.get(f"proximity.{friendly_name}")
|
||||
assert state.state == "not set"
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
hass.states.async_set(f"proximity.{friendly_name}", "0")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"proximity.{friendly_name}")
|
||||
assert state.state == "0"
|
||||
|
||||
for prox in proximities:
|
||||
state = hass.states.get(f"proximity.{prox}")
|
||||
assert state.state == "not set"
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
# sensor entities
|
||||
state = hass.states.get(f"sensor.{friendly_name}_nearest")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
hass.states.async_set(f"proximity.{prox}", "0")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"proximity.{prox}")
|
||||
assert state.state == "0"
|
||||
for device in config["proximity"][friendly_name]["devices"]:
|
||||
entity_base_name = f"sensor.{friendly_name}_{slugify(device)}"
|
||||
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
|
||||
|
||||
|
||||
async def test_proximities_setup(hass: HomeAssistant) -> None:
|
||||
@ -58,31 +73,6 @@ async def test_proximities_setup(hass: HomeAssistant) -> None:
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
|
||||
async def test_proximity(hass: HomeAssistant) -> None:
|
||||
"""Test the proximity."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1", "device_tracker.test2"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "not set"
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
|
||||
hass.states.async_set("proximity.home", "0")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "0"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker in zone."""
|
||||
config = {
|
||||
@ -103,11 +93,317 @@ async def test_device_tracker_test1_in_zone(hass: HomeAssistant) -> None:
|
||||
{"friendly_name": "test1", "latitude": 2.1, "longitude": 1.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "0"
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "arrived"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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")
|
||||
assert state.state == "arrived"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_away(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker state away."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}"
|
||||
state = hass.states.get(f"{entity_base_name}_distance")
|
||||
assert state.state == "11912010"
|
||||
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:
|
||||
"""Test for tracker state away further."""
|
||||
|
||||
config_zones(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "away_from"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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:
|
||||
"""Test for tracker state away closer."""
|
||||
config_zones(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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 == STATE_UNKNOWN
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "towards"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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 == "towards"
|
||||
|
||||
|
||||
async def test_all_device_trackers_in_ignored_zone(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker in ignored zone."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set("device_tracker.test1", "work", {"friendly_name": "test1"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "not set"
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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
|
||||
|
||||
|
||||
async def test_device_tracker_test1_no_coordinates(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker with no coordinates."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1", "not_home", {"friendly_name": "test1"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker states."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": 1000,
|
||||
"zone": "home",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1000001, "longitude": 10.1000001},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}"
|
||||
state = hass.states.get(f"{entity_base_name}_distance")
|
||||
assert state.state == "11912010"
|
||||
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1000002, "longitude": 10.1000002},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "stationary"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}"
|
||||
state = hass.states.get(f"{entity_base_name}_distance")
|
||||
assert state.state == "11912010"
|
||||
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
|
||||
assert state.state == "stationary"
|
||||
|
||||
|
||||
async def test_device_trackers_in_zone(hass: HomeAssistant) -> None:
|
||||
"""Test for trackers in zone."""
|
||||
@ -135,6 +431,8 @@ async def test_device_trackers_in_zone(hass: HomeAssistant) -> None:
|
||||
{"friendly_name": "test2", "latitude": 2.1, "longitude": 1.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "0"
|
||||
assert (state.attributes.get("nearest") == "test1, test2") or (
|
||||
@ -142,153 +440,16 @@ 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")
|
||||
assert state.state == "test1, test2"
|
||||
|
||||
async def test_device_tracker_test1_away(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker state away."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker state away further."""
|
||||
|
||||
config_zones(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "away_from"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awaycloser(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker state away closer."""
|
||||
config_zones(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "towards"
|
||||
|
||||
|
||||
async def test_all_device_trackers_in_ignored_zone(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker in ignored zone."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set("device_tracker.test1", "work", {"friendly_name": "test1"})
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.state == "not set"
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_no_coordinates(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker with no coordinates."""
|
||||
config = {
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1", "not_home", {"friendly_name": "test1"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "not set"
|
||||
assert state.attributes.get("dir_of_travel") == "not set"
|
||||
for device in ["device_tracker.test1", "device_tracker.test2"]:
|
||||
entity_base_name = f"sensor.home_{slugify(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")
|
||||
assert state.state == "arrived"
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
|
||||
@ -328,20 +489,56 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test1(
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"not_home",
|
||||
{"friendly_name": "test2", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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 == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_than_test2_first_test2(
|
||||
hass: HomeAssistant,
|
||||
@ -378,20 +575,56 @@ async def test_device_tracker_test1_awayfurther_than_test2_first_test2(
|
||||
{"friendly_name": "test2", "latitude": 40.1, "longitude": 20.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test2"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test2"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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 == STATE_UNKNOWN
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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 == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(
|
||||
hass: HomeAssistant,
|
||||
@ -423,10 +656,28 @@ async def test_device_tracker_test1_awayfurther_test2_in_ignored_zone(
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.test1')}"
|
||||
state = hass.states.get(f"{entity_base_name}_distance")
|
||||
assert state.state == "11912010"
|
||||
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')}"
|
||||
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
|
||||
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_test2_first(
|
||||
hass: HomeAssistant,
|
||||
@ -489,47 +740,26 @@ async def test_device_tracker_test1_awayfurther_test2_first(
|
||||
hass.states.async_set("device_tracker.test1", "work", {"friendly_name": "test1"})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test2"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test2"
|
||||
|
||||
async def test_device_tracker_test1_awayfurther_a_bit(hass: HomeAssistant) -> None:
|
||||
"""Test for tracker states."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
"proximity": {
|
||||
"home": {
|
||||
"ignored_zones": ["work"],
|
||||
"devices": ["device_tracker.test1"],
|
||||
"tolerance": 1000,
|
||||
"zone": "home",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1000001, "longitude": 10.1000001},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20.1000002, "longitude": 10.1000002},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "stationary"
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
|
||||
@ -568,30 +798,84 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
|
||||
{"friendly_name": "test1", "latitude": 20.1, "longitude": 10.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"not_home",
|
||||
{"friendly_name": "test2", "latitude": 10.1, "longitude": 5.1},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test2"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test2"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
state = hass.states.get(f"{entity_base_name}_distance")
|
||||
assert state.state == "989156"
|
||||
state = hass.states.get(f"{entity_base_name}_direction_of_travel")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"work",
|
||||
{"friendly_name": "test2", "latitude": 12.6, "longitude": 7.6},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# proximity entity
|
||||
state = hass.states.get("proximity.home")
|
||||
assert state.attributes.get("nearest") == "test1"
|
||||
assert state.attributes.get("dir_of_travel") == "unknown"
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest")
|
||||
assert state.state == "test1"
|
||||
|
||||
entity_base_name = f"sensor.home_{slugify('device_tracker.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')}"
|
||||
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"
|
||||
|
||||
|
||||
def config_zones(hass):
|
||||
"""Set up zones for test."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user