Add image integration (#38969)
parent
111c2006c8
commit
24a16ff8fe
|
@ -195,6 +195,7 @@ homeassistant/components/iammeter/* @lewei50
|
|||
homeassistant/components/iaqualink/* @flz
|
||||
homeassistant/components/icloud/* @Quentame
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/image/* @home-assistant/core
|
||||
homeassistant/components/incomfort/* @zxdavb
|
||||
homeassistant/components/influxdb/* @fabaff @mdegat01
|
||||
homeassistant/components/input_boolean/* @home-assistant/core
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "doods",
|
||||
"name": "DOODS - Distributed Outside Object Detection Service",
|
||||
"documentation": "https://www.home-assistant.io/integrations/doods",
|
||||
"requirements": ["pydoods==1.0.2", "pillow==7.1.2"],
|
||||
"requirements": ["pydoods==1.0.2", "pillow==7.2.0"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
"""The Picture integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
import pathlib
|
||||
import secrets
|
||||
import shutil
|
||||
import typing
|
||||
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from aiohttp import hdrs, web
|
||||
from aiohttp.web_request import FileField
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import collection
|
||||
from homeassistant.helpers.storage import Store
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
VALID_SIZES = {256, 512}
|
||||
MAX_SIZE = 1024 * 1024 * 10
|
||||
|
||||
CREATE_FIELDS = {
|
||||
vol.Required("file"): FileField,
|
||||
}
|
||||
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional("name"): vol.All(str, vol.Length(min=1)),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Image integration."""
|
||||
image_dir = pathlib.Path(hass.config.path(DOMAIN))
|
||||
hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir)
|
||||
await storage_collection.async_load()
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS,
|
||||
).async_setup(hass, create_create=False)
|
||||
|
||||
hass.http.register_view(ImageUploadView)
|
||||
hass.http.register_view(ImageServeView(image_dir, storage_collection))
|
||||
return True
|
||||
|
||||
|
||||
class ImageStorageCollection(collection.StorageCollection):
|
||||
"""Image collection stored in storage."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, image_dir: pathlib.Path) -> None:
|
||||
"""Initialize media storage collection."""
|
||||
super().__init__(
|
||||
Store(hass, STORAGE_VERSION, STORAGE_KEY),
|
||||
logging.getLogger(f"{__name__}.storage_collection"),
|
||||
)
|
||||
self.async_add_listener(self._change_listener)
|
||||
self.image_dir = image_dir
|
||||
|
||||
async def _process_create_data(self, data: typing.Dict) -> typing.Dict:
|
||||
"""Validate the config is valid."""
|
||||
data = self.CREATE_SCHEMA(dict(data))
|
||||
uploaded_file: FileField = data["file"]
|
||||
|
||||
if not uploaded_file.content_type.startswith("image/"):
|
||||
raise vol.Invalid("Only images are allowed")
|
||||
|
||||
data[CONF_ID] = secrets.token_hex(16)
|
||||
data["filesize"] = await self.hass.async_add_executor_job(self._move_data, data)
|
||||
|
||||
data["content_type"] = uploaded_file.content_type
|
||||
data["name"] = uploaded_file.filename
|
||||
data["uploaded_at"] = dt_util.utcnow().isoformat()
|
||||
|
||||
return data
|
||||
|
||||
def _move_data(self, data):
|
||||
"""Move data."""
|
||||
uploaded_file: FileField = data.pop("file")
|
||||
|
||||
# Verify we can read the image
|
||||
try:
|
||||
image = Image.open(uploaded_file.file)
|
||||
except UnidentifiedImageError:
|
||||
raise vol.Invalid("Unable to identify image file")
|
||||
|
||||
# Reset content
|
||||
uploaded_file.file.seek(0)
|
||||
|
||||
media_folder: pathlib.Path = (self.image_dir / data[CONF_ID])
|
||||
media_folder.mkdir(parents=True)
|
||||
|
||||
media_file = media_folder / "original"
|
||||
|
||||
# Raises if path is no longer relative to the media dir
|
||||
media_file.relative_to(media_folder)
|
||||
|
||||
_LOGGER.debug("Storing file %s", media_file)
|
||||
|
||||
with media_file.open("wb") as target:
|
||||
shutil.copyfileobj(uploaded_file.file, target)
|
||||
|
||||
image.close()
|
||||
|
||||
return media_file.stat().st_size
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: typing.Dict) -> str:
|
||||
"""Suggest an ID based on the config."""
|
||||
return info[CONF_ID]
|
||||
|
||||
async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict:
|
||||
"""Return a new updated data object."""
|
||||
return {**data, **self.UPDATE_SCHEMA(update_data)}
|
||||
|
||||
async def _change_listener(self, change_type, item_id, data):
|
||||
"""Handle change."""
|
||||
if change_type != collection.CHANGE_REMOVED:
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(shutil.rmtree, self.image_dir / item_id)
|
||||
|
||||
|
||||
class ImageUploadView(HomeAssistantView):
|
||||
"""View to upload images."""
|
||||
|
||||
url = "/api/image/upload"
|
||||
name = "api:image:upload"
|
||||
|
||||
async def post(self, request):
|
||||
"""Handle upload."""
|
||||
# Increase max payload
|
||||
request._client_max_size = MAX_SIZE # pylint: disable=protected-access
|
||||
|
||||
data = await request.post()
|
||||
item = await request.app["hass"].data[DOMAIN].async_create_item(data)
|
||||
return self.json(item)
|
||||
|
||||
|
||||
class ImageServeView(HomeAssistantView):
|
||||
"""View to download images."""
|
||||
|
||||
url = "/api/image/serve/{image_id}/{filename}"
|
||||
name = "api:image:serve"
|
||||
requires_auth = False
|
||||
|
||||
def __init__(
|
||||
self, image_folder: pathlib.Path, image_collection: ImageStorageCollection
|
||||
):
|
||||
"""Initialize image serve view."""
|
||||
self.transform_lock = asyncio.Lock()
|
||||
self.image_folder = image_folder
|
||||
self.image_collection = image_collection
|
||||
|
||||
async def get(self, request: web.Request, image_id: str, filename: str):
|
||||
"""Serve image."""
|
||||
image_size = filename.split("-", 1)[0]
|
||||
try:
|
||||
parts = image_size.split("x", 1)
|
||||
width = int(parts[0])
|
||||
height = int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
raise web.HTTPBadRequest
|
||||
|
||||
if not width or width != height or width not in VALID_SIZES:
|
||||
raise web.HTTPBadRequest
|
||||
|
||||
image_info = self.image_collection.data.get(image_id)
|
||||
|
||||
if image_info is None:
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
hass = request.app["hass"]
|
||||
target_file = self.image_folder / image_id / f"{width}x{height}"
|
||||
|
||||
if not target_file.is_file():
|
||||
async with self.transform_lock:
|
||||
# Another check in case another request already finished it while waiting
|
||||
if not target_file.is_file():
|
||||
await hass.async_add_executor_job(
|
||||
_generate_thumbnail,
|
||||
self.image_folder / image_id / "original",
|
||||
image_info["content_type"],
|
||||
target_file,
|
||||
(width, height),
|
||||
)
|
||||
|
||||
return web.FileResponse(
|
||||
target_file, headers={hdrs.CONTENT_TYPE: image_info["content_type"]}
|
||||
)
|
||||
|
||||
|
||||
def _generate_thumbnail(original_path, content_type, target_path, target_size):
|
||||
"""Generate a size."""
|
||||
image = ImageOps.exif_transpose(Image.open(original_path))
|
||||
image.thumbnail(target_size)
|
||||
image.save(target_path, format=content_type.split("/", 1)[1])
|
|
@ -0,0 +1,3 @@
|
|||
"""Constants for the Image integration."""
|
||||
|
||||
DOMAIN = "image"
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"domain": "image",
|
||||
"name": "Image",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/image",
|
||||
"requirements": ["pillow==7.2.0"],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/core"]
|
||||
}
|
|
@ -57,6 +57,7 @@ ATTR_USER_ID = "user_id"
|
|||
|
||||
CONF_DEVICE_TRACKERS = "device_trackers"
|
||||
CONF_USER_ID = "user_id"
|
||||
CONF_PICTURE = "picture"
|
||||
|
||||
DOMAIN = "person"
|
||||
|
||||
|
@ -73,6 +74,7 @@ PERSON_SCHEMA = vol.Schema(
|
|||
vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All(
|
||||
cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
|
||||
),
|
||||
vol.Optional(CONF_PICTURE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -129,6 +131,7 @@ CREATE_FIELDS = {
|
|||
vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All(
|
||||
cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
|
||||
),
|
||||
vol.Optional(CONF_PICTURE): vol.Any(str, None),
|
||||
}
|
||||
|
||||
|
||||
|
@ -138,6 +141,7 @@ UPDATE_FIELDS = {
|
|||
vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All(
|
||||
cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
|
||||
),
|
||||
vol.Optional(CONF_PICTURE): vol.Any(str, None),
|
||||
}
|
||||
|
||||
|
||||
|
@ -372,6 +376,11 @@ class Person(RestoreEntity):
|
|||
"""Return the name of the entity."""
|
||||
return self._config[CONF_NAME]
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> Optional[str]:
|
||||
"""Return entity picture."""
|
||||
return self._config.get(CONF_PICTURE)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"domain": "person",
|
||||
"name": "Person",
|
||||
"documentation": "https://www.home-assistant.io/integrations/person",
|
||||
"dependencies": ["image"],
|
||||
"after_dependencies": ["device_tracker"],
|
||||
"codeowners": [],
|
||||
"quality_scale": "internal"
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "proxy",
|
||||
"name": "Camera Proxy",
|
||||
"documentation": "https://www.home-assistant.io/integrations/proxy",
|
||||
"requirements": ["pillow==7.1.2"],
|
||||
"requirements": ["pillow==7.2.0"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "qrcode",
|
||||
"name": "QR Code",
|
||||
"documentation": "https://www.home-assistant.io/integrations/qrcode",
|
||||
"requirements": ["pillow==7.1.2", "pyzbar==0.1.7"],
|
||||
"requirements": ["pillow==7.2.0", "pyzbar==0.1.7"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "seven_segments",
|
||||
"name": "Seven Segments OCR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
|
||||
"requirements": ["pillow==7.1.2"],
|
||||
"requirements": ["pillow==7.2.0"],
|
||||
"codeowners": ["@fabaff"]
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "sighthound",
|
||||
"name": "Sighthound",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sighthound",
|
||||
"requirements": ["pillow==7.1.2", "simplehound==0.3"],
|
||||
"requirements": ["pillow==7.2.0", "simplehound==0.3"],
|
||||
"codeowners": ["@robmarkcole"]
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"pycocotools==2.0.1",
|
||||
"numpy==1.19.1",
|
||||
"protobuf==3.12.2",
|
||||
"pillow==7.1.2"
|
||||
"pillow==7.2.0"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -353,7 +353,13 @@ class StorageCollectionWebsocket:
|
|||
return f"{self.model_name}_id"
|
||||
|
||||
@callback
|
||||
def async_setup(self, hass: HomeAssistant, *, create_list: bool = True) -> None:
|
||||
def async_setup(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
create_list: bool = True,
|
||||
create_create: bool = True,
|
||||
) -> None:
|
||||
"""Set up the websocket commands."""
|
||||
if create_list:
|
||||
websocket_api.async_register_command(
|
||||
|
@ -365,19 +371,20 @@ class StorageCollectionWebsocket:
|
|||
),
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(
|
||||
hass,
|
||||
f"{self.api_prefix}/create",
|
||||
websocket_api.require_admin(
|
||||
websocket_api.async_response(self.ws_create_item)
|
||||
),
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{
|
||||
**self.create_schema,
|
||||
vol.Required("type"): f"{self.api_prefix}/create",
|
||||
}
|
||||
),
|
||||
)
|
||||
if create_create:
|
||||
websocket_api.async_register_command(
|
||||
hass,
|
||||
f"{self.api_prefix}/create",
|
||||
websocket_api.require_admin(
|
||||
websocket_api.async_response(self.ws_create_item)
|
||||
),
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{
|
||||
**self.create_schema,
|
||||
vol.Required("type"): f"{self.api_prefix}/create",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(
|
||||
hass,
|
||||
|
|
|
@ -18,6 +18,7 @@ importlib-metadata==1.6.0;python_version<'3.8'
|
|||
jinja2>=2.11.1
|
||||
netdisco==2.8.2
|
||||
paho-mqtt==1.5.0
|
||||
pillow==7.2.0
|
||||
pip>=8.0.3
|
||||
python-slugify==4.0.1
|
||||
pytz>=2020.1
|
||||
|
|
|
@ -1081,12 +1081,13 @@ piglow==1.2.4
|
|||
pilight==0.1.1
|
||||
|
||||
# homeassistant.components.doods
|
||||
# homeassistant.components.image
|
||||
# homeassistant.components.proxy
|
||||
# homeassistant.components.qrcode
|
||||
# homeassistant.components.seven_segments
|
||||
# homeassistant.components.sighthound
|
||||
# homeassistant.components.tensorflow
|
||||
pillow==7.1.2
|
||||
pillow==7.2.0
|
||||
|
||||
# homeassistant.components.dominos
|
||||
pizzapi==0.0.3
|
||||
|
|
|
@ -501,12 +501,13 @@ pexpect==4.6.0
|
|||
pilight==0.1.1
|
||||
|
||||
# homeassistant.components.doods
|
||||
# homeassistant.components.image
|
||||
# homeassistant.components.proxy
|
||||
# homeassistant.components.qrcode
|
||||
# homeassistant.components.seven_segments
|
||||
# homeassistant.components.sighthound
|
||||
# homeassistant.components.tensorflow
|
||||
pillow==7.1.2
|
||||
pillow==7.2.0
|
||||
|
||||
# homeassistant.components.plex
|
||||
plexapi==4.0.0
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Image integration."""
|
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -0,0 +1,76 @@
|
|||
"""Test that we can upload images."""
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from aiohttp import ClientSession, ClientWebSocketResponse
|
||||
|
||||
from homeassistant.components.websocket_api import const as ws_const
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as util_dt
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
async def test_upload_image(hass, hass_client, hass_ws_client):
|
||||
"""Test we can upload an image."""
|
||||
now = util_dt.utcnow()
|
||||
test_image = pathlib.Path(__file__).parent / "logo.png"
|
||||
|
||||
with tempfile.TemporaryDirectory() as tempdir, patch.object(
|
||||
hass.config, "path", return_value=tempdir
|
||||
), patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
assert await async_setup_component(hass, "image", {})
|
||||
ws_client: ClientWebSocketResponse = await hass_ws_client()
|
||||
client: ClientSession = await hass_client()
|
||||
|
||||
with test_image.open("rb") as fp:
|
||||
res = await client.post("/api/image/upload", data={"file": fp})
|
||||
|
||||
assert res.status == 200
|
||||
|
||||
item = await res.json()
|
||||
|
||||
assert item["content_type"] == "image/png"
|
||||
assert item["filesize"] == 38847
|
||||
assert item["name"] == "logo.png"
|
||||
assert item["uploaded_at"] == now.isoformat()
|
||||
|
||||
tempdir = pathlib.Path(tempdir)
|
||||
item_folder: pathlib.Path = tempdir / item["id"]
|
||||
assert (item_folder / "original").read_bytes() == test_image.read_bytes()
|
||||
|
||||
# fetch non-existing image
|
||||
res = await client.get("/api/image/serve/non-existing/256x256")
|
||||
assert res.status == 404
|
||||
|
||||
# fetch invalid sizes
|
||||
for inv_size in ("256", "256x25A", "100x100", "25Ax256"):
|
||||
res = await client.get(f"/api/image/serve/{item['id']}/{inv_size}")
|
||||
assert res.status == 400
|
||||
|
||||
# fetch resized version
|
||||
res = await client.get(f"/api/image/serve/{item['id']}/256x256")
|
||||
assert res.status == 200
|
||||
assert (item_folder / "256x256").is_file()
|
||||
|
||||
# List item
|
||||
await ws_client.send_json({"id": 6, "type": "image/list"})
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["id"] == 6
|
||||
assert msg["type"] == ws_const.TYPE_RESULT
|
||||
assert msg["success"]
|
||||
assert msg["result"] == [item]
|
||||
|
||||
# Delete item
|
||||
await ws_client.send_json(
|
||||
{"id": 7, "type": "image/delete", "image_id": item["id"]}
|
||||
)
|
||||
msg = await ws_client.receive_json()
|
||||
|
||||
assert msg["id"] == 7
|
||||
assert msg["type"] == ws_const.TYPE_RESULT
|
||||
assert msg["success"]
|
||||
|
||||
# Ensure removed from disk
|
||||
assert not item_folder.is_dir()
|
|
@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import (
|
|||
)
|
||||
from homeassistant.components.person import ATTR_SOURCE, ATTR_USER_ID, DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_GPS_ACCURACY,
|
||||
ATTR_ID,
|
||||
ATTR_LATITUDE,
|
||||
|
@ -24,7 +25,7 @@ from homeassistant.helpers import collection, entity_registry
|
|||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import assert_setup_component, mock_component, mock_restore_cache
|
||||
from tests.common import mock_component, mock_restore_cache
|
||||
|
||||
DEVICE_TRACKER = "device_tracker.test_tracker"
|
||||
DEVICE_TRACKER_2 = "device_tracker.test_tracker_2"
|
||||
|
@ -67,8 +68,7 @@ def storage_setup(hass, hass_storage, hass_admin_user):
|
|||
async def test_minimal_setup(hass):
|
||||
"""Test minimal config with only name."""
|
||||
config = {DOMAIN: {"id": "1234", "name": "test person"}}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.test_person")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -76,6 +76,7 @@ async def test_minimal_setup(hass):
|
|||
assert state.attributes.get(ATTR_LONGITUDE) is None
|
||||
assert state.attributes.get(ATTR_SOURCE) is None
|
||||
assert state.attributes.get(ATTR_USER_ID) is None
|
||||
assert state.attributes.get(ATTR_ENTITY_PICTURE) is None
|
||||
|
||||
|
||||
async def test_setup_no_id(hass):
|
||||
|
@ -94,8 +95,7 @@ async def test_setup_user_id(hass, hass_admin_user):
|
|||
"""Test config with user id."""
|
||||
user_id = hass_admin_user.id
|
||||
config = {DOMAIN: {"id": "1234", "name": "test person", "user_id": user_id}}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.test_person")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -115,8 +115,7 @@ async def test_valid_invalid_user_ids(hass, hass_admin_user):
|
|||
{"id": "5678", "name": "test bad user", "user_id": "bad_user_id"},
|
||||
]
|
||||
}
|
||||
with assert_setup_component(2):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.test_valid_user")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -141,8 +140,7 @@ async def test_setup_tracker(hass, hass_admin_user):
|
|||
"device_trackers": DEVICE_TRACKER,
|
||||
}
|
||||
}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.tracked_person")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -198,8 +196,7 @@ async def test_setup_two_trackers(hass, hass_admin_user):
|
|||
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
||||
}
|
||||
}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.tracked_person")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -285,8 +282,7 @@ async def test_ignore_unavailable_states(hass, hass_admin_user):
|
|||
"device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2],
|
||||
}
|
||||
}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.tracked_person")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
@ -337,10 +333,10 @@ async def test_restore_home_state(hass, hass_admin_user):
|
|||
"name": "tracked person",
|
||||
"user_id": user_id,
|
||||
"device_trackers": DEVICE_TRACKER,
|
||||
"picture": "/bla",
|
||||
}
|
||||
}
|
||||
with assert_setup_component(1):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
|
||||
state = hass.states.get("person.tracked_person")
|
||||
assert state.state == "home"
|
||||
|
@ -350,6 +346,7 @@ async def test_restore_home_state(hass, hass_admin_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
|
||||
assert state.attributes.get(ATTR_ENTITY_PICTURE) == "/bla"
|
||||
|
||||
|
||||
async def test_duplicate_ids(hass, hass_admin_user):
|
||||
|
@ -360,8 +357,7 @@ async def test_duplicate_ids(hass, hass_admin_user):
|
|||
{"id": "1234", "name": "test user 2"},
|
||||
]
|
||||
}
|
||||
with assert_setup_component(2):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
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
|
||||
|
@ -371,8 +367,7 @@ async def test_duplicate_ids(hass, hass_admin_user):
|
|||
async def test_create_person_during_run(hass):
|
||||
"""Test that person is updated if created while hass is running."""
|
||||
config = {DOMAIN: {}}
|
||||
with assert_setup_component(0):
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
hass.states.async_set(DEVICE_TRACKER, "home")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -465,6 +460,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup, hass_read_only_use
|
|||
"name": "Hello",
|
||||
"device_trackers": [DEVICE_TRACKER],
|
||||
"user_id": hass_read_only_user.id,
|
||||
"picture": "/bla",
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
|
@ -529,6 +525,7 @@ async def test_ws_update(hass, hass_ws_client, storage_setup):
|
|||
"name": "Updated Name",
|
||||
"device_trackers": [DEVICE_TRACKER_2],
|
||||
"user_id": None,
|
||||
"picture": "/bla",
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
|
@ -542,6 +539,7 @@ async def test_ws_update(hass, hass_ws_client, storage_setup):
|
|||
assert persons[0]["name"] == "Updated Name"
|
||||
assert persons[0]["device_trackers"] == [DEVICE_TRACKER_2]
|
||||
assert persons[0]["user_id"] is None
|
||||
assert persons[0]["picture"] == "/bla"
|
||||
|
||||
state = hass.states.get("person.tracked_person")
|
||||
assert state.name == "Updated Name"
|
||||
|
|
Loading…
Reference in New Issue