Skip to content

Commit

Permalink
Add zone related sensors in proximity (home-assistant#109630)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mib1185 authored Feb 4, 2024
1 parent a95a51d commit ffe9f08
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 31 deletions.
19 changes: 13 additions & 6 deletions homeassistant/components/proximity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import logging
from typing import cast

import voluptuous as vol

Expand All @@ -11,6 +12,7 @@
CONF_NAME,
CONF_UNIT_OF_MEASUREMENT,
CONF_ZONE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
Expand Down Expand Up @@ -203,16 +205,21 @@ def __init__(
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]),
}
2 changes: 2 additions & 0 deletions homeassistant/components/proximity/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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"
Expand Down
19 changes: 9 additions & 10 deletions homeassistant/components/proximity/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -130,7 +131,7 @@ async def async_check_tracked_entity_change(
},
)

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
Expand Down Expand Up @@ -303,7 +304,7 @@ async def _async_update_data(self) -> 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,
Expand All @@ -318,28 +319,26 @@ async def _async_update_data(self) -> 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:
Expand Down
39 changes: 28 additions & 11 deletions homeassistant/components/proximity/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,53 @@
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,
),
]


Expand Down Expand Up @@ -151,8 +166,10 @@ def __init__(
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."""
Expand Down
15 changes: 13 additions & 2 deletions homeassistant/components/proximity/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,26 @@
"entity": {
"sensor": {
"dir_of_travel": {
"name": "Direction of travel",
"name": "{tracked_entity} Direction of travel",
"state": {
"arrived": "Arrived",
"away_from": "Away from",
"stationary": "Stationary",
"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": {
Expand Down
4 changes: 2 additions & 2 deletions tests/components/proximity/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
126 changes: 126 additions & 0 deletions tests/components/proximity/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit ffe9f08

Please sign in to comment.