Add zone related sensors in proximity (#109630)
* move legacy needed convertions into legacy entity * add zone related sensors * fix test coverage * fix typing * fix entity name translations * rename placeholder to tracked_entitypull/109664/head
parent
a95a51da05
commit
ffe9f0825a
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -11,6 +12,7 @@ from homeassistant.const import (
|
|||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_ZONE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
|
@ -203,16 +205,21 @@ class Proximity(CoordinatorEntity[ProximityDataUpdateCoordinator]):
|
|||
self._attr_unit_of_measurement = self.coordinator.unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self) -> str | int | float:
|
||||
def data(self) -> dict[str, str | int | None]:
|
||||
"""Get data from coordinator."""
|
||||
return self.coordinator.data.proximity
|
||||
|
||||
@property
|
||||
def state(self) -> str | float:
|
||||
"""Return the state."""
|
||||
return self.coordinator.data.proximity[ATTR_DIST_TO]
|
||||
if isinstance(distance := self.data[ATTR_DIST_TO], str):
|
||||
return distance
|
||||
return self.coordinator.convert_legacy(cast(int, distance))
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_DIR_OF_TRAVEL: str(
|
||||
self.coordinator.data.proximity[ATTR_DIR_OF_TRAVEL]
|
||||
),
|
||||
ATTR_NEAREST: str(self.coordinator.data.proximity[ATTR_NEAREST]),
|
||||
ATTR_DIR_OF_TRAVEL: str(self.data[ATTR_DIR_OF_TRAVEL] or STATE_UNKNOWN),
|
||||
ATTR_NEAREST: str(self.data[ATTR_NEAREST]),
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ 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_NEAREST_DIR_OF_TRAVEL: Final = "nearest_dir_of_travel"
|
||||
ATTR_NEAREST_DIST_TO: Final = "nearest_dist_to_zone"
|
||||
ATTR_PROXIMITY_DATA: Final = "proximity_data"
|
||||
|
||||
CONF_IGNORED_ZONES = "ignored_zones"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -52,11 +53,11 @@ class StateChangedData:
|
|||
class ProximityData:
|
||||
"""ProximityCoordinatorData class."""
|
||||
|
||||
proximity: dict[str, str | float]
|
||||
proximity: dict[str, str | int | None]
|
||||
entities: dict[str, dict[str, str | int | None]]
|
||||
|
||||
|
||||
DEFAULT_PROXIMITY_DATA: dict[str, str | float] = {
|
||||
DEFAULT_PROXIMITY_DATA: dict[str, str | int | None] = {
|
||||
ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
|
||||
ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
|
||||
ATTR_NEAREST: DEFAULT_NEAREST,
|
||||
|
@ -130,7 +131,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
|||
},
|
||||
)
|
||||
|
||||
def _convert(self, value: float | str) -> float | str:
|
||||
def convert_legacy(self, value: float | str) -> float | str:
|
||||
"""Round and convert given distance value."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
@ -303,7 +304,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
|||
)
|
||||
|
||||
# takeover data for legacy proximity entity
|
||||
proximity_data: dict[str, str | float] = {
|
||||
proximity_data: dict[str, str | int | None] = {
|
||||
ATTR_DIST_TO: DEFAULT_DIST_TO_ZONE,
|
||||
ATTR_DIR_OF_TRAVEL: DEFAULT_DIR_OF_TRAVEL,
|
||||
ATTR_NEAREST: DEFAULT_NEAREST,
|
||||
|
@ -318,28 +319,26 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]):
|
|||
_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_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL],
|
||||
ATTR_NEAREST: str(entity_data[ATTR_NAME]),
|
||||
}
|
||||
continue
|
||||
|
||||
if float(nearest_distance_to) > float(distance_to):
|
||||
if cast(int, nearest_distance_to) > int(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_DIR_OF_TRAVEL: entity_data[ATTR_DIR_OF_TRAVEL],
|
||||
ATTR_NEAREST: str(entity_data[ATTR_NAME]),
|
||||
}
|
||||
continue
|
||||
|
||||
if float(nearest_distance_to) == float(distance_to):
|
||||
if cast(int, nearest_distance_to) == int(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)
|
||||
|
||||
def _create_removed_tracked_entity_issue(self, entity_id: str) -> None:
|
||||
|
|
|
@ -17,38 +17,53 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_DIR_OF_TRAVEL, ATTR_DIST_TO, ATTR_NEAREST, DOMAIN
|
||||
from .const import (
|
||||
ATTR_DIR_OF_TRAVEL,
|
||||
ATTR_DIST_TO,
|
||||
ATTR_NEAREST,
|
||||
ATTR_NEAREST_DIR_OF_TRAVEL,
|
||||
ATTR_NEAREST_DIST_TO,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import ProximityDataUpdateCoordinator
|
||||
|
||||
DIRECTIONS = ["arrived", "away_from", "stationary", "towards"]
|
||||
|
||||
SENSORS_PER_ENTITY: list[SensorEntityDescription] = [
|
||||
SensorEntityDescription(
|
||||
key=ATTR_DIST_TO,
|
||||
name="Distance",
|
||||
translation_key=ATTR_DIST_TO,
|
||||
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",
|
||||
],
|
||||
options=DIRECTIONS,
|
||||
),
|
||||
]
|
||||
|
||||
SENSORS_PER_PROXIMITY: list[SensorEntityDescription] = [
|
||||
SensorEntityDescription(
|
||||
key=ATTR_NEAREST,
|
||||
name="Nearest",
|
||||
translation_key=ATTR_NEAREST,
|
||||
icon="mdi:near-me",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_DIST_TO,
|
||||
translation_key=ATTR_NEAREST_DIST_TO,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_DIR_OF_TRAVEL,
|
||||
translation_key=ATTR_NEAREST_DIR_OF_TRAVEL,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=DIRECTIONS,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -151,8 +166,10 @@ class ProximityTrackedEntitySensor(
|
|||
self.tracked_entity_id = tracked_entity_descriptor.entity_id
|
||||
|
||||
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)
|
||||
self._attr_translation_placeholders = {
|
||||
"tracked_entity": self.tracked_entity_id.split(".")[-1]
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register entity mapping."""
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"entity": {
|
||||
"sensor": {
|
||||
"dir_of_travel": {
|
||||
"name": "Direction of travel",
|
||||
"name": "{tracked_entity} Direction of travel",
|
||||
"state": {
|
||||
"arrived": "Arrived",
|
||||
"away_from": "Away from",
|
||||
|
@ -40,7 +40,18 @@
|
|||
"towards": "Towards"
|
||||
}
|
||||
},
|
||||
"nearest": { "name": "Nearest device" }
|
||||
"dist_to_zone": { "name": "{tracked_entity} Distance" },
|
||||
"nearest": { "name": "Nearest device" },
|
||||
"nearest_dir_of_travel": {
|
||||
"name": "Nearest direction of travel",
|
||||
"state": {
|
||||
"arrived": "Arrived",
|
||||
"away_from": "Away from",
|
||||
"stationary": "Stationary",
|
||||
"towards": "Towards"
|
||||
}
|
||||
},
|
||||
"nearest_dist_to_zone": { "name": "Nearest distance" }
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
]),
|
||||
}),
|
||||
'proximity': dict({
|
||||
'dir_of_travel': 'unknown',
|
||||
'dist_to_zone': 2219,
|
||||
'dir_of_travel': None,
|
||||
'dist_to_zone': 2218752,
|
||||
'nearest': 'test1',
|
||||
}),
|
||||
'tracked_states': dict({
|
||||
|
|
|
@ -907,6 +907,132 @@ async def test_device_tracker_test1_nearest_after_test2_in_ignored_zone(
|
|||
assert state.state == "away_from"
|
||||
|
||||
|
||||
async def test_nearest_sensors(hass: HomeAssistant, config_zones) -> None:
|
||||
"""Test for nearest sensors."""
|
||||
mock_config = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="home",
|
||||
data={
|
||||
CONF_ZONE: "zone.home",
|
||||
CONF_TRACKED_ENTITIES: ["device_tracker.test1", "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()
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20, "longitude": 10},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"not_home",
|
||||
{"friendly_name": "test2", "latitude": 40, "longitude": 20},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 15, "longitude": 8},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"not_home",
|
||||
{"friendly_name": "test2", "latitude": 45, "longitude": 22},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# sensor entities
|
||||
state = hass.states.get("sensor.home_nearest_device")
|
||||
assert state.state == "test1"
|
||||
state = hass.states.get("sensor.home_nearest_distance")
|
||||
assert state.state == "1615590"
|
||||
state = hass.states.get("sensor.home_test1_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
state = hass.states.get("sensor.home_test1_distance")
|
||||
assert state.state == "1615590"
|
||||
state = hass.states.get("sensor.home_test1_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
state = hass.states.get("sensor.home_test2_distance")
|
||||
assert state.state == "5176058"
|
||||
state = hass.states.get("sensor.home_test2_direction_of_travel")
|
||||
assert state.state == "away_from"
|
||||
|
||||
# move the far tracker
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2",
|
||||
"not_home",
|
||||
{"friendly_name": "test2", "latitude": 40, "longitude": 20},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.home_nearest_device")
|
||||
assert state.state == "test1"
|
||||
state = hass.states.get("sensor.home_nearest_distance")
|
||||
assert state.state == "1615590"
|
||||
state = hass.states.get("sensor.home_nearest_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
state = hass.states.get("sensor.home_test1_distance")
|
||||
assert state.state == "1615590"
|
||||
state = hass.states.get("sensor.home_test1_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
state = hass.states.get("sensor.home_test2_distance")
|
||||
assert state.state == "4611404"
|
||||
state = hass.states.get("sensor.home_test2_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
|
||||
# move the near tracker
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1",
|
||||
"not_home",
|
||||
{"friendly_name": "test1", "latitude": 20, "longitude": 10},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.home_nearest_device")
|
||||
assert state.state == "test1"
|
||||
state = hass.states.get("sensor.home_nearest_distance")
|
||||
assert state.state == "2204122"
|
||||
state = hass.states.get("sensor.home_nearest_direction_of_travel")
|
||||
assert state.state == "away_from"
|
||||
state = hass.states.get("sensor.home_test1_distance")
|
||||
assert state.state == "2204122"
|
||||
state = hass.states.get("sensor.home_test1_direction_of_travel")
|
||||
assert state.state == "away_from"
|
||||
state = hass.states.get("sensor.home_test2_distance")
|
||||
assert state.state == "4611404"
|
||||
state = hass.states.get("sensor.home_test2_direction_of_travel")
|
||||
assert state.state == "towards"
|
||||
|
||||
# get unknown distance and direction
|
||||
hass.states.async_set(
|
||||
"device_tracker.test1", "not_home", {"friendly_name": "test1"}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"device_tracker.test2", "not_home", {"friendly_name": "test2"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.home_nearest_device")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_nearest_distance")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_nearest_direction_of_travel")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_test1_distance")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_test1_direction_of_travel")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_test2_distance")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
state = hass.states.get("sensor.home_test2_direction_of_travel")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_create_deprecated_proximity_issue(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
|
|
Loading…
Reference in New Issue