From 3cd6989824cf2db4ee667421c022f1eb0ce72a77 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Fri, 24 Jan 2020 08:22:17 +0000 Subject: [PATCH 01/10] Adds save_image --- .../components/sighthound/image_processing.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index 175b1edc4c68ed..be02cd63283eb3 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -1,6 +1,9 @@ """Person detection using Sighthound cloud service.""" +import io import logging +import os +from PIL import Image, ImageDraw import simplehound.core as hound import voluptuous as vol @@ -14,6 +17,7 @@ from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) @@ -22,6 +26,7 @@ ATTR_BOUNDING_BOX = "bounding_box" ATTR_PEOPLE = "people" CONF_ACCOUNT_TYPE = "account_type" +CONF_SAVE_FILE_FOLDER = "save_file_folder" DEV = "dev" PROD = "prod" @@ -29,6 +34,7 @@ { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]), + vol.Optional(CONF_SAVE_FILE_FOLDER): cv.isdir, } ) @@ -45,10 +51,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Sighthound error %s setup aborted", exc) return + save_file_folder = config.get(CONF_SAVE_FILE_FOLDER) + if save_file_folder: + save_file_folder = os.path.join(save_file_folder, "") # If no trailing / add it + entities = [] for camera in config[CONF_SOURCE]: sighthound = SighthoundEntity( - api, camera[CONF_ENTITY_ID], camera.get(CONF_NAME) + api, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), save_file_folder ) entities.append(sighthound) add_entities(entities) @@ -57,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SighthoundEntity(ImageProcessingEntity): """Create a sighthound entity.""" - def __init__(self, api, camera_entity, name): + def __init__(self, api, camera_entity, name, save_file_folder): """Init.""" self._api = api self._camera = camera_entity @@ -69,6 +79,8 @@ def __init__(self, api, camera_entity, name): self._state = None self._image_width = None self._image_height = None + if save_file_folder: + self._save_file_folder = save_file_folder def process_image(self, image): """Process an image.""" @@ -81,6 +93,8 @@ def process_image(self, image): self._image_height = metadata["image_height"] for person in people: self.fire_person_detected_event(person) + if hasattr(self, "_save_file_folder") and self._state > 0: + self.save_image(image, people, self._save_file_folder) def fire_person_detected_event(self, person): """Send event with detected total_persons.""" @@ -94,6 +108,21 @@ def fire_person_detected_event(self, person): }, ) + def save_image(self, image, people, directory): + """Save a timestamped image with bounding boxes around targets.""" + + img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + draw = ImageDraw.Draw(img) + + for person in people: + box = hound.bbox_to_tf_style( + person["boundingBox"], self._image_width, self._image_height + ) + draw_box(draw, box, self._image_width, self._image_height) + + latest_save_path = directory + "{}_latest.jpg".format(self._name) + img.save(latest_save_path) + @property def camera_entity(self): """Return camera entity id from process pictures.""" From 8cdf6d037fddaba13b919e2ccd7ad1761ff4cb53 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Fri, 14 Feb 2020 07:08:55 +0000 Subject: [PATCH 02/10] Update test_image_processing.py --- .../sighthound/test_image_processing.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 4548a3a6a3583d..fb82cfd82a4a61 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -1,4 +1,5 @@ """Tests for the Sighthound integration.""" +import os from unittest.mock import patch import pytest @@ -10,6 +11,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component +TEST_DIR = os.path.join(os.path.dirname(__file__)) + VALID_CONFIG = { ip.DOMAIN: { "platform": "sighthound", @@ -91,3 +94,18 @@ def capture_person_event(event): state = hass.states.get(VALID_ENTITY_ID) assert state.state == "2" assert len(person_events) == 2 + + +async def test_save_image(hass, mock_image, mock_detections): + """Save a processed image.""" + VALID_CONFIG.update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + with patch("PIL.Image.Image.save") as mock_save: + await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.async_block_till_done() + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == "2" + mock_save.assert_called_with("test.jpg") From 7283ff92d6554ff855fbbd0e4c78cbd624d268b1 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 07:13:20 +0000 Subject: [PATCH 03/10] Update test --- .../components/sighthound/image_processing.py | 2 -- .../sighthound/test_image_processing.py | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index be02cd63283eb3..a383e7cbd142c4 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -110,7 +110,6 @@ def fire_person_detected_event(self, person): def save_image(self, image, people, directory): """Save a timestamped image with bounding boxes around targets.""" - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") draw = ImageDraw.Draw(img) @@ -119,7 +118,6 @@ def save_image(self, image, people, directory): person["boundingBox"], self._image_width, self._image_height ) draw_box(draw, box, self._image_width, self._image_height) - latest_save_path = directory + "{}_latest.jpg".format(self._name) img.save(latest_save_path) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index fb82cfd82a4a61..136d67e1732e84 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -59,6 +59,13 @@ def mock_image(): yield image +@pytest.fixture +def mock_pil_img(): + """Return a mock PIL image.""" + with patch("PIL.Image.open") as pil_img: + yield pil_img + + async def test_bad_api_key(hass, caplog): """Catch bad api key.""" with patch("simplehound.core.cloud.detect", side_effect=hound.SimplehoundException): @@ -96,16 +103,17 @@ def capture_person_event(event): assert len(person_events) == 2 -async def test_save_image(hass, mock_image, mock_detections): +async def test_save_image(hass, mock_image, mock_detections, mock_pil_img): """Save a processed image.""" - VALID_CONFIG.update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + VALID_CONFIG[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) - data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} - with patch("PIL.Image.Image.save") as mock_save: + with patch("PIL.Image.Image") as pil_img: + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) assert state.state == "2" - mock_save.assert_called_with("test.jpg") + pil_img.save.assert_called() + # pil_img.save.assert_called_with("test.jpg") # kwargs={format: "JPEG"} From d2840581f3491dc5c33f275a9c1afb1388426b03 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 07:30:21 +0000 Subject: [PATCH 04/10] Tidy test --- tests/components/sighthound/test_image_processing.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 136d67e1732e84..1914595cc08a68 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -59,13 +59,6 @@ def mock_image(): yield image -@pytest.fixture -def mock_pil_img(): - """Return a mock PIL image.""" - with patch("PIL.Image.open") as pil_img: - yield pil_img - - async def test_bad_api_key(hass, caplog): """Catch bad api key.""" with patch("simplehound.core.cloud.detect", side_effect=hound.SimplehoundException): @@ -103,13 +96,13 @@ def capture_person_event(event): assert len(person_events) == 2 -async def test_save_image(hass, mock_image, mock_detections, mock_pil_img): +async def test_save_image(hass, mock_image, mock_detections): """Save a processed image.""" VALID_CONFIG[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) - with patch("PIL.Image.Image") as pil_img: + with patch("PIL.Image.open") as pil_img: data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() From b073e73bac411c7fee08902d20a1c26417a8a7b9 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 12:42:02 +0000 Subject: [PATCH 05/10] update image_processing with reviewer comments --- .../components/sighthound/image_processing.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index a383e7cbd142c4..ff67749b192319 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -1,7 +1,7 @@ """Person detection using Sighthound cloud service.""" import io import logging -import os +from pathlib import Path from PIL import Image, ImageDraw import simplehound.core as hound @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): save_file_folder = config.get(CONF_SAVE_FILE_FOLDER) if save_file_folder: - save_file_folder = os.path.join(save_file_folder, "") # If no trailing / add it + save_file_folder = Path(save_file_folder) entities = [] for camera in config[CONF_SOURCE]: @@ -79,8 +79,7 @@ def __init__(self, api, camera_entity, name, save_file_folder): self._state = None self._image_width = None self._image_height = None - if save_file_folder: - self._save_file_folder = save_file_folder + self._save_file_folder = save_file_folder def process_image(self, image): """Process an image.""" @@ -93,7 +92,7 @@ def process_image(self, image): self._image_height = metadata["image_height"] for person in people: self.fire_person_detected_event(person) - if hasattr(self, "_save_file_folder") and self._state > 0: + if self._save_file_folder and self._state > 0: self.save_image(image, people, self._save_file_folder) def fire_person_detected_event(self, person): @@ -118,7 +117,7 @@ def save_image(self, image, people, directory): person["boundingBox"], self._image_width, self._image_height ) draw_box(draw, box, self._image_width, self._image_height) - latest_save_path = directory + "{}_latest.jpg".format(self._name) + latest_save_path = directory / f"{self._name}_latest.jpg" img.save(latest_save_path) @property From 01d32afe9f5ac7be875f2a82c7c3cc573b2f443f Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 13:47:18 +0000 Subject: [PATCH 06/10] Update test_image_processing.py --- tests/components/sighthound/test_image_processing.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 1914595cc08a68..5ab8764fb72420 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -98,15 +98,18 @@ def capture_person_event(event): async def test_save_image(hass, mock_image, mock_detections): """Save a processed image.""" - VALID_CONFIG[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + VALID_CONFIG_SAVE_FILE = VALID_CONFIG.copy() + VALID_CONFIG_SAVE_FILE[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) - with patch("PIL.Image.open") as pil_img: + with patch( + "homeassistant.components.sighthound.image_processing.Image.open" + ) as pil_img_open: + pil_img = pil_img_open.return_value data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() state = hass.states.get(VALID_ENTITY_ID) assert state.state == "2" - pil_img.save.assert_called() - # pil_img.save.assert_called_with("test.jpg") # kwargs={format: "JPEG"} + assert pil_img.save.call_count == 1 From 6e5f4da698f28a82e3a4b0c8dc3ad335554648a0 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 14:43:06 +0000 Subject: [PATCH 07/10] Ammend tests not passing --- homeassistant/components/sighthound/image_processing.py | 3 ++- tests/components/sighthound/test_image_processing.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index ff67749b192319..748563563bcda2 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -109,7 +109,8 @@ def fire_person_detected_event(self, person): def save_image(self, image, people, directory): """Save a timestamped image with bounding boxes around targets.""" - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + img = Image.open(io.BytesIO(bytearray(image))) + # img = img.convert("RGB") # TODO add a patch draw = ImageDraw.Draw(img) for person in people: diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 5ab8764fb72420..69d5fc879b122b 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -1,4 +1,5 @@ """Tests for the Sighthound integration.""" +from copy import deepcopy import os from unittest.mock import patch @@ -98,7 +99,8 @@ def capture_person_event(event): async def test_save_image(hass, mock_image, mock_detections): """Save a processed image.""" - VALID_CONFIG_SAVE_FILE = VALID_CONFIG.copy() + VALID_CONFIG_SAVE_FILE = deepcopy(VALID_CONFIG) # this fails + # VALID_CONFIG_SAVE_FILE = VALID_CONFIG.copy() # this passes VALID_CONFIG_SAVE_FILE[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) From efa6e12b67bdbfed529851999d156f1f56a7b8f8 Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 14:50:15 +0000 Subject: [PATCH 08/10] Patch convert --- homeassistant/components/sighthound/image_processing.py | 3 +-- tests/components/sighthound/test_image_processing.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index 748563563bcda2..ff67749b192319 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -109,8 +109,7 @@ def fire_person_detected_event(self, person): def save_image(self, image, people, directory): """Save a timestamped image with bounding boxes around targets.""" - img = Image.open(io.BytesIO(bytearray(image))) - # img = img.convert("RGB") # TODO add a patch + img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") draw = ImageDraw.Draw(img) for person in people: diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 69d5fc879b122b..5def0c4171b597 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -99,8 +99,7 @@ def capture_person_event(event): async def test_save_image(hass, mock_image, mock_detections): """Save a processed image.""" - VALID_CONFIG_SAVE_FILE = deepcopy(VALID_CONFIG) # this fails - # VALID_CONFIG_SAVE_FILE = VALID_CONFIG.copy() # this passes + VALID_CONFIG_SAVE_FILE = deepcopy(VALID_CONFIG) VALID_CONFIG_SAVE_FILE[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) assert hass.states.get(VALID_ENTITY_ID) @@ -109,6 +108,7 @@ async def test_save_image(hass, mock_image, mock_detections): "homeassistant.components.sighthound.image_processing.Image.open" ) as pil_img_open: pil_img = pil_img_open.return_value + pil_img = pil_img.convert.return_value data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() From 7ad17fe9deae8af5b15b232fb1dafc62b39fa2af Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 15:29:48 +0000 Subject: [PATCH 09/10] remove join --- tests/components/sighthound/test_image_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 5def0c4171b597..8d79ed9198c170 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -12,7 +12,7 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component -TEST_DIR = os.path.join(os.path.dirname(__file__)) +TEST_DIR = os.path.dirname(__file__) VALID_CONFIG = { ip.DOMAIN: { From 38aaca3e828511cdbf70fa16e42957f085b99f6f Mon Sep 17 00:00:00 2001 From: Robin Cole Date: Sun, 23 Feb 2020 16:44:37 +0000 Subject: [PATCH 10/10] Use valid_config_save_file --- tests/components/sighthound/test_image_processing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 8d79ed9198c170..3c0d10bd5b3a95 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -99,9 +99,9 @@ def capture_person_event(event): async def test_save_image(hass, mock_image, mock_detections): """Save a processed image.""" - VALID_CONFIG_SAVE_FILE = deepcopy(VALID_CONFIG) - VALID_CONFIG_SAVE_FILE[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) - await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + valid_config_save_file = deepcopy(VALID_CONFIG) + valid_config_save_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) + await async_setup_component(hass, ip.DOMAIN, valid_config_save_file) assert hass.states.get(VALID_ENTITY_ID) with patch(