mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +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.
|
For more details about this component, please refer to the documentation.
|
||||||
https://home-assistant.io/components/person/
|
https://home-assistant.io/components/person/
|
||||||
"""
|
"""
|
||||||
|
from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
DOMAIN as DEVICE_TRACKER_DOMAIN)
|
||||||
from homeassistant.const import (
|
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
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
ATTR_EDITABLE = 'editable'
|
||||||
ATTR_SOURCE = 'source'
|
ATTR_SOURCE = 'source'
|
||||||
ATTR_USER_ID = 'user_id'
|
ATTR_USER_ID = 'user_id'
|
||||||
CONF_DEVICE_TRACKERS = 'device_trackers'
|
CONF_DEVICE_TRACKERS = 'device_trackers'
|
||||||
CONF_USER_ID = 'user_id'
|
CONF_USER_ID = 'user_id'
|
||||||
DOMAIN = 'person'
|
DOMAIN = 'person'
|
||||||
|
STORAGE_KEY = DOMAIN
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
SAVE_DELAY = 10
|
||||||
|
|
||||||
PERSON_SCHEMA = vol.Schema({
|
PERSON_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_ID): cv.string,
|
vol.Required(CONF_ID): cv.string,
|
||||||
@ -34,30 +45,161 @@ PERSON_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_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)
|
}, 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."""
|
"""Set up the person component."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
conf = config[DOMAIN]
|
conf_persons = config.get(DOMAIN, [])
|
||||||
entities = []
|
manager = hass.data[DOMAIN] = PersonManager(hass, component, conf_persons)
|
||||||
for person_conf in conf:
|
await manager.async_initialize()
|
||||||
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))
|
|
||||||
|
|
||||||
if not entities:
|
websocket_api.async_register_command(hass, ws_list_person)
|
||||||
_LOGGER.error("No persons could be set up")
|
websocket_api.async_register_command(hass, ws_create_person)
|
||||||
return False
|
websocket_api.async_register_command(hass, ws_update_person)
|
||||||
|
websocket_api.async_register_command(hass, ws_delete_person)
|
||||||
await component.async_add_entities(entities)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -65,21 +207,20 @@ async def async_setup(hass, config):
|
|||||||
class Person(RestoreEntity):
|
class Person(RestoreEntity):
|
||||||
"""Represent a tracked person."""
|
"""Represent a tracked person."""
|
||||||
|
|
||||||
def __init__(self, config, user_id):
|
def __init__(self, config, editable):
|
||||||
"""Set up person."""
|
"""Set up person."""
|
||||||
self._id = config[CONF_ID]
|
self._config = config
|
||||||
|
self._editable = editable
|
||||||
self._latitude = None
|
self._latitude = None
|
||||||
self._longitude = None
|
self._longitude = None
|
||||||
self._name = config[CONF_NAME]
|
|
||||||
self._source = None
|
self._source = None
|
||||||
self._state = None
|
self._state = None
|
||||||
self._trackers = config.get(CONF_DEVICE_TRACKERS)
|
self._unsub_track_device = None
|
||||||
self._user_id = user_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the entity."""
|
"""Return the name of the entity."""
|
||||||
return self._name
|
return self._config[CONF_NAME]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -97,22 +238,25 @@ class Person(RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the state attributes of the person."""
|
"""Return the state attributes of the person."""
|
||||||
data = {}
|
data = {
|
||||||
data[ATTR_ID] = self._id
|
ATTR_EDITABLE: self._editable,
|
||||||
|
ATTR_ID: self.unique_id,
|
||||||
|
}
|
||||||
if self._latitude is not None:
|
if self._latitude is not None:
|
||||||
data[ATTR_LATITUDE] = round(self._latitude, 5)
|
data[ATTR_LATITUDE] = round(self._latitude, 5)
|
||||||
if self._longitude is not None:
|
if self._longitude is not None:
|
||||||
data[ATTR_LONGITUDE] = round(self._longitude, 5)
|
data[ATTR_LONGITUDE] = round(self._longitude, 5)
|
||||||
if self._source is not None:
|
if self._source is not None:
|
||||||
data[ATTR_SOURCE] = self._source
|
data[ATTR_SOURCE] = self._source
|
||||||
if self._user_id is not None:
|
user_id = self._config.get(CONF_USER_ID)
|
||||||
data[ATTR_USER_ID] = self._user_id
|
if user_id is not None:
|
||||||
|
data[ATTR_USER_ID] = user_id
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique ID for the person."""
|
"""Return a unique ID for the person."""
|
||||||
return self._id
|
return self._config[CONF_ID]
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register device trackers."""
|
"""Register device trackers."""
|
||||||
@ -121,8 +265,32 @@ class Person(RestoreEntity):
|
|||||||
if state:
|
if state:
|
||||||
self._parse_source_state(state)
|
self._parse_source_state(state)
|
||||||
|
|
||||||
if not self._trackers:
|
@callback
|
||||||
return
|
def person_start_hass(now):
|
||||||
|
self.person_updated()
|
||||||
|
|
||||||
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
|
person_start_hass)
|
||||||
|
|
||||||
|
@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
|
@callback
|
||||||
def async_handle_tracker_update(entity, old_state, new_state):
|
def async_handle_tracker_update(entity, old_state, new_state):
|
||||||
@ -133,13 +301,101 @@ class Person(RestoreEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Subscribe to device trackers for %s", self.entity_id)
|
"Subscribe to device trackers for %s", self.entity_id)
|
||||||
|
|
||||||
for tracker in self._trackers:
|
self._unsub_track_device = async_track_state_change(
|
||||||
async_track_state_change(
|
self.hass, trackers, async_handle_tracker_update)
|
||||||
self.hass, tracker, 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):
|
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._state = state.state
|
||||||
self._source = state.entity_id
|
self._source = state.entity_id
|
||||||
self._latitude = state.attributes.get(ATTR_LATITUDE)
|
self._latitude = state.attributes.get(ATTR_LATITUDE)
|
||||||
self._longitude = state.attributes.get(ATTR_LONGITUDE)
|
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()
|
||||||
return Context(user_id=user.id)
|
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
|
@callback
|
||||||
def async_handle(self, msg):
|
def async_handle(self, msg):
|
||||||
"""Handle a single incoming message."""
|
"""Handle a single incoming message."""
|
||||||
|
@ -319,7 +319,7 @@ class Entity:
|
|||||||
@callback
|
@callback
|
||||||
def async_schedule_update_ha_state(self, force_refresh=False):
|
def async_schedule_update_ha_state(self, force_refresh=False):
|
||||||
"""Schedule an update ha state change task."""
|
"""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):
|
async def async_device_update(self, warning=True):
|
||||||
"""Process 'update' or 'async_update' from entity.
|
"""Process 'update' or 'async_update' from entity.
|
||||||
|
@ -1,16 +1,41 @@
|
|||||||
"""The tests for the person component."""
|
"""The tests for the person component."""
|
||||||
from homeassistant.components.person import ATTR_SOURCE, ATTR_USER_ID, DOMAIN
|
from homeassistant.components.person import ATTR_SOURCE, ATTR_USER_ID, DOMAIN
|
||||||
from homeassistant.const import (
|
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.core import CoreState, State
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from tests.common import mock_component, mock_restore_cache
|
from tests.common import mock_component, mock_restore_cache
|
||||||
|
|
||||||
DEVICE_TRACKER = 'device_tracker.test_tracker'
|
DEVICE_TRACKER = 'device_tracker.test_tracker'
|
||||||
DEVICE_TRACKER_2 = 'device_tracker.test_tracker_2'
|
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):
|
async def test_minimal_setup(hass):
|
||||||
"""Test minimal config with only name."""
|
"""Test minimal config with only name."""
|
||||||
config = {DOMAIN: {'id': '1234', 'name': 'test person'}}
|
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)
|
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."""
|
"""Test config with user id."""
|
||||||
user_id = hass_owner_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {
|
config = {
|
||||||
DOMAIN: {'id': '1234', 'name': 'test person', 'user_id': user_id}}
|
DOMAIN: {'id': '1234', 'name': 'test person', 'user_id': user_id}}
|
||||||
assert await async_setup_component(hass, DOMAIN, config)
|
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
|
assert state.attributes.get(ATTR_USER_ID) == user_id
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_invalid_user_id(hass):
|
async def test_valid_invalid_user_ids(hass, hass_admin_user):
|
||||||
"""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):
|
|
||||||
"""Test a person with valid user id and a person with invalid user id ."""
|
"""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: [
|
config = {DOMAIN: [
|
||||||
{'id': '1234', 'name': 'test valid user', 'user_id': user_id},
|
{'id': '1234', 'name': 'test valid user', 'user_id': user_id},
|
||||||
{'id': '5678', 'name': 'test bad user', 'user_id': 'bad_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
|
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."""
|
"""Test set up person with one device tracker."""
|
||||||
user_id = hass_owner_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
'device_trackers': DEVICE_TRACKER}}
|
'device_trackers': DEVICE_TRACKER}}
|
||||||
@ -98,6 +115,12 @@ async def test_setup_tracker(hass, hass_owner_user):
|
|||||||
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()
|
||||||
|
|
||||||
|
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')
|
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'
|
||||||
@ -120,9 +143,9 @@ async def test_setup_tracker(hass, hass_owner_user):
|
|||||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
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."""
|
"""Test set up person with two device trackers."""
|
||||||
user_id = hass_owner_user.id
|
user_id = hass_admin_user.id
|
||||||
config = {DOMAIN: {
|
config = {DOMAIN: {
|
||||||
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
'id': '1234', 'name': 'tracked person', 'user_id': user_id,
|
||||||
'device_trackers': [DEVICE_TRACKER, DEVICE_TRACKER_2]}}
|
'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_SOURCE) is None
|
||||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
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')
|
hass.states.async_set(DEVICE_TRACKER, 'home')
|
||||||
await hass.async_block_till_done()
|
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
|
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."""
|
"""Test that the state is restored for a person on startup."""
|
||||||
user_id = hass_owner_user.id
|
user_id = hass_admin_user.id
|
||||||
attrs = {
|
attrs = {
|
||||||
ATTR_ID: '1234', ATTR_LATITUDE: 10.12346, ATTR_LONGITUDE: 11.12346,
|
ATTR_ID: '1234', ATTR_LATITUDE: 10.12346, ATTR_LONGITUDE: 11.12346,
|
||||||
ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id}
|
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.
|
# 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_SOURCE) == 'person.tracked_person'
|
||||||
assert state.attributes.get(ATTR_USER_ID) == user_id
|
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