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_entity
pull/109664/head
Michael 2024-02-05 00:42:07 +01:00 committed by GitHub
parent a95a51da05
commit ffe9f0825a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 193 additions and 31 deletions

View File

@ -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]),
}

View File

@ -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"

View File

@ -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:

View File

@ -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."""

View File

@ -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": {

View File

@ -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({

View File

@ -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,