mirror of
https://github.com/home-assistant/core.git
synced 2026-07-02 04:06:41 +01:00
1522 lines
47 KiB
Python
1522 lines
47 KiB
Python
"""The tests for the person component."""
|
|
|
|
from datetime import timedelta
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
|
|
from homeassistant.components import person
|
|
from homeassistant.components.device_tracker import (
|
|
ATTR_IN_ZONES,
|
|
ATTR_SOURCE_TYPE,
|
|
ATTR_TRACKING_TYPE,
|
|
SourceType,
|
|
TrackingType,
|
|
)
|
|
from homeassistant.components.person import (
|
|
ATTR_DEVICE_TRACKERS,
|
|
ATTR_SOURCE,
|
|
ATTR_USER_ID,
|
|
DOMAIN,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_EDITABLE,
|
|
ATTR_ENTITY_PICTURE,
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_GPS_ACCURACY,
|
|
ATTR_ID,
|
|
ATTR_LATITUDE,
|
|
ATTR_LONGITUDE,
|
|
EVENT_HOMEASSISTANT_START,
|
|
SERVICE_RELOAD,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.core import Context, CoreState, HomeAssistant, State
|
|
from homeassistant.helpers import entity_registry as er
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from .conftest import DEVICE_TRACKER, DEVICE_TRACKER_2
|
|
|
|
from tests.common import MockUser, mock_component, mock_restore_cache
|
|
from tests.typing import WebSocketGenerator
|
|
|
|
|
|
async def test_minimal_setup(hass: HomeAssistant) -> None:
|
|
"""Test minimal config with only name."""
|
|
config = {DOMAIN: {"id": "1234", "name": "test person"}}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
state = hass.states.get("person.test_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "test person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
}
|
|
|
|
|
|
async def test_setup_no_id(hass: HomeAssistant) -> None:
|
|
"""Test config with no id."""
|
|
config = {DOMAIN: {"name": "test user"}}
|
|
assert not await async_setup_component(hass, DOMAIN, config)
|
|
|
|
|
|
async def test_setup_no_name(hass: HomeAssistant) -> None:
|
|
"""Test config with no name."""
|
|
config = {DOMAIN: {"id": "1234"}}
|
|
assert not await async_setup_component(hass, DOMAIN, config)
|
|
|
|
|
|
async def test_setup_user_id(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
|
|
"""Test config with user id."""
|
|
user_id = hass_admin_user.id
|
|
config = {DOMAIN: {"id": "1234", "name": "test person", "user_id": user_id}}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
state = hass.states.get("person.test_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "test person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
|
|
async def test_valid_invalid_user_ids(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test a person with valid user id and a person with invalid user id ."""
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: [
|
|
{"id": "1234", "name": "test valid user", "user_id": user_id},
|
|
{"id": "5678", "name": "test bad user", "user_id": "bad_user_id"},
|
|
]
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
state = hass.states.get("person.test_valid_user")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "test valid user",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
state = hass.states.get("person.test_bad_user")
|
|
assert state is None
|
|
|
|
|
|
async def test_setup_tracker(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
|
|
"""Test set up person with one device tracker."""
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": DEVICE_TRACKER,
|
|
}
|
|
}
|
|
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_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
# Test home without coordinates
|
|
hass.states.async_set(DEVICE_TRACKER, "home")
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
|
|
# A legacy tracker reporting home (no coordinates) is placed at the home zone.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: 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(
|
|
DEVICE_TRACKER,
|
|
"not_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 == "not_home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_GPS_ACCURACY: 10,
|
|
ATTR_LATITUDE: 10.123456,
|
|
ATTR_LONGITUDE: 11.123456,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
}
|
|
|
|
|
|
async def test_setup_two_trackers(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test set up person with two device trackers."""
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
expected_attributes = {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(
|
|
DEVICE_TRACKER,
|
|
"home",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.ROUTER,
|
|
ATTR_IN_ZONES: ["zone.home"],
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_IN_ZONES: ["zone.home"],
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
}
|
|
|
|
hass.states.async_set(
|
|
DEVICE_TRACKER_2,
|
|
"not_home",
|
|
{
|
|
ATTR_LATITUDE: 12.123456,
|
|
ATTR_LONGITUDE: 13.123456,
|
|
ATTR_GPS_ACCURACY: 12,
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
ATTR_SOURCE_TYPE: SourceType.GPS,
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(
|
|
DEVICE_TRACKER, "not_home", {ATTR_SOURCE_TYPE: SourceType.ROUTER}
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "not_home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_GPS_ACCURACY: 12,
|
|
ATTR_LATITUDE: 12.123456,
|
|
ATTR_LONGITUDE: 13.123456,
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
}
|
|
|
|
hass.states.async_set(DEVICE_TRACKER_2, "zone1", {ATTR_SOURCE_TYPE: SourceType.GPS})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "zone1"
|
|
assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER_2}
|
|
|
|
hass.states.async_set(DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SourceType.ROUTER})
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(DEVICE_TRACKER_2, "zone2", {ATTR_SOURCE_TYPE: SourceType.GPS})
|
|
await hass.async_block_till_done()
|
|
|
|
# Legacy router reporting home (no in_zones) is placed at the home zone.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
}
|
|
|
|
|
|
async def test_setup_router_ble_trackers(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test router and BLE trackers."""
|
|
# BLE trackers are considered stationary trackers; however unlike
|
|
# a router based tracker whose states are home and not_home, a BLE
|
|
# tracker may have the value of any zone that the beacon is
|
|
# configured for.
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
expected_attributes = {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(
|
|
DEVICE_TRACKER, "not_home", {ATTR_SOURCE_TYPE: SourceType.ROUTER}
|
|
)
|
|
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}
|
|
|
|
# Set the BLE tracker to the "office" zone.
|
|
hass.states.async_set(
|
|
DEVICE_TRACKER_2,
|
|
"office",
|
|
{
|
|
ATTR_LATITUDE: 12.123456,
|
|
ATTR_LONGITUDE: 13.123456,
|
|
ATTR_GPS_ACCURACY: 12,
|
|
ATTR_IN_ZONES: ["zone.office"],
|
|
ATTR_SOURCE_TYPE: SourceType.BLUETOOTH_LE,
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
# The person should be in the office.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "office"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_GPS_ACCURACY: 12,
|
|
ATTR_LATITUDE: 12.123456,
|
|
ATTR_LONGITUDE: 13.123456,
|
|
ATTR_IN_ZONES: ["zone.office"],
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
}
|
|
|
|
|
|
# Representative device tracker states for the priority buckets used by
|
|
# `Person._update_state`, in priority order:
|
|
# 1. a scanner reporting a non-empty in_zones, i.e. connected in a known
|
|
# zone (highest priority)
|
|
# 2. a tracker reporting "home"
|
|
# 3. any GPS tracker, regardless of its state
|
|
# 4. everything else, e.g. a tracker reporting "not_home" (lowest priority)
|
|
# Each value is a (state, attributes) tuple passed to `hass.states.async_set`.
|
|
_ROUTER_HOME: tuple[str, dict[str, Any]] = (
|
|
"home",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.ROUTER,
|
|
ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
|
|
ATTR_IN_ZONES: ["zone.home"],
|
|
},
|
|
)
|
|
_ROUTER_NOT_HOME: tuple[str, dict[str, Any]] = (
|
|
"not_home",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.ROUTER,
|
|
ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
|
|
ATTR_IN_ZONES: [],
|
|
},
|
|
)
|
|
# A scanner tracker associated with a non-home zone reports the zone's name as
|
|
# its state and lists the zone in `in_zones` (see device_tracker PR #172157).
|
|
_SCANNER_OFFICE: tuple[str, dict[str, Any]] = (
|
|
"office",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.ROUTER,
|
|
ATTR_TRACKING_TYPE: TrackingType.CONNECTION,
|
|
ATTR_IN_ZONES: ["zone.office"],
|
|
},
|
|
)
|
|
_GPS_NOT_HOME: tuple[str, dict[str, Any]] = (
|
|
"not_home",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.GPS,
|
|
ATTR_TRACKING_TYPE: TrackingType.POSITION,
|
|
ATTR_LATITUDE: 1.0,
|
|
ATTR_LONGITUDE: 2.0,
|
|
ATTR_GPS_ACCURACY: 5,
|
|
ATTR_IN_ZONES: [],
|
|
},
|
|
)
|
|
_GPS_WORK: tuple[str, dict[str, Any]] = (
|
|
"work",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.GPS,
|
|
ATTR_TRACKING_TYPE: TrackingType.POSITION,
|
|
ATTR_LATITUDE: 3.0,
|
|
ATTR_LONGITUDE: 4.0,
|
|
ATTR_GPS_ACCURACY: 7,
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
},
|
|
)
|
|
# Legacy trackers come from integrations that predate `in_zones`, so they report
|
|
# only a state and a source type (no `in_zones`).
|
|
_LEGACY_HOME: tuple[str, dict[str, Any]] = (
|
|
"home",
|
|
{ATTR_SOURCE_TYPE: SourceType.ROUTER},
|
|
)
|
|
_LEGACY_NOT_HOME: tuple[str, dict[str, Any]] = (
|
|
"not_home",
|
|
{ATTR_SOURCE_TYPE: SourceType.ROUTER},
|
|
)
|
|
# A legacy tracker in a non-home zone reports the zone name, still no `in_zones`.
|
|
_LEGACY_OFFICE: tuple[str, dict[str, Any]] = (
|
|
"office",
|
|
{ATTR_SOURCE_TYPE: SourceType.ROUTER},
|
|
)
|
|
# Legacy GPS trackers report coordinates but no `in_zones`.
|
|
_LEGACY_GPS_NOT_HOME: tuple[str, dict[str, Any]] = (
|
|
"not_home",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.GPS,
|
|
ATTR_LATITUDE: 5.0,
|
|
ATTR_LONGITUDE: 6.0,
|
|
ATTR_GPS_ACCURACY: 8,
|
|
},
|
|
)
|
|
_LEGACY_GPS_WORK: tuple[str, dict[str, Any]] = (
|
|
"work",
|
|
{
|
|
ATTR_SOURCE_TYPE: SourceType.GPS,
|
|
ATTR_LATITUDE: 7.0,
|
|
ATTR_LONGITUDE: 8.0,
|
|
ATTR_GPS_ACCURACY: 9,
|
|
},
|
|
)
|
|
|
|
|
|
async def _async_setup_person_two_trackers(hass: HomeAssistant, user_id: str) -> None:
|
|
"""Set up a person tracked by two device trackers, with hass running."""
|
|
hass.set_state(CoreState.not_running)
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("high_priority", "low_priority", "expected_state", "expected_extra"),
|
|
[
|
|
# A scanner reporting a zone outranks a GPS tracker. It has no
|
|
# coordinates.
|
|
pytest.param(
|
|
_ROUTER_HOME,
|
|
_GPS_NOT_HOME,
|
|
"home",
|
|
{
|
|
ATTR_IN_ZONES: ["zone.home"],
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="scanner_beats_gps",
|
|
),
|
|
# A legacy "home" tracker (no in_zones) likewise outranks GPS; it is
|
|
# placed at the home zone.
|
|
pytest.param(
|
|
_LEGACY_HOME,
|
|
_GPS_NOT_HOME,
|
|
"home",
|
|
{
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="legacy_home_beats_gps",
|
|
),
|
|
# A GPS tracker outranks a "not_home" tracker.
|
|
pytest.param(
|
|
_GPS_WORK,
|
|
_ROUTER_NOT_HOME,
|
|
"work",
|
|
{
|
|
ATTR_GPS_ACCURACY: 7,
|
|
ATTR_LATITUDE: 3.0,
|
|
ATTR_LONGITUDE: 4.0,
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="gps_beats_not_home",
|
|
),
|
|
],
|
|
)
|
|
async def test_state_priority_overrides_recency(
|
|
hass: HomeAssistant,
|
|
hass_admin_user: MockUser,
|
|
freezer: FrozenDateTimeFactory,
|
|
high_priority: tuple[str, dict[str, Any]],
|
|
low_priority: tuple[str, dict[str, Any]],
|
|
expected_state: str,
|
|
expected_extra: dict[str, Any],
|
|
) -> None:
|
|
"""Test the higher-priority bucket wins even when its state is stale.
|
|
|
|
There is no time-based expiry: a long-stale state from a higher-priority
|
|
bucket still wins over a fresh state from a lower-priority bucket.
|
|
"""
|
|
await _async_setup_person_two_trackers(hass, hass_admin_user.id)
|
|
|
|
# The higher-priority tracker reports first and then goes stale.
|
|
hass.states.async_set(DEVICE_TRACKER, high_priority[0], high_priority[1])
|
|
await hass.async_block_till_done()
|
|
freezer.tick(timedelta(hours=2))
|
|
# The lower-priority tracker reports a much more recent update.
|
|
hass.states.async_set(DEVICE_TRACKER_2, low_priority[0], low_priority[1])
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == expected_state
|
|
assert (
|
|
state.attributes
|
|
== {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: hass_admin_user.id,
|
|
}
|
|
| expected_extra
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("older", "newer", "expected_state", "expected_extra"),
|
|
[
|
|
# GPS bucket: the most recent GPS state wins.
|
|
pytest.param(
|
|
_GPS_WORK,
|
|
_GPS_NOT_HOME,
|
|
"not_home",
|
|
{
|
|
ATTR_GPS_ACCURACY: 5,
|
|
ATTR_LATITUDE: 1.0,
|
|
ATTR_LONGITUDE: 2.0,
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
},
|
|
id="gps_newer_not_home",
|
|
),
|
|
pytest.param(
|
|
_GPS_NOT_HOME,
|
|
_GPS_WORK,
|
|
"work",
|
|
{
|
|
ATTR_GPS_ACCURACY: 7,
|
|
ATTR_LATITUDE: 3.0,
|
|
ATTR_LONGITUDE: 4.0,
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
},
|
|
id="gps_newer_work",
|
|
),
|
|
# Highest-priority bucket: the most recent scanner in a zone wins
|
|
# (here a fresh "office" scanner over a stale "home" one).
|
|
pytest.param(
|
|
_ROUTER_HOME,
|
|
_SCANNER_OFFICE,
|
|
"office",
|
|
{ATTR_IN_ZONES: ["zone.office"], ATTR_SOURCE: DEVICE_TRACKER_2},
|
|
id="scanner_newer_office",
|
|
),
|
|
# Lowest-priority bucket: the most recent "not_home" tracker wins.
|
|
pytest.param(
|
|
_ROUTER_NOT_HOME,
|
|
_LEGACY_NOT_HOME,
|
|
"not_home",
|
|
{ATTR_SOURCE: DEVICE_TRACKER_2},
|
|
id="not_home_newer",
|
|
),
|
|
# A pair of legacy "home" trackers (no in_zones) likewise picks the
|
|
# most recent; it is placed at the home zone.
|
|
pytest.param(
|
|
_LEGACY_HOME,
|
|
_LEGACY_HOME,
|
|
"home",
|
|
{
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
},
|
|
id="legacy_home_newer",
|
|
),
|
|
],
|
|
)
|
|
async def test_most_recent_state_in_bucket_wins(
|
|
hass: HomeAssistant,
|
|
hass_admin_user: MockUser,
|
|
freezer: FrozenDateTimeFactory,
|
|
older: tuple[str, dict[str, Any]],
|
|
newer: tuple[str, dict[str, Any]],
|
|
expected_state: str,
|
|
expected_extra: dict[str, Any],
|
|
) -> None:
|
|
"""Test that within a bucket the most recently updated state is picked."""
|
|
await _async_setup_person_two_trackers(hass, hass_admin_user.id)
|
|
|
|
hass.states.async_set(DEVICE_TRACKER, older[0], older[1])
|
|
await hass.async_block_till_done()
|
|
freezer.tick(timedelta(minutes=5))
|
|
hass.states.async_set(DEVICE_TRACKER_2, newer[0], newer[1])
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == expected_state
|
|
# The newer tracker is the source.
|
|
assert (
|
|
state.attributes
|
|
== {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: hass_admin_user.id,
|
|
}
|
|
| expected_extra
|
|
)
|
|
|
|
|
|
async def test_scanner_associated_with_other_zone(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test a person tracked by a scanner associated with a non-home zone.
|
|
|
|
A connected scanner associated with a non-home zone reports the zone name
|
|
and lists the zone in `in_zones`. Being a non-GPS tracker, it provides no
|
|
coordinates of its own.
|
|
"""
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": DEVICE_TRACKER,
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
|
|
hass.states.async_set(DEVICE_TRACKER, _SCANNER_OFFICE[0], _SCANNER_OFFICE[1])
|
|
await hass.async_block_till_done()
|
|
|
|
# No coordinates: a scanner tracker provides none of its own.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "office"
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: ["zone.office"],
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("tracker", "expected_state", "expected_extra"),
|
|
[
|
|
# A legacy "home" tracker has no coordinates of its own, so it is
|
|
# placed at the home zone.
|
|
pytest.param(
|
|
_LEGACY_HOME,
|
|
"home",
|
|
{
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="home",
|
|
),
|
|
# A legacy "not_home" tracker contributes no coordinates and no zones.
|
|
pytest.param(
|
|
_LEGACY_NOT_HOME,
|
|
"not_home",
|
|
{ATTR_SOURCE: DEVICE_TRACKER},
|
|
id="not_home",
|
|
),
|
|
# A legacy tracker in a non-home zone gets no coordinate fallback.
|
|
pytest.param(
|
|
_LEGACY_OFFICE,
|
|
"office",
|
|
{ATTR_SOURCE: DEVICE_TRACKER},
|
|
id="office",
|
|
),
|
|
# Legacy GPS trackers contribute their own coordinates but no zones.
|
|
pytest.param(
|
|
_LEGACY_GPS_NOT_HOME,
|
|
"not_home",
|
|
{
|
|
ATTR_GPS_ACCURACY: 8,
|
|
ATTR_LATITUDE: 5.0,
|
|
ATTR_LONGITUDE: 6.0,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="gps_not_home",
|
|
),
|
|
pytest.param(
|
|
_LEGACY_GPS_WORK,
|
|
"work",
|
|
{
|
|
ATTR_GPS_ACCURACY: 9,
|
|
ATTR_LATITUDE: 7.0,
|
|
ATTR_LONGITUDE: 8.0,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
},
|
|
id="gps_work",
|
|
),
|
|
],
|
|
)
|
|
async def test_legacy_device_tracker(
|
|
hass: HomeAssistant,
|
|
hass_admin_user: MockUser,
|
|
tracker: tuple[str, dict[str, Any]],
|
|
expected_state: str,
|
|
expected_extra: dict[str, Any],
|
|
) -> None:
|
|
"""Test a legacy tracker that reports a state but no in_zones."""
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": DEVICE_TRACKER,
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
|
|
hass.states.async_set(DEVICE_TRACKER, tracker[0], tracker[1])
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == expected_state
|
|
assert (
|
|
state.attributes
|
|
== {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
| expected_extra
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("competitor"),
|
|
[
|
|
# A fresh GPS tracker reporting coordinates...
|
|
pytest.param(_GPS_WORK, id="vs_gps"),
|
|
# ...and a fresh legacy "home" tracker both lose to the scanner that
|
|
# reports being in a zone.
|
|
pytest.param(_LEGACY_HOME, id="vs_legacy_home"),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("scanner", "expected_state", "expected_in_zones"),
|
|
[
|
|
pytest.param(_ROUTER_HOME, "home", ["zone.home"], id="home"),
|
|
pytest.param(_SCANNER_OFFICE, "office", ["zone.office"], id="office"),
|
|
],
|
|
)
|
|
async def test_scanner_in_zone_has_highest_priority(
|
|
hass: HomeAssistant,
|
|
hass_admin_user: MockUser,
|
|
freezer: FrozenDateTimeFactory,
|
|
scanner: tuple[str, dict[str, Any]],
|
|
expected_state: str,
|
|
expected_in_zones: list[str],
|
|
competitor: tuple[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Test a scanner in a zone wins, even when stale.
|
|
|
|
A scanner reporting a non-empty `in_zones` is the most reliable presence
|
|
signal and takes precedence over a fresher GPS or legacy "home" tracker. It
|
|
contributes no coordinates of its own.
|
|
"""
|
|
await _async_setup_person_two_trackers(hass, hass_admin_user.id)
|
|
|
|
# The scanner reports first and then goes stale.
|
|
hass.states.async_set(DEVICE_TRACKER, scanner[0], scanner[1])
|
|
await hass.async_block_till_done()
|
|
freezer.tick(timedelta(hours=2))
|
|
# A competing tracker reports a much more recent update.
|
|
hass.states.async_set(DEVICE_TRACKER_2, competitor[0], competitor[1])
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == expected_state
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: expected_in_zones,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
ATTR_USER_ID: hass_admin_user.id,
|
|
}
|
|
|
|
|
|
async def test_scanner_without_in_zones_not_prioritized(
|
|
hass: HomeAssistant,
|
|
hass_admin_user: MockUser,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test a disconnected scanner does not get highest priority.
|
|
|
|
With an empty `in_zones` it falls into the lowest-priority bucket, so a
|
|
GPS tracker wins.
|
|
"""
|
|
await _async_setup_person_two_trackers(hass, hass_admin_user.id)
|
|
|
|
hass.states.async_set(DEVICE_TRACKER, _ROUTER_NOT_HOME[0], _ROUTER_NOT_HOME[1])
|
|
await hass.async_block_till_done()
|
|
freezer.tick(timedelta(minutes=5))
|
|
hass.states.async_set(DEVICE_TRACKER_2, _GPS_WORK[0], _GPS_WORK[1])
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "work"
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_GPS_ACCURACY: 7,
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: ["zone.work"],
|
|
ATTR_LATITUDE: 3.0,
|
|
ATTR_LONGITUDE: 4.0,
|
|
ATTR_SOURCE: DEVICE_TRACKER_2,
|
|
ATTR_USER_ID: hass_admin_user.id,
|
|
}
|
|
|
|
|
|
async def test_ignore_unavailable_states(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test set up person with two device trackers, one unavailable."""
|
|
hass.set_state(CoreState.not_running)
|
|
user_id = hass_admin_user.id
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
expected_attributes = {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(DEVICE_TRACKER, "home")
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(DEVICE_TRACKER, "unavailable")
|
|
await hass.async_block_till_done()
|
|
|
|
# Unknown, as only 1 device tracker has a state, but we ignore that one
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
hass.states.async_set(DEVICE_TRACKER_2, "not_home")
|
|
await hass.async_block_till_done()
|
|
|
|
# Take state of tracker 2
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "not_home"
|
|
assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER_2}
|
|
|
|
# state 1 is newer but ignored, keep tracker 2 state
|
|
hass.states.async_set(DEVICE_TRACKER, "unknown")
|
|
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_2}
|
|
|
|
|
|
async def test_restore_home_state(
|
|
hass: HomeAssistant, hass_admin_user: MockUser
|
|
) -> None:
|
|
"""Test that the state is restored for a person on startup."""
|
|
user_id = hass_admin_user.id
|
|
attrs = {
|
|
ATTR_ID: "1234",
|
|
ATTR_LATITUDE: 10.12346,
|
|
ATTR_LONGITUDE: 11.12346,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
state = State("person.tracked_person", "home", attrs)
|
|
mock_restore_cache(hass, (state,))
|
|
hass.set_state(CoreState.not_running)
|
|
mock_component(hass, "recorder")
|
|
config = {
|
|
DOMAIN: {
|
|
"id": "1234",
|
|
"name": "tracked person",
|
|
"user_id": user_id,
|
|
"device_trackers": DEVICE_TRACKER,
|
|
"picture": "/bla",
|
|
}
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
# When restoring state the entity_id of the person will be used as source.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
assert state.attributes == {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER],
|
|
ATTR_EDITABLE: False,
|
|
ATTR_ENTITY_PICTURE: "/bla",
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_LATITUDE: 10.12346,
|
|
ATTR_LONGITUDE: 11.12346,
|
|
ATTR_SOURCE: "person.tracked_person",
|
|
ATTR_USER_ID: user_id,
|
|
}
|
|
|
|
|
|
async def test_duplicate_ids(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
|
|
"""Test we don't allow duplicate IDs."""
|
|
config = {
|
|
DOMAIN: [
|
|
{"id": "1234", "name": "test user 1"},
|
|
{"id": "1234", "name": "test user 2"},
|
|
]
|
|
}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
|
|
assert len(hass.states.async_entity_ids(DOMAIN)) == 1
|
|
assert hass.states.get("person.test_user_1") is not None
|
|
assert hass.states.get("person.test_user_2") is None
|
|
|
|
|
|
async def test_create_person_during_run(hass: HomeAssistant) -> None:
|
|
"""Test that person is updated if created while hass is running."""
|
|
config = {DOMAIN: {}}
|
|
assert await async_setup_component(hass, DOMAIN, config)
|
|
hass.states.async_set(DEVICE_TRACKER, "home")
|
|
await hass.async_block_till_done()
|
|
|
|
await person.async_create_person(
|
|
hass, "tracked person", device_trackers=[DEVICE_TRACKER]
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
|
|
|
|
async def test_load_person_storage(
|
|
hass: HomeAssistant, hass_admin_user: MockUser, storage_setup
|
|
) -> None:
|
|
"""Test set up person from storage."""
|
|
expected_attributes = {
|
|
ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER],
|
|
ATTR_EDITABLE: True,
|
|
ATTR_FRIENDLY_NAME: "tracked person",
|
|
ATTR_ID: "1234",
|
|
ATTR_IN_ZONES: [],
|
|
ATTR_USER_ID: hass_admin_user.id,
|
|
}
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == STATE_UNKNOWN
|
|
assert state.attributes == expected_attributes
|
|
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
hass.states.async_set(DEVICE_TRACKER, "home")
|
|
await hass.async_block_till_done()
|
|
|
|
# A legacy tracker reporting home (no coordinates) is placed at the home zone.
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.state == "home"
|
|
assert state.attributes == expected_attributes | {
|
|
ATTR_LATITUDE: 32.87336,
|
|
ATTR_LONGITUDE: -117.22743,
|
|
ATTR_SOURCE: DEVICE_TRACKER,
|
|
}
|
|
|
|
|
|
async def test_load_person_storage_two_nonlinked(
|
|
hass: HomeAssistant, hass_storage: dict[str, Any]
|
|
) -> None:
|
|
"""Test loading two users with both not having a user linked."""
|
|
hass_storage[DOMAIN] = {
|
|
"key": DOMAIN,
|
|
"version": 1,
|
|
"data": {
|
|
"persons": [
|
|
{
|
|
"id": "1234",
|
|
"name": "tracked person 1",
|
|
"user_id": None,
|
|
"device_trackers": [],
|
|
},
|
|
{
|
|
"id": "5678",
|
|
"name": "tracked person 2",
|
|
"user_id": None,
|
|
"device_trackers": [],
|
|
},
|
|
]
|
|
},
|
|
}
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
|
|
assert len(hass.states.async_entity_ids(DOMAIN)) == 2
|
|
assert hass.states.get("person.tracked_person_1") is not None
|
|
assert hass.states.get("person.tracked_person_2") is not None
|
|
|
|
|
|
async def test_ws_list(
|
|
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup
|
|
) -> None:
|
|
"""Test listing via WS."""
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
resp = await client.send_json({"id": 6, "type": "person/list"})
|
|
resp = await client.receive_json()
|
|
assert resp["success"]
|
|
assert resp["result"]["storage"] == manager.async_items()
|
|
assert len(resp["result"]["storage"]) == 1
|
|
assert len(resp["result"]["config"]) == 0
|
|
|
|
|
|
async def test_ws_create(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
storage_setup,
|
|
hass_read_only_user: MockUser,
|
|
) -> None:
|
|
"""Test creating via WS."""
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": "person/create",
|
|
"name": "Hello",
|
|
"device_trackers": [DEVICE_TRACKER],
|
|
"user_id": hass_read_only_user.id,
|
|
"picture": "/bla",
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
|
|
persons = manager.async_items()
|
|
assert len(persons) == 2
|
|
|
|
assert resp["success"]
|
|
assert resp["result"] == persons[1]
|
|
|
|
|
|
async def test_ws_create_requires_admin(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
storage_setup,
|
|
hass_admin_user: MockUser,
|
|
hass_read_only_user: MockUser,
|
|
) -> None:
|
|
"""Test creating via WS requires admin."""
|
|
hass_admin_user.groups = []
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": "person/create",
|
|
"name": "Hello",
|
|
"device_trackers": [DEVICE_TRACKER],
|
|
"user_id": hass_read_only_user.id,
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
|
|
persons = manager.async_items()
|
|
assert len(persons) == 1
|
|
|
|
assert not resp["success"]
|
|
|
|
|
|
async def test_ws_update(
|
|
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, storage_setup
|
|
) -> None:
|
|
"""Test updating via WS."""
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
persons = manager.async_items()
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": "person/update",
|
|
"person_id": persons[0]["id"],
|
|
"user_id": persons[0]["user_id"],
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
|
|
assert resp["success"]
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 7,
|
|
"type": "person/update",
|
|
"person_id": persons[0]["id"],
|
|
"name": "Updated Name",
|
|
"device_trackers": [DEVICE_TRACKER_2],
|
|
"user_id": None,
|
|
"picture": "/bla",
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
|
|
persons = manager.async_items()
|
|
assert len(persons) == 1
|
|
|
|
assert resp["success"]
|
|
assert resp["result"] == persons[0]
|
|
assert persons[0]["name"] == "Updated Name"
|
|
assert persons[0]["name"] == "Updated Name"
|
|
assert persons[0]["device_trackers"] == [DEVICE_TRACKER_2]
|
|
assert persons[0]["user_id"] is None
|
|
assert persons[0]["picture"] == "/bla"
|
|
|
|
state = hass.states.get("person.tracked_person")
|
|
assert state.name == "Updated Name"
|
|
|
|
|
|
async def test_ws_update_require_admin(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
storage_setup,
|
|
hass_admin_user: MockUser,
|
|
) -> None:
|
|
"""Test updating via WS requires admin."""
|
|
hass_admin_user.groups = []
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
original = dict(manager.async_items()[0])
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": "person/update",
|
|
"person_id": original["id"],
|
|
"name": "Updated Name",
|
|
"device_trackers": [DEVICE_TRACKER_2],
|
|
"user_id": None,
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
assert not resp["success"]
|
|
|
|
not_updated = dict(manager.async_items()[0])
|
|
assert original == not_updated
|
|
|
|
|
|
async def test_ws_delete(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
entity_registry: er.EntityRegistry,
|
|
storage_setup,
|
|
) -> None:
|
|
"""Test deleting via WS."""
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
persons = manager.async_items()
|
|
|
|
resp = await client.send_json(
|
|
{"id": 6, "type": "person/delete", "person_id": persons[0]["id"]}
|
|
)
|
|
resp = await client.receive_json()
|
|
|
|
persons = manager.async_items()
|
|
assert len(persons) == 0
|
|
|
|
assert resp["success"]
|
|
assert len(hass.states.async_entity_ids(DOMAIN)) == 0
|
|
assert not entity_registry.async_is_registered("person.tracked_person")
|
|
|
|
|
|
async def test_ws_delete_require_admin(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
storage_setup,
|
|
hass_admin_user: MockUser,
|
|
) -> None:
|
|
"""Test deleting via WS requires admin."""
|
|
hass_admin_user.groups = []
|
|
manager = hass.data[DOMAIN][1]
|
|
|
|
client = await hass_ws_client(hass)
|
|
|
|
resp = await client.send_json(
|
|
{
|
|
"id": 6,
|
|
"type": "person/delete",
|
|
"person_id": manager.async_items()[0]["id"],
|
|
"name": "Updated Name",
|
|
"device_trackers": [DEVICE_TRACKER_2],
|
|
"user_id": None,
|
|
}
|
|
)
|
|
resp = await client.receive_json()
|
|
assert not resp["success"]
|
|
|
|
persons = manager.async_items()
|
|
assert len(persons) == 1
|
|
|
|
|
|
async def test_create_invalid_user_id(hass: HomeAssistant, storage_collection) -> None:
|
|
"""Test we do not allow invalid user ID during creation."""
|
|
with pytest.raises(ValueError):
|
|
await storage_collection.async_create_item(
|
|
{"name": "Hello", "user_id": "non-existing"}
|
|
)
|
|
|
|
|
|
async def test_create_duplicate_user_id(
|
|
hass: HomeAssistant, hass_admin_user: MockUser, storage_collection
|
|
) -> None:
|
|
"""Test we do not allow duplicate user ID during creation."""
|
|
await storage_collection.async_create_item(
|
|
{"name": "Hello", "user_id": hass_admin_user.id}
|
|
)
|
|
|
|
with pytest.raises(ValueError):
|
|
await storage_collection.async_create_item(
|
|
{"name": "Hello", "user_id": hass_admin_user.id}
|
|
)
|
|
|
|
|
|
async def test_update_double_user_id(
|
|
hass: HomeAssistant, hass_admin_user: MockUser, storage_collection
|
|
) -> None:
|
|
"""Test we do not allow double user ID during update."""
|
|
await storage_collection.async_create_item(
|
|
{"name": "Hello", "user_id": hass_admin_user.id}
|
|
)
|
|
person = await storage_collection.async_create_item({"name": "Hello"})
|
|
|
|
with pytest.raises(ValueError):
|
|
await storage_collection.async_update_item(
|
|
person["id"], {"user_id": hass_admin_user.id}
|
|
)
|
|
|
|
|
|
async def test_update_invalid_user_id(hass: HomeAssistant, storage_collection) -> None:
|
|
"""Test updating to invalid user ID."""
|
|
person = await storage_collection.async_create_item({"name": "Hello"})
|
|
|
|
with pytest.raises(ValueError):
|
|
await storage_collection.async_update_item(
|
|
person["id"], {"user_id": "non-existing"}
|
|
)
|
|
|
|
|
|
async def test_update_person_when_user_removed(
|
|
hass: HomeAssistant, storage_setup, hass_read_only_user: MockUser
|
|
) -> None:
|
|
"""Update person when user is removed."""
|
|
storage_collection = hass.data[DOMAIN][1]
|
|
|
|
person = await storage_collection.async_create_item(
|
|
{"name": "Hello", "user_id": hass_read_only_user.id}
|
|
)
|
|
|
|
await hass.auth.async_remove_user(hass_read_only_user)
|
|
await hass.async_block_till_done()
|
|
|
|
assert storage_collection.data[person["id"]]["user_id"] is None
|
|
|
|
|
|
async def test_removing_device_tracker(
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry, storage_setup
|
|
) -> None:
|
|
"""Test we automatically remove removed device trackers."""
|
|
storage_collection = hass.data[DOMAIN][1]
|
|
entry = entity_registry.async_get_or_create(
|
|
"device_tracker", "mobile_app", "bla", suggested_object_id="pixel"
|
|
)
|
|
|
|
person = await storage_collection.async_create_item(
|
|
{"name": "Hello", "device_trackers": [entry.entity_id]}
|
|
)
|
|
|
|
entity_registry.async_remove(entry.entity_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert storage_collection.data[person["id"]]["device_trackers"] == []
|
|
|
|
|
|
async def test_add_user_device_tracker(
|
|
hass: HomeAssistant, storage_setup, hass_read_only_user: MockUser
|
|
) -> None:
|
|
"""Test adding a device tracker to a person tied to a user."""
|
|
storage_collection = hass.data[DOMAIN][1]
|
|
pers = await storage_collection.async_create_item(
|
|
{
|
|
"name": "Hello",
|
|
"user_id": hass_read_only_user.id,
|
|
"device_trackers": ["device_tracker.on_create"],
|
|
}
|
|
)
|
|
|
|
await person.async_add_user_device_tracker(
|
|
hass, hass_read_only_user.id, "device_tracker.added"
|
|
)
|
|
|
|
assert storage_collection.data[pers["id"]]["device_trackers"] == [
|
|
"device_tracker.on_create",
|
|
"device_tracker.added",
|
|
]
|
|
|
|
|
|
async def test_reload(hass: HomeAssistant, hass_admin_user: MockUser) -> None:
|
|
"""Test reloading the YAML config."""
|
|
assert await async_setup_component(
|
|
hass,
|
|
DOMAIN,
|
|
{
|
|
DOMAIN: [
|
|
{"name": "Person 1", "id": "id-1"},
|
|
{"name": "Person 2", "id": "id-2"},
|
|
]
|
|
},
|
|
)
|
|
|
|
assert len(hass.states.async_entity_ids()) == 3 # zone.home, Person1, Person2
|
|
|
|
state_1 = hass.states.get("person.person_1")
|
|
state_2 = hass.states.get("person.person_2")
|
|
state_3 = hass.states.get("person.person_3")
|
|
|
|
assert state_1 is not None
|
|
assert state_1.name == "Person 1"
|
|
assert state_2 is not None
|
|
assert state_2.name == "Person 2"
|
|
assert state_3 is None
|
|
|
|
with patch(
|
|
"homeassistant.config.load_yaml_config_file",
|
|
autospec=True,
|
|
return_value={
|
|
DOMAIN: [
|
|
{"name": "Person 1-updated", "id": "id-1"},
|
|
{"name": "Person 3", "id": "id-3"},
|
|
]
|
|
},
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_RELOAD,
|
|
blocking=True,
|
|
context=Context(user_id=hass_admin_user.id),
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(hass.states.async_entity_ids()) == 3 # zone.home, Person1, Person3
|
|
|
|
state_1 = hass.states.get("person.person_1")
|
|
state_2 = hass.states.get("person.person_2")
|
|
state_3 = hass.states.get("person.person_3")
|
|
|
|
assert state_1 is not None
|
|
assert state_1.name == "Person 1-updated"
|
|
assert state_2 is None
|
|
assert state_3 is not None
|
|
assert state_3.name == "Person 3"
|
|
|
|
|
|
async def test_person_storage_fixing_device_trackers(storage_collection) -> None:
|
|
"""Test None device trackers become lists."""
|
|
with patch.object(
|
|
storage_collection.store,
|
|
"async_load",
|
|
return_value={"items": [{"id": "bla", "name": "bla", "device_trackers": None}]},
|
|
):
|
|
await storage_collection.async_load()
|
|
|
|
assert storage_collection.data["bla"]["device_trackers"] == []
|
|
|
|
|
|
async def test_persons_with_entity(hass: HomeAssistant) -> None:
|
|
"""Test finding persons with an entity."""
|
|
assert await async_setup_component(
|
|
hass,
|
|
DOMAIN,
|
|
{
|
|
"person": [
|
|
{
|
|
"id": "abcd",
|
|
"name": "Paulus",
|
|
"device_trackers": [
|
|
"device_tracker.paulus_iphone",
|
|
"device_tracker.paulus_ipad",
|
|
],
|
|
},
|
|
{
|
|
"id": "efgh",
|
|
"name": "Anne Therese",
|
|
"device_trackers": [
|
|
"device_tracker.at_pixel",
|
|
],
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
assert person.persons_with_entity(hass, "device_tracker.paulus_iphone") == [
|
|
"person.paulus"
|
|
]
|
|
|
|
|
|
async def test_entities_in_person(hass: HomeAssistant) -> None:
|
|
"""Test finding entities tracked by person."""
|
|
assert await async_setup_component(
|
|
hass,
|
|
DOMAIN,
|
|
{
|
|
"person": [
|
|
{
|
|
"id": "abcd",
|
|
"name": "Paulus",
|
|
"device_trackers": [
|
|
"device_tracker.paulus_iphone",
|
|
"device_tracker.paulus_ipad",
|
|
],
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
assert person.entities_in_person(hass, "person.paulus") == [
|
|
"device_tracker.paulus_iphone",
|
|
"device_tracker.paulus_ipad",
|
|
]
|