Compare commits

...

1 Commits

Author SHA1 Message Date
Erik f19a392d71 Add property in_zones to TrackerEntity 2026-05-21 21:02:51 +02:00
2 changed files with 133 additions and 11 deletions
@@ -207,6 +207,7 @@ class TrackerEntityDescription(EntityDescription, frozen_or_thawed=True):
CACHED_TRACKER_PROPERTIES_WITH_ATTR_ = {
"in_zones",
"latitude",
"location_accuracy",
"location_name",
@@ -220,6 +221,7 @@ class TrackerEntity(
"""Base class for a tracked device."""
entity_description: TrackerEntityDescription
_attr_in_zones: list[str] | None = None
_attr_latitude: float | None = None
_attr_location_accuracy: float = 0
_attr_location_name: str | None = None
@@ -239,6 +241,14 @@ class TrackerEntity(
"""All updates need to be written to the state machine if we're not polling."""
return not self.should_poll
@cached_property
def in_zones(self) -> list[str] | None:
"""Return the entity_id of zones the device is currently in.
Ignored if latitude and longitude are both set.
"""
return self._attr_in_zones
@cached_property
def location_accuracy(self) -> float:
"""Return the location accuracy of the device.
@@ -270,8 +280,9 @@ class TrackerEntity(
self.hass, self.latitude, self.longitude, self.location_accuracy
)
else:
self.__active_zone = None
self.__in_zones = None
zones = self.in_zones
self.__active_zone = None if not zones else self.hass.states.get(zones[0])
self.__in_zones = zones
super()._async_write_ha_state()
@property
@@ -280,7 +291,9 @@ class TrackerEntity(
if self.location_name is not None:
return self.location_name
if self.latitude is not None and self.longitude is not None:
if (
self.latitude is not None and self.longitude is not None
) or self.__in_zones is not None:
zone_state = self.__active_zone
if zone_state is None:
state = STATE_NOT_HOME
@@ -296,11 +309,10 @@ class TrackerEntity(
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
attr: dict[str, Any] = {ATTR_IN_ZONES: self.__in_zones or []}
attr.update(super().state_attributes)
if self.latitude is not None and self.longitude is not None:
attr[ATTR_IN_ZONES] = self.__in_zones or []
attr[ATTR_LATITUDE] = self.latitude
attr[ATTR_LONGITUDE] = self.longitude
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
@@ -146,6 +146,7 @@ class MockTrackerEntity(TrackerEntity):
def __init__(
self,
battery_level: int | None = None,
in_zones: list[str] | None = None,
location_name: str | None = None,
latitude: float | None = None,
longitude: float | None = None,
@@ -153,6 +154,7 @@ class MockTrackerEntity(TrackerEntity):
) -> None:
"""Initialize entity."""
self._battery_level = battery_level
self._in_zones = in_zones
self._location_name = location_name
self._latitude = latitude
self._longitude = longitude
@@ -171,6 +173,11 @@ class MockTrackerEntity(TrackerEntity):
"""Return the source type, eg gps or router, of the device."""
return SourceType.GPS
@property
def in_zones(self) -> list[str] | None:
"""Return the entity_id of zones the device is currently in."""
return self._in_zones
@property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
@@ -198,6 +205,12 @@ def battery_level_fixture() -> int | None:
return None
@pytest.fixture(name="in_zones")
def in_zones_fixture() -> list[str] | None:
"""Return the in_zones value of the entity for the test."""
return None
@pytest.fixture(name="location_name")
def location_name_fixture() -> str | None:
"""Return the location_name of the entity for the test."""
@@ -226,6 +239,7 @@ def accuracy_fixture() -> float:
def tracker_entity_fixture(
entity_id: str,
battery_level: int | None,
in_zones: list[str] | None,
location_name: str | None,
latitude: float | None,
longitude: float | None,
@@ -234,6 +248,7 @@ def tracker_entity_fixture(
"""Create a test tracker entity."""
entity = MockTrackerEntity(
battery_level=battery_level,
in_zones=in_zones,
location_name=location_name,
latitude=latitude,
longitude=longitude,
@@ -464,6 +479,7 @@ async def test_load_unload_entry_tracker(
@pytest.mark.parametrize(
(
"battery_level",
"in_zones",
"location_name",
"latitude",
"longitude",
@@ -471,7 +487,8 @@ async def test_load_unload_entry_tracker(
"expected_attributes",
),
[
(
pytest.param(
None,
None,
None,
1.0,
@@ -484,8 +501,10 @@ async def test_load_unload_entry_tracker(
ATTR_LATITUDE: 1.0,
ATTR_LONGITUDE: 2.0,
},
id="lat_long_no_zone",
),
(
pytest.param(
None,
None,
None,
50.0,
@@ -498,8 +517,10 @@ async def test_load_unload_entry_tracker(
ATTR_LATITUDE: 50.0,
ATTR_LONGITUDE: 60.0,
},
id="lat_long_home",
),
(
pytest.param(
None,
None,
None,
-50.0,
@@ -512,8 +533,10 @@ async def test_load_unload_entry_tracker(
ATTR_LATITUDE: -50.0,
ATTR_LONGITUDE: -60.0,
},
id="lat_long_other_zone",
),
(
pytest.param(
None,
None,
"zen_zone",
None,
@@ -523,8 +546,10 @@ async def test_load_unload_entry_tracker(
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: [],
},
id="location_name",
),
(
pytest.param(
None,
None,
None,
None,
@@ -534,18 +559,102 @@ async def test_load_unload_entry_tracker(
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: [],
},
id="no_location",
),
(
pytest.param(
100,
None,
None,
None,
None,
STATE_UNKNOWN,
{
ATTR_BATTERY_LEVEL: 100,
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: [],
},
id="battery_only",
),
pytest.param(
None,
["zone.home"],
None,
None,
None,
STATE_HOME,
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: ["zone.home"],
},
id="in_zones_home",
),
pytest.param(
None,
["zone.other_zone"],
None,
None,
None,
"other zone",
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: ["zone.other_zone"],
},
id="in_zones_other_zone",
),
pytest.param(
None,
["zone.other_zone", "zone.other_zone_larger"],
None,
None,
None,
"other zone",
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: ["zone.other_zone", "zone.other_zone_larger"],
},
id="in_zones_multiple",
),
pytest.param(
None,
[],
None,
None,
None,
STATE_NOT_HOME,
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: [],
},
id="in_zones_empty",
),
pytest.param(
None,
["zone.home"],
None,
1.0,
2.0,
STATE_NOT_HOME,
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_GPS_ACCURACY: 0,
ATTR_IN_ZONES: [],
ATTR_LATITUDE: 1.0,
ATTR_LONGITUDE: 2.0,
},
id="in_zones_ignored_when_lat_long_set",
),
pytest.param(
None,
["zone.home"],
"zen_zone",
None,
None,
"zen_zone",
{
ATTR_SOURCE_TYPE: SourceType.GPS,
ATTR_IN_ZONES: ["zone.home"],
},
id="location_name_wins_over_in_zones",
),
],
)
@@ -674,6 +783,7 @@ def test_tracker_entity() -> None:
"""Test coverage for base TrackerEntity class."""
entity = TrackerEntity()
assert entity.source_type is SourceType.GPS
assert entity.in_zones is None
assert entity.latitude is None
assert entity.longitude is None
assert entity.location_name is None