Person: Use the home zone lat/lon coordinates when detected home by a stationary tracker (#134075)

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Sören Beye
2025-08-27 10:57:25 +02:00
committed by GitHub
parent 43a1a679f9
commit e894a03c43
3 changed files with 77 additions and 32 deletions

View File

@@ -15,6 +15,7 @@ from homeassistant.components.device_tracker import (
DOMAIN as DEVICE_TRACKER_DOMAIN, DOMAIN as DEVICE_TRACKER_DOMAIN,
SourceType, SourceType,
) )
from homeassistant.components.zone import ENTITY_ID_HOME
from homeassistant.const import ( from homeassistant.const import (
ATTR_EDITABLE, ATTR_EDITABLE,
ATTR_GPS_ACCURACY, ATTR_GPS_ACCURACY,
@@ -464,7 +465,7 @@ class Person(
"""Register device trackers.""" """Register device trackers."""
await super().async_added_to_hass() await super().async_added_to_hass()
if state := await self.async_get_last_state(): if state := await self.async_get_last_state():
self._parse_source_state(state) self._parse_source_state(state, state)
if self.hass.is_running: if self.hass.is_running:
# Update person now if hass is already running. # Update person now if hass is already running.
@@ -514,7 +515,7 @@ class Person(
@callback @callback
def _update_state(self) -> None: def _update_state(self) -> None:
"""Update the state.""" """Update the state."""
latest_non_gps_home = latest_not_home = latest_gps = latest = None latest_non_gps_home = latest_not_home = latest_gps = latest = coordinates = None
for entity_id in self._config[CONF_DEVICE_TRACKERS]: for entity_id in self._config[CONF_DEVICE_TRACKERS]:
state = self.hass.states.get(entity_id) state = self.hass.states.get(entity_id)
@@ -530,13 +531,23 @@ class Person(
if latest_non_gps_home: if latest_non_gps_home:
latest = latest_non_gps_home latest = latest_non_gps_home
if (
latest_non_gps_home.attributes.get(ATTR_LATITUDE) is None
and latest_non_gps_home.attributes.get(ATTR_LONGITUDE) is None
and (home_zone := self.hass.states.get(ENTITY_ID_HOME))
):
coordinates = home_zone
else:
coordinates = latest_non_gps_home
elif latest_gps: elif latest_gps:
latest = latest_gps latest = latest_gps
coordinates = latest_gps
else: else:
latest = latest_not_home latest = latest_not_home
coordinates = latest_not_home
if latest: if latest and coordinates:
self._parse_source_state(latest) self._parse_source_state(latest, coordinates)
else: else:
self._attr_state = None self._attr_state = None
self._source = None self._source = None
@@ -548,15 +559,15 @@ class Person(
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def _parse_source_state(self, state: State) -> None: def _parse_source_state(self, state: State, coordinates: State) -> None:
"""Parse source state and set person attributes. """Parse source state and set person attributes.
This is a device tracker state or the restored person state. This is a device tracker state or the restored person state.
""" """
self._attr_state = state.state self._attr_state = state.state
self._source = state.entity_id self._source = state.entity_id
self._latitude = state.attributes.get(ATTR_LATITUDE) self._latitude = coordinates.attributes.get(ATTR_LATITUDE)
self._longitude = state.attributes.get(ATTR_LONGITUDE) self._longitude = coordinates.attributes.get(ATTR_LONGITUDE)
self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY)
@callback @callback

View File

@@ -2,7 +2,7 @@
"domain": "person", "domain": "person",
"name": "Person", "name": "Person",
"codeowners": [], "codeowners": [],
"dependencies": ["image_upload", "http"], "dependencies": ["image_upload", "http", "zone"],
"documentation": "https://www.home-assistant.io/integrations/person", "documentation": "https://www.home-assistant.io/integrations/person",
"integration_type": "system", "integration_type": "system",
"iot_class": "calculated", "iot_class": "calculated",

View File

@@ -14,7 +14,9 @@ from homeassistant.components.person import (
DOMAIN, DOMAIN,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_EDITABLE,
ATTR_ENTITY_PICTURE, ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_GPS_ACCURACY, ATTR_GPS_ACCURACY,
ATTR_ID, ATTR_ID,
ATTR_LATITUDE, ATTR_LATITUDE,
@@ -112,14 +114,19 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) ->
} }
assert await async_setup_component(hass, DOMAIN, config) assert await async_setup_component(hass, DOMAIN, config)
expected_attributes = {
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER],
ATTR_EDITABLE: False,
ATTR_FRIENDLY_NAME: "tracked person",
ATTR_ID: "1234",
ATTR_USER_ID: user_id,
}
state = hass.states.get("person.tracked_person") state = hass.states.get("person.tracked_person")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert state.attributes.get(ATTR_ID) == "1234" assert state.attributes == expected_attributes
assert state.attributes.get(ATTR_LATITUDE) is None
assert state.attributes.get(ATTR_LONGITUDE) is None
assert state.attributes.get(ATTR_SOURCE) is None
assert state.attributes.get(ATTR_USER_ID) == user_id
# Test home without coordinates
hass.states.async_set(DEVICE_TRACKER, "home") hass.states.async_set(DEVICE_TRACKER, "home")
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -131,13 +138,41 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) ->
state = hass.states.get("person.tracked_person") state = hass.states.get("person.tracked_person")
assert state.state == "home" assert state.state == "home"
assert state.attributes.get(ATTR_ID) == "1234" assert state.attributes == expected_attributes | {
assert state.attributes.get(ATTR_LATITUDE) is None ATTR_LATITUDE: 32.87336,
assert state.attributes.get(ATTR_LONGITUDE) is None ATTR_LONGITUDE: -117.22743,
assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER ATTR_SOURCE: DEVICE_TRACKER,
assert state.attributes.get(ATTR_USER_ID) == user_id }
assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [DEVICE_TRACKER]
# Test home with coordinates
hass.states.async_set(
DEVICE_TRACKER,
"home",
{ATTR_LATITUDE: 10.123456, ATTR_LONGITUDE: 11.123456, ATTR_GPS_ACCURACY: 10},
)
await hass.async_block_till_done()
state = hass.states.get("person.tracked_person")
assert state.state == "home"
assert state.attributes == expected_attributes | {
ATTR_GPS_ACCURACY: 10,
ATTR_LATITUDE: 10.123456,
ATTR_LONGITUDE: 11.123456,
ATTR_SOURCE: DEVICE_TRACKER,
}
# Test not_home without coordinates
hass.states.async_set(
DEVICE_TRACKER,
"not_home",
)
await hass.async_block_till_done()
state = hass.states.get("person.tracked_person")
assert state.state == "not_home"
assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER}
# Test not_home with coordinates
hass.states.async_set( hass.states.async_set(
DEVICE_TRACKER, DEVICE_TRACKER,
"not_home", "not_home",
@@ -147,13 +182,12 @@ async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) ->
state = hass.states.get("person.tracked_person") state = hass.states.get("person.tracked_person")
assert state.state == "not_home" assert state.state == "not_home"
assert state.attributes.get(ATTR_ID) == "1234" assert state.attributes == expected_attributes | {
assert state.attributes.get(ATTR_LATITUDE) == 10.123456 ATTR_GPS_ACCURACY: 10,
assert state.attributes.get(ATTR_LONGITUDE) == 11.123456 ATTR_LATITUDE: 10.123456,
assert state.attributes.get(ATTR_GPS_ACCURACY) == 10 ATTR_LONGITUDE: 11.123456,
assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER ATTR_SOURCE: DEVICE_TRACKER,
assert state.attributes.get(ATTR_USER_ID) == user_id }
assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [DEVICE_TRACKER]
async def test_setup_two_trackers( async def test_setup_two_trackers(
@@ -188,8 +222,8 @@ async def test_setup_two_trackers(
state = hass.states.get("person.tracked_person") state = hass.states.get("person.tracked_person")
assert state.state == "home" assert state.state == "home"
assert state.attributes.get(ATTR_ID) == "1234" assert state.attributes.get(ATTR_ID) == "1234"
assert state.attributes.get(ATTR_LATITUDE) is None assert state.attributes.get(ATTR_LATITUDE) == 32.87336
assert state.attributes.get(ATTR_LONGITUDE) is None assert state.attributes.get(ATTR_LONGITUDE) == -117.22743
assert state.attributes.get(ATTR_GPS_ACCURACY) is None assert state.attributes.get(ATTR_GPS_ACCURACY) is None
assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER
assert state.attributes.get(ATTR_USER_ID) == user_id assert state.attributes.get(ATTR_USER_ID) == user_id
@@ -453,8 +487,8 @@ async def test_load_person_storage(
state = hass.states.get("person.tracked_person") state = hass.states.get("person.tracked_person")
assert state.state == "home" assert state.state == "home"
assert state.attributes.get(ATTR_ID) == "1234" assert state.attributes.get(ATTR_ID) == "1234"
assert state.attributes.get(ATTR_LATITUDE) is None assert state.attributes.get(ATTR_LATITUDE) == 32.87336
assert state.attributes.get(ATTR_LONGITUDE) is None assert state.attributes.get(ATTR_LONGITUDE) == -117.22743
assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER
assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id
@@ -817,7 +851,7 @@ async def test_reload(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
}, },
) )
assert len(hass.states.async_entity_ids()) == 2 assert len(hass.states.async_entity_ids()) == 3 # Person1, Person2, zone.home
state_1 = hass.states.get("person.person_1") state_1 = hass.states.get("person.person_1")
state_2 = hass.states.get("person.person_2") state_2 = hass.states.get("person.person_2")
@@ -847,7 +881,7 @@ async def test_reload(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 2 assert len(hass.states.async_entity_ids()) == 3 # Person1, Person2, zone.home
state_1 = hass.states.get("person.person_1") state_1 = hass.states.get("person.person_1")
state_2 = hass.states.get("person.person_2") state_2 = hass.states.get("person.person_2")