From 24a16ff8febff2040c00b83b42b04e02233854fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Aug 2020 11:33:04 +0200 Subject: [PATCH] Add image integration (#38969) --- CODEOWNERS | 1 + homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/image/__init__.py | 204 ++++++++++++++++++ homeassistant/components/image/const.py | 3 + homeassistant/components/image/manifest.json | 12 ++ homeassistant/components/person/__init__.py | 9 + homeassistant/components/person/manifest.json | 1 + homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- .../components/seven_segments/manifest.json | 2 +- .../components/sighthound/manifest.json | 2 +- .../components/tensorflow/manifest.json | 2 +- homeassistant/helpers/collection.py | 35 +-- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 +- requirements_test_all.txt | 3 +- tests/components/image/__init__.py | 1 + tests/components/image/logo.png | Bin 0 -> 38847 bytes tests/components/image/test_init.py | 76 +++++++ tests/components/person/test_init.py | 36 ++-- 20 files changed, 356 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/image/__init__.py create mode 100644 homeassistant/components/image/const.py create mode 100644 homeassistant/components/image/manifest.json create mode 100644 tests/components/image/__init__.py create mode 100644 tests/components/image/logo.png create mode 100644 tests/components/image/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 472e0ae730d..f497b64d7ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index f363f46d2d7..7f75a5e8897 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -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": [] } diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py new file mode 100644 index 00000000000..a8110decd0a --- /dev/null +++ b/homeassistant/components/image/__init__.py @@ -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]) diff --git a/homeassistant/components/image/const.py b/homeassistant/components/image/const.py new file mode 100644 index 00000000000..5241f7ec07b --- /dev/null +++ b/homeassistant/components/image/const.py @@ -0,0 +1,3 @@ +"""Constants for the Image integration.""" + +DOMAIN = "image" diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json new file mode 100644 index 00000000000..4fc9c2d1d05 --- /dev/null +++ b/homeassistant/components/image/manifest.json @@ -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"] +} diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 49f1b45f2ed..584ce708d15 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -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. diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index bd1dfa6b588..7aec7df7c9a 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -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" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index ffa659f979e..081645a4aa8 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -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": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index eaa813cae95..00d528ba399 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -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": [] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 352b96b5f22..4996ba29f83 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -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"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 1ad7effdf0e..a5c56b37778 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -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"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index fc87b5cdbff..ddb0ec542ba 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -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": [] } diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 06c86d3aa1c..9e7c6061987 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -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, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58d3e3be883..9a8efbe2bd6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index c2527888b73..58fdb388958 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b8b4019fbd..c035d938e5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/image/__init__.py b/tests/components/image/__init__.py new file mode 100644 index 00000000000..8bf90c4f516 --- /dev/null +++ b/tests/components/image/__init__.py @@ -0,0 +1 @@ +"""Tests for the Image integration.""" diff --git a/tests/components/image/logo.png b/tests/components/image/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4fe49f1263073fe708e82d1301ea136ad33ae2a0 GIT binary patch literal 38847 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 z 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 literal 0 HcmV?d00001 diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py new file mode 100644 index 00000000000..277e6f07149 --- /dev/null +++ b/tests/components/image/test_init.py @@ -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() diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 887f0d94fef..64aa583e2f5 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -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"