Area registry (#20435)
* First draft of area registry * Refactor based on input * Add tests for areas Add tests for updating device * Updating a device shouldn't require area * Fix Martins comment * Require admin * Save after deleting * Rename read to list_areas Fix device entry_dict Remove area id from device when deleting area * Fix tests
This commit is contained in:
parent
2c7060896b
commit
bd335e1ac1
9 changed files with 714 additions and 35 deletions
126
homeassistant/components/config/area_registry.py
Normal file
126
homeassistant/components/config/area_registry.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
"""HTTP views to interact with the area registry."""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.components.websocket_api.decorators import (
|
||||||
|
async_response, require_admin)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.area_registry import async_get_registry
|
||||||
|
|
||||||
|
|
||||||
|
DEPENDENCIES = ['websocket_api']
|
||||||
|
|
||||||
|
WS_TYPE_LIST = 'config/area_registry/list'
|
||||||
|
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_LIST,
|
||||||
|
})
|
||||||
|
|
||||||
|
WS_TYPE_CREATE = 'config/area_registry/create'
|
||||||
|
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_CREATE,
|
||||||
|
vol.Required('name'): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
WS_TYPE_DELETE = 'config/area_registry/delete'
|
||||||
|
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_DELETE,
|
||||||
|
vol.Required('area_id'): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
WS_TYPE_UPDATE = 'config/area_registry/update'
|
||||||
|
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_UPDATE,
|
||||||
|
vol.Required('area_id'): str,
|
||||||
|
vol.Required('name'): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass):
|
||||||
|
"""Enable the Area Registry views."""
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_LIST, websocket_list_areas, SCHEMA_WS_LIST
|
||||||
|
)
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_CREATE, websocket_create_area, SCHEMA_WS_CREATE
|
||||||
|
)
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_DELETE, websocket_delete_area, SCHEMA_WS_DELETE
|
||||||
|
)
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_UPDATE, websocket_update_area, SCHEMA_WS_UPDATE
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@async_response
|
||||||
|
async def websocket_list_areas(hass, connection, msg):
|
||||||
|
"""Handle list areas command."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
connection.send_message(websocket_api.result_message(
|
||||||
|
msg['id'], [{
|
||||||
|
'name': entry.name,
|
||||||
|
'area_id': entry.id,
|
||||||
|
} for entry in registry.async_list_areas()]
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
@async_response
|
||||||
|
async def websocket_create_area(hass, connection, msg):
|
||||||
|
"""Create area command."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
try:
|
||||||
|
entry = registry.async_create(msg['name'])
|
||||||
|
except ValueError as err:
|
||||||
|
connection.send_message(websocket_api.error_message(
|
||||||
|
msg['id'], 'invalid_info', str(err)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
connection.send_message(websocket_api.result_message(
|
||||||
|
msg['id'], _entry_dict(entry)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
@async_response
|
||||||
|
async def websocket_delete_area(hass, connection, msg):
|
||||||
|
"""Delete area command."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await registry.async_delete(msg['area_id'])
|
||||||
|
except KeyError:
|
||||||
|
connection.send_message(websocket_api.error_message(
|
||||||
|
msg['id'], 'invalid_info', "Area ID doesn't exist"
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
connection.send_message(websocket_api.result_message(
|
||||||
|
msg['id'], 'success'
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
@async_response
|
||||||
|
async def websocket_update_area(hass, connection, msg):
|
||||||
|
"""Handle update area websocket command."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry = registry.async_update(msg['area_id'], msg['name'])
|
||||||
|
except ValueError as err:
|
||||||
|
connection.send_message(websocket_api.error_message(
|
||||||
|
msg['id'], 'invalid_info', str(err)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
connection.send_message(websocket_api.result_message(
|
||||||
|
msg['id'], _entry_dict(entry)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _entry_dict(entry):
|
||||||
|
"""Convert entry to API format."""
|
||||||
|
return {
|
||||||
|
'area_id': entry.id,
|
||||||
|
'name': entry.name
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
"""HTTP views to interact with the device registry."""
|
"""HTTP views to interact with the device registry."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import async_get_registry
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.components.websocket_api.decorators import (
|
||||||
|
async_response, require_admin)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.device_registry import async_get_registry
|
||||||
|
|
||||||
DEPENDENCIES = ['websocket_api']
|
DEPENDENCIES = ['websocket_api']
|
||||||
|
|
||||||
|
@ -11,22 +14,53 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
vol.Required('type'): WS_TYPE_LIST,
|
vol.Required('type'): WS_TYPE_LIST,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
WS_TYPE_UPDATE = 'config/device_registry/update'
|
||||||
|
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||||
|
vol.Required('type'): WS_TYPE_UPDATE,
|
||||||
|
vol.Required('device_id'): str,
|
||||||
|
vol.Optional('area_id'): str,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass):
|
async def async_setup(hass):
|
||||||
"""Enable the Entity Registry views."""
|
"""Enable the Device Registry views."""
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_LIST, websocket_list_devices,
|
WS_TYPE_LIST, websocket_list_devices,
|
||||||
SCHEMA_WS_LIST
|
SCHEMA_WS_LIST
|
||||||
)
|
)
|
||||||
|
hass.components.websocket_api.async_register_command(
|
||||||
|
WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.async_response
|
@async_response
|
||||||
async def websocket_list_devices(hass, connection, msg):
|
async def websocket_list_devices(hass, connection, msg):
|
||||||
"""Handle list devices command."""
|
"""Handle list devices command."""
|
||||||
registry = await async_get_registry(hass)
|
registry = await async_get_registry(hass)
|
||||||
connection.send_message(websocket_api.result_message(
|
connection.send_message(websocket_api.result_message(
|
||||||
msg['id'], [{
|
msg['id'], [_entry_dict(entry) for entry in registry.devices.values()]
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
@async_response
|
||||||
|
async def websocket_update_device(hass, connection, msg):
|
||||||
|
"""Handle update area websocket command."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
|
||||||
|
entry = registry.async_update_device(
|
||||||
|
msg['device_id'], area_id=msg['area_id'])
|
||||||
|
|
||||||
|
connection.send_message(websocket_api.result_message(
|
||||||
|
msg['id'], _entry_dict(entry)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _entry_dict(entry):
|
||||||
|
"""Convert entry to API format."""
|
||||||
|
return {
|
||||||
'config_entries': list(entry.config_entries),
|
'config_entries': list(entry.config_entries),
|
||||||
'connections': list(entry.connections),
|
'connections': list(entry.connections),
|
||||||
'manufacturer': entry.manufacturer,
|
'manufacturer': entry.manufacturer,
|
||||||
|
@ -35,5 +69,5 @@ async def websocket_list_devices(hass, connection, msg):
|
||||||
'sw_version': entry.sw_version,
|
'sw_version': entry.sw_version,
|
||||||
'id': entry.id,
|
'id': entry.id,
|
||||||
'hub_device_id': entry.hub_device_id,
|
'hub_device_id': entry.hub_device_id,
|
||||||
} for entry in registry.devices.values()]
|
'area_id': entry.area_id,
|
||||||
))
|
}
|
||||||
|
|
139
homeassistant/helpers/area_registry.py
Normal file
139
homeassistant/helpers/area_registry.py
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
"""Provide a way to connect devices to one physical location."""
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_REGISTRY = 'area_registry'
|
||||||
|
|
||||||
|
STORAGE_KEY = 'core.area_registry'
|
||||||
|
STORAGE_VERSION = 1
|
||||||
|
SAVE_DELAY = 10
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True, frozen=True)
|
||||||
|
class AreaEntry:
|
||||||
|
"""Area Registry Entry."""
|
||||||
|
|
||||||
|
name = attr.ib(type=str, default=None)
|
||||||
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
|
||||||
|
|
||||||
|
class AreaRegistry:
|
||||||
|
"""Class to hold a registry of areas."""
|
||||||
|
|
||||||
|
def __init__(self, hass) -> None:
|
||||||
|
"""Initialize the area registry."""
|
||||||
|
self.hass = hass
|
||||||
|
self.areas = None
|
||||||
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_list_areas(self) -> List[AreaEntry]:
|
||||||
|
"""Get all areas."""
|
||||||
|
return self.areas.values()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_create(self, name: str) -> AreaEntry:
|
||||||
|
"""Create a new area."""
|
||||||
|
if self._async_is_registered(name):
|
||||||
|
raise ValueError('Name is already in use')
|
||||||
|
|
||||||
|
area = AreaEntry()
|
||||||
|
self.areas[area.id] = area
|
||||||
|
|
||||||
|
return self.async_update(area.id, name=name)
|
||||||
|
|
||||||
|
async def async_delete(self, area_id: str) -> None:
|
||||||
|
"""Delete area."""
|
||||||
|
device_registry = await \
|
||||||
|
self.hass.helpers.device_registry.async_get_registry()
|
||||||
|
device_registry.async_clear_area_id(area_id)
|
||||||
|
|
||||||
|
del self.areas[area_id]
|
||||||
|
|
||||||
|
self.async_schedule_save()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update(self, area_id: str, name: str) -> AreaEntry:
|
||||||
|
"""Update name of area."""
|
||||||
|
old = self.areas[area_id]
|
||||||
|
|
||||||
|
changes = {}
|
||||||
|
|
||||||
|
if name == old.name:
|
||||||
|
return old
|
||||||
|
|
||||||
|
if self._async_is_registered(name):
|
||||||
|
raise ValueError('Name is already in use')
|
||||||
|
else:
|
||||||
|
changes['name'] = name
|
||||||
|
|
||||||
|
new = self.areas[area_id] = attr.evolve(old, **changes)
|
||||||
|
self.async_schedule_save()
|
||||||
|
return new
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_is_registered(self, name) -> Optional[AreaEntry]:
|
||||||
|
"""Check if a name is currently registered."""
|
||||||
|
for area in self.areas.values():
|
||||||
|
if name == area.name:
|
||||||
|
return area
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_load(self) -> None:
|
||||||
|
"""Load the area registry."""
|
||||||
|
data = await self._store.async_load()
|
||||||
|
|
||||||
|
areas = OrderedDict()
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
for area in data['areas']:
|
||||||
|
areas[area['id']] = AreaEntry(
|
||||||
|
name=area['name'],
|
||||||
|
id=area['id']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.areas = areas
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data['areas'] = [
|
||||||
|
{
|
||||||
|
'name': entry.name,
|
||||||
|
'id': entry.id,
|
||||||
|
} for entry in self.areas.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_get_registry(hass) -> AreaRegistry:
|
||||||
|
"""Return area registry instance."""
|
||||||
|
task = hass.data.get(DATA_REGISTRY)
|
||||||
|
|
||||||
|
if task is None:
|
||||||
|
async def _load_reg():
|
||||||
|
registry = AreaRegistry(hass)
|
||||||
|
await registry.async_load()
|
||||||
|
return registry
|
||||||
|
|
||||||
|
task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg())
|
||||||
|
|
||||||
|
return await task
|
|
@ -36,6 +36,7 @@ class DeviceEntry:
|
||||||
name = attr.ib(type=str, default=None)
|
name = attr.ib(type=str, default=None)
|
||||||
sw_version = attr.ib(type=str, default=None)
|
sw_version = attr.ib(type=str, default=None)
|
||||||
hub_device_id = attr.ib(type=str, default=None)
|
hub_device_id = attr.ib(type=str, default=None)
|
||||||
|
area_id = attr.ib(type=str, default=None)
|
||||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,9 +120,14 @@ class DeviceRegistry:
|
||||||
manufacturer=manufacturer,
|
manufacturer=manufacturer,
|
||||||
model=model,
|
model=model,
|
||||||
name=name,
|
name=name,
|
||||||
sw_version=sw_version,
|
sw_version=sw_version
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_update_device(self, device_id, *, area_id=_UNDEF):
|
||||||
|
"""Update properties of a device."""
|
||||||
|
return self._async_update_device(device_id, area_id=area_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF,
|
def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF,
|
||||||
remove_config_entry_id=_UNDEF,
|
remove_config_entry_id=_UNDEF,
|
||||||
|
@ -131,7 +137,8 @@ class DeviceRegistry:
|
||||||
model=_UNDEF,
|
model=_UNDEF,
|
||||||
name=_UNDEF,
|
name=_UNDEF,
|
||||||
sw_version=_UNDEF,
|
sw_version=_UNDEF,
|
||||||
hub_device_id=_UNDEF):
|
hub_device_id=_UNDEF,
|
||||||
|
area_id=_UNDEF):
|
||||||
"""Update device attributes."""
|
"""Update device attributes."""
|
||||||
old = self.devices[device_id]
|
old = self.devices[device_id]
|
||||||
|
|
||||||
|
@ -169,6 +176,9 @@ class DeviceRegistry:
|
||||||
if value is not _UNDEF and value != getattr(old, attr_name):
|
if value is not _UNDEF and value != getattr(old, attr_name):
|
||||||
changes[attr_name] = value
|
changes[attr_name] = value
|
||||||
|
|
||||||
|
if (area_id is not _UNDEF and area_id != old.area_id):
|
||||||
|
changes['area_id'] = area_id
|
||||||
|
|
||||||
if not changes:
|
if not changes:
|
||||||
return old
|
return old
|
||||||
|
|
||||||
|
@ -197,6 +207,8 @@ class DeviceRegistry:
|
||||||
id=device['id'],
|
id=device['id'],
|
||||||
# Introduced in 0.79
|
# Introduced in 0.79
|
||||||
hub_device_id=device.get('hub_device_id'),
|
hub_device_id=device.get('hub_device_id'),
|
||||||
|
# Introduced in 0.87
|
||||||
|
area_id=device.get('area_id')
|
||||||
)
|
)
|
||||||
|
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
|
@ -222,6 +234,7 @@ class DeviceRegistry:
|
||||||
'sw_version': entry.sw_version,
|
'sw_version': entry.sw_version,
|
||||||
'id': entry.id,
|
'id': entry.id,
|
||||||
'hub_device_id': entry.hub_device_id,
|
'hub_device_id': entry.hub_device_id,
|
||||||
|
'area_id': entry.area_id
|
||||||
} for entry in self.devices.values()
|
} for entry in self.devices.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -235,6 +248,13 @@ class DeviceRegistry:
|
||||||
self._async_update_device(
|
self._async_update_device(
|
||||||
dev_id, remove_config_entry_id=config_entry_id)
|
dev_id, remove_config_entry_id=config_entry_id)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_clear_area_id(self, area_id: str) -> None:
|
||||||
|
"""Clear area id from registry entries."""
|
||||||
|
for dev_id, device in self.devices.items():
|
||||||
|
if area_id == device.area_id:
|
||||||
|
self._async_update_device(dev_id, area_id=None)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def async_get_registry(hass) -> DeviceRegistry:
|
async def async_get_registry(hass) -> DeviceRegistry:
|
||||||
|
|
|
@ -1,35 +1,37 @@
|
||||||
"""Test the helper method for writing tests."""
|
"""Test the helper method for writing tests."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import OrderedDict
|
|
||||||
from datetime import timedelta
|
|
||||||
import functools as ft
|
import functools as ft
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from unittest.mock import patch, MagicMock, Mock
|
|
||||||
from io import StringIO
|
|
||||||
import logging
|
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
from homeassistant import auth, core as ha, config_entries
|
from collections import OrderedDict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import timedelta
|
||||||
|
from io import StringIO
|
||||||
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
|
import homeassistant.util.dt as date_util
|
||||||
|
import homeassistant.util.yaml as yaml
|
||||||
|
|
||||||
|
from homeassistant import auth, config_entries, core as ha
|
||||||
from homeassistant.auth import (
|
from homeassistant.auth import (
|
||||||
models as auth_models, auth_store, providers as auth_providers,
|
models as auth_models, auth_store, providers as auth_providers,
|
||||||
permissions as auth_permissions)
|
permissions as auth_permissions)
|
||||||
from homeassistant.auth.permissions import system_policies
|
from homeassistant.auth.permissions import system_policies
|
||||||
from homeassistant.setup import setup_component, async_setup_component
|
|
||||||
from homeassistant.config import async_process_component_config
|
|
||||||
from homeassistant.helpers import (
|
|
||||||
intent, entity, restore_state, entity_registry,
|
|
||||||
entity_platform, storage, device_registry)
|
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
|
||||||
import homeassistant.util.dt as date_util
|
|
||||||
import homeassistant.util.yaml as yaml
|
|
||||||
from homeassistant.const import (
|
|
||||||
STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED,
|
|
||||||
EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE,
|
|
||||||
ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE)
|
|
||||||
from homeassistant.components import mqtt, recorder
|
from homeassistant.components import mqtt, recorder
|
||||||
|
from homeassistant.config import async_process_component_config
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DISCOVERED, ATTR_SERVICE, DEVICE_DEFAULT_NAME,
|
||||||
|
EVENT_HOMEASSISTANT_CLOSE, EVENT_PLATFORM_DISCOVERED, EVENT_STATE_CHANGED,
|
||||||
|
EVENT_TIME_CHANGED, SERVER_PORT, STATE_ON, STATE_OFF)
|
||||||
|
from homeassistant.helpers import (
|
||||||
|
area_registry, device_registry, entity, entity_platform, entity_registry,
|
||||||
|
intent, restore_state, storage)
|
||||||
|
from homeassistant.setup import async_setup_component, setup_component
|
||||||
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
from homeassistant.util.async_ import (
|
from homeassistant.util.async_ import (
|
||||||
run_callback_threadsafe, run_coroutine_threadsafe)
|
run_callback_threadsafe, run_coroutine_threadsafe)
|
||||||
|
|
||||||
|
@ -333,6 +335,19 @@ def mock_registry(hass, mock_entries=None):
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def mock_area_registry(hass, mock_entries=None):
|
||||||
|
"""Mock the Area Registry."""
|
||||||
|
registry = area_registry.AreaRegistry(hass)
|
||||||
|
registry.areas = mock_entries or OrderedDict()
|
||||||
|
|
||||||
|
async def _get_reg():
|
||||||
|
return registry
|
||||||
|
|
||||||
|
hass.data[area_registry.DATA_REGISTRY] = \
|
||||||
|
hass.loop.create_task(_get_reg())
|
||||||
|
return registry
|
||||||
|
|
||||||
|
|
||||||
def mock_device_registry(hass, mock_entries=None):
|
def mock_device_registry(hass, mock_entries=None):
|
||||||
"""Mock the Device Registry."""
|
"""Mock the Device Registry."""
|
||||||
registry = device_registry.DeviceRegistry(hass)
|
registry = device_registry.DeviceRegistry(hass)
|
||||||
|
|
155
tests/components/config/test_area_registry.py
Normal file
155
tests/components/config/test_area_registry.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
"""Test area_registry API."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.config import area_registry
|
||||||
|
from tests.common import mock_area_registry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(hass, hass_ws_client):
|
||||||
|
"""Fixture that can interact with the config manager API."""
|
||||||
|
hass.loop.run_until_complete(area_registry.async_setup(hass))
|
||||||
|
yield hass.loop.run_until_complete(hass_ws_client(hass))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def registry(hass):
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_area_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_areas(hass, client, registry):
|
||||||
|
"""Test list entries."""
|
||||||
|
registry.async_create('mock 1')
|
||||||
|
registry.async_create('mock 2')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'type': 'config/area_registry/list',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert len(msg['result']) == len(registry.areas)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_area(hass, client, registry):
|
||||||
|
"""Test create entry."""
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'name': "mock",
|
||||||
|
'type': 'config/area_registry/create',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert 'mock' in msg['result']['name']
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_area_with_name_already_in_use(hass, client, registry):
|
||||||
|
"""Test create entry that should fail."""
|
||||||
|
registry.async_create('mock')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'name': "mock",
|
||||||
|
'type': 'config/area_registry/create',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert not msg['success']
|
||||||
|
assert msg['error']['code'] == 'invalid_info'
|
||||||
|
assert msg['error']['message'] == "Name is already in use"
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_area(hass, client, registry):
|
||||||
|
"""Test delete entry."""
|
||||||
|
area = registry.async_create('mock')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'area_id': area.id,
|
||||||
|
'type': 'config/area_registry/delete',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg['success']
|
||||||
|
assert not registry.areas
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_non_existing_area(hass, client, registry):
|
||||||
|
"""Test delete entry that should fail."""
|
||||||
|
registry.async_create('mock')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'area_id': '',
|
||||||
|
'type': 'config/area_registry/delete',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert not msg['success']
|
||||||
|
assert msg['error']['code'] == 'invalid_info'
|
||||||
|
assert msg['error']['message'] == "Area ID doesn't exist"
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area(hass, client, registry):
|
||||||
|
"""Test update entry."""
|
||||||
|
area = registry.async_create('mock 1')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'area_id': area.id,
|
||||||
|
'name': "mock 2",
|
||||||
|
'type': 'config/area_registry/update',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg['result']['area_id'] == area.id
|
||||||
|
assert msg['result']['name'] == 'mock 2'
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area_with_same_name(hass, client, registry):
|
||||||
|
"""Test update entry."""
|
||||||
|
area = registry.async_create('mock 1')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'area_id': area.id,
|
||||||
|
'name': "mock 1",
|
||||||
|
'type': 'config/area_registry/update',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg['result']['area_id'] == area.id
|
||||||
|
assert msg['result']['name'] == 'mock 1'
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area_with_name_already_in_use(hass, client, registry):
|
||||||
|
"""Test update entry."""
|
||||||
|
area = registry.async_create('mock 1')
|
||||||
|
registry.async_create('mock 2')
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'area_id': area.id,
|
||||||
|
'name': "mock 2",
|
||||||
|
'type': 'config/area_registry/update',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert not msg['success']
|
||||||
|
assert msg['error']['code'] == 'invalid_info'
|
||||||
|
assert msg['error']['message'] == "Name is already in use"
|
||||||
|
assert len(registry.areas) == 2
|
|
@ -1,4 +1,4 @@
|
||||||
"""Test entity_registry API."""
|
"""Test device_registry API."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.config import device_registry
|
from homeassistant.components.config import device_registry
|
||||||
|
@ -48,6 +48,7 @@ async def test_list_devices(hass, client, registry):
|
||||||
'name': None,
|
'name': None,
|
||||||
'sw_version': None,
|
'sw_version': None,
|
||||||
'hub_device_id': None,
|
'hub_device_id': None,
|
||||||
|
'area_id': None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'config_entries': ['1234'],
|
'config_entries': ['1234'],
|
||||||
|
@ -57,5 +58,30 @@ async def test_list_devices(hass, client, registry):
|
||||||
'name': None,
|
'name': None,
|
||||||
'sw_version': None,
|
'sw_version': None,
|
||||||
'hub_device_id': dev1,
|
'hub_device_id': dev1,
|
||||||
|
'area_id': None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_device(hass, client, registry):
|
||||||
|
"""Test update entry."""
|
||||||
|
device = registry.async_get_or_create(
|
||||||
|
config_entry_id='1234',
|
||||||
|
connections={('ethernet', '12:34:56:78:90:AB:CD:EF')},
|
||||||
|
identifiers={('bridgeid', '0123')},
|
||||||
|
manufacturer='manufacturer', model='model')
|
||||||
|
|
||||||
|
assert not device.area_id
|
||||||
|
|
||||||
|
await client.send_json({
|
||||||
|
'id': 1,
|
||||||
|
'device_id': device.id,
|
||||||
|
'area_id': '12345A',
|
||||||
|
'type': 'config/device_registry/update',
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert msg['result']['id'] == device.id
|
||||||
|
assert msg['result']['area_id'] == '12345A'
|
||||||
|
assert len(registry.devices) == 1
|
||||||
|
|
127
tests/helpers/test_area_registry.py
Normal file
127
tests/helpers/test_area_registry.py
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
"""Tests for the Area Registry."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.helpers import area_registry
|
||||||
|
from tests.common import mock_area_registry, flush_store
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def registry(hass):
|
||||||
|
"""Return an empty, loaded, registry."""
|
||||||
|
return mock_area_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_areas(registry):
|
||||||
|
"""Make sure that we can read areas."""
|
||||||
|
registry.async_create('mock')
|
||||||
|
|
||||||
|
areas = registry.async_list_areas()
|
||||||
|
|
||||||
|
assert len(areas) == len(registry.areas)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_area(registry):
|
||||||
|
"""Make sure that we can create an area."""
|
||||||
|
area = registry.async_create('mock')
|
||||||
|
|
||||||
|
assert area.name == 'mock'
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_area_with_name_already_in_use(registry):
|
||||||
|
"""Make sure that we can't create an area with a name already in use."""
|
||||||
|
area1 = registry.async_create('mock')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e_info:
|
||||||
|
area2 = registry.async_create('mock')
|
||||||
|
assert area1 != area2
|
||||||
|
assert e_info == "Name is already in use"
|
||||||
|
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_area(registry):
|
||||||
|
"""Make sure that we can delete an area."""
|
||||||
|
area = registry.async_create('mock')
|
||||||
|
|
||||||
|
await registry.async_delete(area.id)
|
||||||
|
|
||||||
|
assert not registry.areas
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_non_existing_area(registry):
|
||||||
|
"""Make sure that we can't delete an area that doesn't exist."""
|
||||||
|
registry.async_create('mock')
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
await registry.async_delete('')
|
||||||
|
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area(registry):
|
||||||
|
"""Make sure that we can read areas."""
|
||||||
|
area = registry.async_create('mock')
|
||||||
|
|
||||||
|
updated_area = registry.async_update(area.id, name='mock1')
|
||||||
|
|
||||||
|
assert updated_area != area
|
||||||
|
assert updated_area.name == 'mock1'
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area_with_same_name(registry):
|
||||||
|
"""Make sure that we can reapply the same name to the area."""
|
||||||
|
area = registry.async_create('mock')
|
||||||
|
|
||||||
|
updated_area = registry.async_update(area.id, name='mock')
|
||||||
|
|
||||||
|
assert updated_area == area
|
||||||
|
assert len(registry.areas) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_area_with_name_already_in_use(registry):
|
||||||
|
"""Make sure that we can't update an area with a name already in use."""
|
||||||
|
area1 = registry.async_create('mock1')
|
||||||
|
area2 = registry.async_create('mock2')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as e_info:
|
||||||
|
registry.async_update(area1.id, name='mock2')
|
||||||
|
assert e_info == "Name is already in use"
|
||||||
|
|
||||||
|
assert area1.name == 'mock1'
|
||||||
|
assert area2.name == 'mock2'
|
||||||
|
assert len(registry.areas) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_area(hass, registry):
|
||||||
|
"""Make sure that we can load/save data correctly."""
|
||||||
|
registry.async_create('mock1')
|
||||||
|
registry.async_create('mock2')
|
||||||
|
|
||||||
|
assert len(registry.areas) == 2
|
||||||
|
|
||||||
|
registry2 = area_registry.AreaRegistry(hass)
|
||||||
|
await flush_store(registry._store)
|
||||||
|
await registry2.async_load()
|
||||||
|
|
||||||
|
assert list(registry.areas) == list(registry2.areas)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_area_from_storage(hass, hass_storage):
|
||||||
|
"""Test loading stored areas on start."""
|
||||||
|
hass_storage[area_registry.STORAGE_KEY] = {
|
||||||
|
'version': area_registry.STORAGE_VERSION,
|
||||||
|
'data': {
|
||||||
|
'areas': [
|
||||||
|
{
|
||||||
|
'id': '12345A',
|
||||||
|
'name': 'mock'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry = await area_registry.async_get_registry(hass)
|
||||||
|
|
||||||
|
assert len(registry.areas) == 1
|
|
@ -133,6 +133,7 @@ async def test_loading_from_storage(hass, hass_storage):
|
||||||
'model': 'model',
|
'model': 'model',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'sw_version': 'version',
|
'sw_version': 'version',
|
||||||
|
'area_id': '12345A'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -146,6 +147,7 @@ async def test_loading_from_storage(hass, hass_storage):
|
||||||
identifiers={('serial', '12:34:56:AB:CD:EF')},
|
identifiers={('serial', '12:34:56:AB:CD:EF')},
|
||||||
manufacturer='manufacturer', model='model')
|
manufacturer='manufacturer', model='model')
|
||||||
assert entry.id == 'abcdefghijklm'
|
assert entry.id == 'abcdefghijklm'
|
||||||
|
assert entry.area_id == '12345A'
|
||||||
assert isinstance(entry.config_entries, set)
|
assert isinstance(entry.config_entries, set)
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,6 +188,25 @@ async def test_removing_config_entries(registry):
|
||||||
assert entry3.config_entries == set()
|
assert entry3.config_entries == set()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_removing_area_id(registry):
|
||||||
|
"""Make sure we can clear area id."""
|
||||||
|
entry = registry.async_get_or_create(
|
||||||
|
config_entry_id='123',
|
||||||
|
connections={
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF')
|
||||||
|
},
|
||||||
|
identifiers={('bridgeid', '0123')},
|
||||||
|
manufacturer='manufacturer', model='model')
|
||||||
|
|
||||||
|
entry_w_area = registry.async_update_device(entry.id, area_id='12345A')
|
||||||
|
|
||||||
|
registry.async_clear_area_id('12345A')
|
||||||
|
entry_wo_area = registry.async_get_device({('bridgeid', '0123')}, set())
|
||||||
|
|
||||||
|
assert not entry_wo_area.area_id
|
||||||
|
assert entry_w_area != entry_wo_area
|
||||||
|
|
||||||
|
|
||||||
async def test_specifying_hub_device_create(registry):
|
async def test_specifying_hub_device_create(registry):
|
||||||
"""Test specifying a hub and updating."""
|
"""Test specifying a hub and updating."""
|
||||||
hub = registry.async_get_or_create(
|
hub = registry.async_get_or_create(
|
||||||
|
@ -328,3 +349,19 @@ async def test_format_mac(registry):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert list(invalid_mac_entry.connections)[0][1] == invalid
|
assert list(invalid_mac_entry.connections)[0][1] == invalid
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update(registry):
|
||||||
|
"""Verify that we can update area_id of a device."""
|
||||||
|
entry = registry.async_get_or_create(
|
||||||
|
config_entry_id='1234',
|
||||||
|
connections={
|
||||||
|
(device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF')
|
||||||
|
})
|
||||||
|
|
||||||
|
assert not entry.area_id
|
||||||
|
|
||||||
|
updated_entry = registry.async_update_device(entry.id, area_id='12345A')
|
||||||
|
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.area_id == '12345A'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue