mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Person component: add storage and WS commands (#20852)
* Forbid duplicate IDs * Allow loading persons from storage * Convert to PersonManager * Add storage support and WS commands to Person component * Convert list command to differentiate types * Allow loading person component without defining persons * Fix cleanups after update/delete * Address comments * Start tracking when HA started
This commit is contained in:
parent
cfd1563bc8
commit
987b5cd905
@ -4,26 +4,37 @@ Support for tracking people.
|
||||
For more details about this component, please refer to the documentation.
|
||||
https://home-assistant.io/components/person/
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
||||
from homeassistant.const import (
|
||||
ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_NAME)
|
||||
ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_EDITABLE = 'editable'
|
||||
ATTR_SOURCE = 'source'
|
||||
ATTR_USER_ID = 'user_id'
|
||||
CONF_DEVICE_TRACKERS = 'device_trackers'
|
||||
CONF_USER_ID = 'user_id'
|
||||
DOMAIN = 'person'
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
SAVE_DELAY = 10
|
||||
|
||||
PERSON_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
@ -34,30 +45,161 @@ PERSON_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [PERSON_SCHEMA])
|
||||
vol.Optional(DOMAIN): vol.Any(vol.All(cv.ensure_list, [PERSON_SCHEMA]), {})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_UNDEF = object()
|
||||
|
||||
async def async_setup(hass, config):
|
||||
|
||||
class PersonManager:
|
||||
"""Manage person data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, component: EntityComponent,
|
||||
config_persons):
|
||||
"""Initialize person storage."""
|
||||
self.hass = hass
|
||||
self.component = component
|
||||
self.store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self.storage_data = None
|
||||
|
||||
config_data = self.config_data = OrderedDict()
|
||||
for conf in config_persons:
|
||||
person_id = conf[CONF_ID]
|
||||
|
||||
if person_id in config_data:
|
||||
_LOGGER.error("Found config user with duplicate ID: %s",
|
||||
person_id)
|
||||
continue
|
||||
|
||||
config_data[person_id] = conf
|
||||
|
||||
@property
|
||||
def storage_persons(self):
|
||||
"""Iterate over persons stored in storage."""
|
||||
return list(self.storage_data.values())
|
||||
|
||||
@property
|
||||
def config_persons(self):
|
||||
"""Iterate over persons stored in config."""
|
||||
return list(self.config_data.values())
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Get the person data."""
|
||||
raw_storage = await self.store.async_load()
|
||||
|
||||
if raw_storage is None:
|
||||
raw_storage = {
|
||||
'persons': []
|
||||
}
|
||||
|
||||
storage_data = self.storage_data = OrderedDict()
|
||||
|
||||
for person in raw_storage['persons']:
|
||||
storage_data[person[CONF_ID]] = person
|
||||
|
||||
entities = []
|
||||
|
||||
for person_conf in self.config_data.values():
|
||||
person_id = person_conf[CONF_ID]
|
||||
user_id = person_conf.get(CONF_USER_ID)
|
||||
|
||||
if (user_id is not None
|
||||
and await self.hass.auth.async_get_user(user_id) is None):
|
||||
_LOGGER.error(
|
||||
"Invalid user_id detected for person %s", person_id)
|
||||
continue
|
||||
|
||||
entities.append(Person(person_conf, False))
|
||||
|
||||
for person_conf in storage_data.values():
|
||||
if person_conf[CONF_ID] in self.config_data:
|
||||
_LOGGER.error(
|
||||
"Skipping adding person from storage with same ID as"
|
||||
" configuration.yaml entry: %s", person_id)
|
||||
continue
|
||||
|
||||
entities.append(Person(person_conf, True))
|
||||
|
||||
if entities:
|
||||
await self.component.async_add_entities(entities)
|
||||
|
||||
async def async_create_person(self, *, name, device_trackers=None,
|
||||
user_id=None):
|
||||
"""Create a new person."""
|
||||
person = {
|
||||
CONF_ID: uuid.uuid4().hex,
|
||||
CONF_NAME: name,
|
||||
CONF_USER_ID: user_id,
|
||||
CONF_DEVICE_TRACKERS: device_trackers,
|
||||
}
|
||||
self.storage_data[person[CONF_ID]] = person
|
||||
self._async_schedule_save()
|
||||
await self.component.async_add_entities([Person(person, True)])
|
||||
return person
|
||||
|
||||
async def async_update_person(self, person_id, *, name=_UNDEF,
|
||||
device_trackers=_UNDEF, user_id=_UNDEF):
|
||||
"""Update person."""
|
||||
if person_id not in self.storage_data:
|
||||
raise ValueError("Invalid person specified.")
|
||||
|
||||
changes = {
|
||||
key: value for key, value in (
|
||||
('name', name),
|
||||
('device_trackers', device_trackers),
|
||||
('user_id', user_id)
|
||||
) if value is not _UNDEF
|
||||
}
|
||||
|
||||
self.storage_data[person_id].update(changes)
|
||||
self._async_schedule_save()
|
||||
|
||||
for entity in self.component.entities:
|
||||
if entity.unique_id == person_id:
|
||||
entity.person_updated()
|
||||
break
|
||||
|
||||
return self.storage_data[person_id]
|
||||
|
||||
async def async_delete_person(self, person_id):
|
||||
"""Delete person."""
|
||||
if person_id not in self.storage_data:
|
||||
raise ValueError("Invalid person specified.")
|
||||
|
||||
self.storage_data.pop(person_id)
|
||||
self._async_schedule_save()
|
||||
ent_reg = await self.hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
for entity in self.component.entities:
|
||||
if entity.unique_id == person_id:
|
||||
await entity.async_remove()
|
||||
ent_reg.async_remove(entity.entity_id)
|
||||
break
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
"""Schedule saving the area registry."""
|
||||
self.store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict:
|
||||
"""Return data of area registry to store in a file."""
|
||||
return {
|
||||
'persons': list(self.storage_data.values())
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the person component."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
conf = config[DOMAIN]
|
||||
entities = []
|
||||
for person_conf in conf:
|
||||
user_id = person_conf.get(CONF_USER_ID)
|
||||
if (user_id is not None
|
||||
and await hass.auth.async_get_user(user_id) is None):
|
||||
_LOGGER.error(
|
||||
"Invalid user_id detected for person %s",
|
||||
person_conf[CONF_NAME])
|
||||
continue
|
||||
entities.append(Person(person_conf, user_id))
|
||||
conf_persons = config.get(DOMAIN, [])
|
||||
manager = hass.data[DOMAIN] = PersonManager(hass, component, conf_persons)
|
||||
await manager.async_initialize()
|
||||
|
||||
if not entities:
|
||||
_LOGGER.error("No persons could be set up")
|
||||
return False
|
||||
|
||||
await component.async_add_entities(entities)
|
||||
websocket_api.async_register_command(hass, ws_list_person)
|
||||
websocket_api.async_register_command(hass, ws_create_person)
|
||||
websocket_api.async_register_command(hass, ws_update_person)
|
||||
websocket_api.async_register_command(hass, ws_delete_person)
|
||||
|
||||
return True
|
||||
|
||||
@ -65,21 +207,20 @@ async def async_setup(hass, config):
|
||||
class Person(RestoreEntity):
|
||||
"""Represent a tracked person."""
|
||||
|
||||
def __init__(self, config, user_id):
|
||||
def __init__(self, config, editable):
|
||||
"""Set up person."""
|
||||
self._id = config[CONF_ID]
|
||||
self._config = config
|
||||
self._editable = editable
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._name = config[CONF_NAME]
|
||||
self._source = None
|
||||
self._state = None
|
||||
self._trackers = config.get(CONF_DEVICE_TRACKERS)
|
||||
self._user_id = user_id
|
||||
self._unsub_track_device = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
return self._config[CONF_NAME]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -97,22 +238,25 @@ class Person(RestoreEntity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the person."""
|
||||
data = {}
|
||||
data[ATTR_ID] = self._id
|
||||
data = {
|
||||
ATTR_EDITABLE: self._editable,
|
||||
ATTR_ID: self.unique_id,
|
||||
}
|
||||
if self._latitude is not None:
|
||||
data[ATTR_LATITUDE] = round(self._latitude, 5)
|
||||
if self._longitude is not None:
|
||||
data[ATTR_LONGITUDE] = round(self._longitude, 5)
|
||||
if self._source is not None:
|
||||
data[ATTR_SOURCE] = self._source
|
||||
if self._user_id is not None:
|
||||
data[ATTR_USER_ID] = self._user_id
|
||||
user_id = self._config.get(CONF_USER_ID)
|
||||
if user_id is not None:
|
||||
data[ATTR_USER_ID] = user_id
|
||||
return data
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID for the person."""
|
||||
return self._id
|
||||
return self._config[CONF_ID]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register device trackers."""
|
||||
@ -121,25 +265,137 @@ class Person(RestoreEntity):
|
||||
if state:
|
||||
self._parse_source_state(state)
|
||||
|
||||
if not self._trackers:
|
||||
return
|
||||
|
||||
@callback
|
||||
def async_handle_tracker_update(entity, old_state, new_state):
|
||||
"""Handle the device tracker state changes."""
|
||||
self._parse_source_state(new_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
def person_start_hass(now):
|
||||
self.person_updated()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Subscribe to device trackers for %s", self.entity_id)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
|
||||
person_start_hass)
|
||||
|
||||
for tracker in self._trackers:
|
||||
async_track_state_change(
|
||||
self.hass, tracker, async_handle_tracker_update)
|
||||
@callback
|
||||
def person_updated(self):
|
||||
"""Handle when the config is updated."""
|
||||
if self._unsub_track_device is not None:
|
||||
self._unsub_track_device()
|
||||
self._unsub_track_device = None
|
||||
|
||||
trackers = self._config.get(CONF_DEVICE_TRACKERS)
|
||||
|
||||
if trackers:
|
||||
def sort_key(state):
|
||||
if state:
|
||||
return state.last_updated
|
||||
return dt_util.utc_from_timestamp(0)
|
||||
|
||||
latest = max(
|
||||
[self.hass.states.get(entity_id) for entity_id in trackers],
|
||||
key=sort_key
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_handle_tracker_update(entity, old_state, new_state):
|
||||
"""Handle the device tracker state changes."""
|
||||
self._parse_source_state(new_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Subscribe to device trackers for %s", self.entity_id)
|
||||
|
||||
self._unsub_track_device = async_track_state_change(
|
||||
self.hass, trackers, async_handle_tracker_update)
|
||||
|
||||
else:
|
||||
latest = None
|
||||
|
||||
if latest:
|
||||
self._parse_source_state(latest)
|
||||
else:
|
||||
self._state = None
|
||||
self._source = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _parse_source_state(self, state):
|
||||
"""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.
|
||||
"""
|
||||
self._state = state.state
|
||||
self._source = state.entity_id
|
||||
self._latitude = state.attributes.get(ATTR_LATITUDE)
|
||||
self._longitude = state.attributes.get(ATTR_LONGITUDE)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required('type'): 'person/list',
|
||||
})
|
||||
def ws_list_person(hass: HomeAssistantType,
|
||||
connection: websocket_api.ActiveConnection, msg):
|
||||
"""List persons."""
|
||||
manager = hass.data[DOMAIN] # type: PersonManager
|
||||
connection.send_result(msg['id'], {
|
||||
'storage': manager.storage_persons,
|
||||
'config': manager.config_persons,
|
||||
})
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required('type'): 'person/create',
|
||||
vol.Required('name'): str,
|
||||
vol.Optional('user_id'): vol.Any(str, None),
|
||||
vol.Optional('device_trackers', default=[]): vol.All(
|
||||
cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)),
|
||||
})
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def ws_create_person(hass: HomeAssistantType,
|
||||
connection: websocket_api.ActiveConnection, msg):
|
||||
"""Create a person."""
|
||||
manager = hass.data[DOMAIN] # type: PersonManager
|
||||
person = await manager.async_create_person(
|
||||
name=msg['name'],
|
||||
user_id=msg.get('user_id'),
|
||||
device_trackers=msg['device_trackers']
|
||||
)
|
||||
connection.send_result(msg['id'], person)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required('type'): 'person/update',
|
||||
vol.Required('person_id'): str,
|
||||
vol.Optional('name'): str,
|
||||
vol.Optional('user_id'): vol.Any(str, None),
|
||||
vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All(
|
||||
cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)),
|
||||
})
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def ws_update_person(hass: HomeAssistantType,
|
||||
connection: websocket_api.ActiveConnection, msg):
|
||||
"""Update a person."""
|
||||
manager = hass.data[DOMAIN] # type: PersonManager
|
||||
changes = {}
|
||||
for key in ('name', 'user_id', 'device_trackers'):
|
||||
if key in msg:
|
||||
changes[key] = msg[key]
|
||||
|
||||
person = await manager.async_update_person(msg['person_id'], **changes)
|
||||
connection.send_result(msg['id'], person)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required('type'): 'person/delete',
|
||||
vol.Required('person_id'): str,
|
||||
})
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
async def ws_delete_person(hass: HomeAssistantType,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg):
|
||||
"""Delete a person."""
|
||||
manager = hass.data[DOMAIN] # type: PersonManager
|
||||
await manager.async_delete_person(msg['person_id'])
|
||||
connection.send_result(msg['id'])
|
||||
|
@ -31,6 +31,16 @@ class ActiveConnection:
|
||||
return Context()
|
||||
return Context(user_id=user.id)
|
||||
|
||||
@callback
|
||||
def send_result(self, msg_id, result=None):
|
||||
"""Send a result message."""
|
||||
self.send_message(messages.result_message(msg_id, result))
|
||||
|
||||
@callback
|
||||
def send_error(self, msg_id, code, message):
|
||||
"""Send a error message."""
|
||||
self.send_message(messages.error_message(msg_id, code, message))
|
||||
|
||||
@callback
|
||||
def async_handle(self, msg):
|
||||
"""Handle a single incoming message."""
|
||||
|
@ -319,7 +319,7 @@ class Entity:
|
||||
@callback
|
||||
def async_schedule_update_ha_state(self, force_refresh=False):
|
||||
"""Schedule an update ha state change task."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(force_refresh))
|
||||
self.hass.async_create_task(self.async_update_ha_state(force_refresh))
|
||||
|
||||
async def async_device_update(self, warning=True):
|
||||
"""Process 'update' or 'async_update' from entity.
|
||||
|
@ -1,16 +1,41 @@
|
||||
"""The tests for the person component."""
|
||||
from homeassistant.components.person import ATTR_SOURCE, ATTR_USER_ID, DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN)
|
||||
ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import CoreState, State
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.common import mock_component, mock_restore_cache
|
||||
|
||||
DEVICE_TRACKER = 'device_tracker.test_tracker'
|
||||
DEVICE_TRACKER_2 = 'device_tracker.test_tracker_2'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage_setup(hass, hass_storage, hass_admin_user):
|
||||
"""Storage setup."""
|
||||
hass_storage[DOMAIN] = {
|
||||
'key': DOMAIN,
|
||||
'version': 1,
|
||||
'data': {
|
||||
'persons': [
|
||||
{
|
||||
'id': '1234',
|
||||
'name': 'tracked person',
|
||||
'user_id': hass_admin_user.id,
|
||||
'device_trackers': DEVICE_TRACKER
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
assert hass.loop.run_until_complete(
|
||||
async_setup_component(hass, DOMAIN, {})
|
||||
)
|
||||
|
||||
|
||||
async def test_minimal_setup(hass):
|
||||
"""Test minimal config with only name."""
|
||||
config = {DOMAIN: {'id': '1234', 'name': 'test person'}}
|
||||
@ -36,9 +61,9 @@ async def test_setup_no_name(hass):
|
||||
assert not await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
|
||||
async def test_setup_user_id(hass, hass_owner_user):
|
||||
async def test_setup_user_id(hass, hass_admin_user):
|
||||
"""Test config with user id."""
|
||||
user_id = hass_owner_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)
|
||||
@ -52,17 +77,9 @@ async def test_setup_user_id(hass, hass_owner_user):
|
||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||
|
||||
|
||||
async def test_setup_invalid_user_id(hass):
|
||||
"""Test config with invalid user id."""
|
||||
config = {
|
||||
DOMAIN: {
|
||||
'id': '1234', 'name': 'test bad user', 'user_id': 'bad_user_id'}}
|
||||
assert not await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
|
||||
async def test_valid_invalid_user_ids(hass, hass_owner_user):
|
||||
async def test_valid_invalid_user_ids(hass, hass_admin_user):
|
||||
"""Test a person with valid user id and a person with invalid user id ."""
|
||||
user_id = hass_owner_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'}]}
|
||||
@ -79,9 +96,9 @@ async def test_valid_invalid_user_ids(hass, hass_owner_user):
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_setup_tracker(hass, hass_owner_user):
|
||||
async def test_setup_tracker(hass, hass_admin_user):
|
||||
"""Test set up person with one device tracker."""
|
||||
user_id = hass_owner_user.id
|
||||
user_id = hass_admin_user.id
|
||||
config = {DOMAIN: {
|
||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||
'device_trackers': DEVICE_TRACKER}}
|
||||
@ -98,6 +115,12 @@ async def test_setup_tracker(hass, hass_owner_user):
|
||||
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()
|
||||
|
||||
state = hass.states.get('person.tracked_person')
|
||||
assert state.state == 'home'
|
||||
assert state.attributes.get(ATTR_ID) == '1234'
|
||||
@ -120,9 +143,9 @@ async def test_setup_tracker(hass, hass_owner_user):
|
||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||
|
||||
|
||||
async def test_setup_two_trackers(hass, hass_owner_user):
|
||||
async def test_setup_two_trackers(hass, hass_admin_user):
|
||||
"""Test set up person with two device trackers."""
|
||||
user_id = hass_owner_user.id
|
||||
user_id = hass_admin_user.id
|
||||
config = {DOMAIN: {
|
||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||
'device_trackers': [DEVICE_TRACKER, DEVICE_TRACKER_2]}}
|
||||
@ -136,6 +159,8 @@ async def test_setup_two_trackers(hass, hass_owner_user):
|
||||
assert state.attributes.get(ATTR_SOURCE) is None
|
||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||
|
||||
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()
|
||||
|
||||
@ -161,9 +186,9 @@ async def test_setup_two_trackers(hass, hass_owner_user):
|
||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||
|
||||
|
||||
async def test_restore_home_state(hass, hass_owner_user):
|
||||
async def test_restore_home_state(hass, hass_admin_user):
|
||||
"""Test that the state is restored for a person on startup."""
|
||||
user_id = hass_owner_user.id
|
||||
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}
|
||||
@ -184,3 +209,203 @@ async def test_restore_home_state(hass, hass_owner_user):
|
||||
# When restoring state the entity_id of the person will be used as source.
|
||||
assert state.attributes.get(ATTR_SOURCE) == 'person.tracked_person'
|
||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||
|
||||
|
||||
async def test_duplicate_ids(hass, hass_admin_user):
|
||||
"""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('person')) == 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_load_person_storage(hass, hass_admin_user, storage_setup):
|
||||
"""Test set up person from storage."""
|
||||
state = hass.states.get('person.tracked_person')
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.attributes.get(ATTR_ID) == '1234'
|
||||
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) == hass_admin_user.id
|
||||
|
||||
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()
|
||||
|
||||
state = hass.states.get('person.tracked_person')
|
||||
assert state.state == 'home'
|
||||
assert state.attributes.get(ATTR_ID) == '1234'
|
||||
assert state.attributes.get(ATTR_LATITUDE) is None
|
||||
assert state.attributes.get(ATTR_LONGITUDE) is None
|
||||
assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER
|
||||
assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id
|
||||
|
||||
|
||||
async def test_ws_list(hass, hass_ws_client, storage_setup):
|
||||
"""Test listing via WS."""
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
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.storage_persons
|
||||
assert len(resp['result']['storage']) == 1
|
||||
assert len(resp['result']['config']) == 0
|
||||
|
||||
|
||||
async def test_ws_create(hass, hass_ws_client, storage_setup,
|
||||
hass_read_only_user):
|
||||
"""Test creating via WS."""
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
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.storage_persons
|
||||
assert len(persons) == 2
|
||||
|
||||
assert resp['success']
|
||||
assert resp['result'] == persons[1]
|
||||
|
||||
|
||||
async def test_ws_create_requires_admin(hass, hass_ws_client, storage_setup,
|
||||
hass_admin_user, hass_read_only_user):
|
||||
"""Test creating via WS requires admin."""
|
||||
hass_admin_user.groups = []
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
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.storage_persons
|
||||
assert len(persons) == 1
|
||||
|
||||
assert not resp['success']
|
||||
|
||||
|
||||
async def test_ws_update(hass, hass_ws_client, storage_setup):
|
||||
"""Test updating via WS."""
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
persons = manager.storage_persons
|
||||
|
||||
resp = await client.send_json({
|
||||
'id': 6,
|
||||
'type': 'person/update',
|
||||
'person_id': persons[0]['id'],
|
||||
'name': 'Updated Name',
|
||||
'device_trackers': [DEVICE_TRACKER_2],
|
||||
'user_id': None,
|
||||
})
|
||||
resp = await client.receive_json()
|
||||
|
||||
persons = manager.storage_persons
|
||||
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
|
||||
|
||||
state = hass.states.get('person.tracked_person')
|
||||
assert state.name == 'Updated Name'
|
||||
|
||||
|
||||
async def test_ws_update_require_admin(hass, hass_ws_client, storage_setup,
|
||||
hass_admin_user):
|
||||
"""Test updating via WS requires admin."""
|
||||
hass_admin_user.groups = []
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
original = dict(manager.storage_persons[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.storage_persons[0])
|
||||
assert original == not_updated
|
||||
|
||||
|
||||
async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||
"""Test deleting via WS."""
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
persons = manager.storage_persons
|
||||
|
||||
resp = await client.send_json({
|
||||
'id': 6,
|
||||
'type': 'person/delete',
|
||||
'person_id': persons[0]['id'],
|
||||
})
|
||||
resp = await client.receive_json()
|
||||
|
||||
persons = manager.storage_persons
|
||||
assert len(persons) == 0
|
||||
|
||||
assert resp['success']
|
||||
assert len(hass.states.async_entity_ids('person')) == 0
|
||||
ent_reg = await hass.helpers.entity_registry.async_get_registry()
|
||||
assert not ent_reg.async_is_registered('person.tracked_person')
|
||||
|
||||
|
||||
async def test_ws_delete_require_admin(hass, hass_ws_client, storage_setup,
|
||||
hass_admin_user):
|
||||
"""Test deleting via WS requires admin."""
|
||||
hass_admin_user.groups = []
|
||||
manager = hass.data[DOMAIN]
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
resp = await client.send_json({
|
||||
'id': 6,
|
||||
'type': 'person/delete',
|
||||
'person_id': manager.storage_persons[0]['id'],
|
||||
'name': 'Updated Name',
|
||||
'device_trackers': [DEVICE_TRACKER_2],
|
||||
'user_id': None,
|
||||
})
|
||||
resp = await client.receive_json()
|
||||
assert not resp['success']
|
||||
|
||||
persons = manager.storage_persons
|
||||
assert len(persons) == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user