From bca629ed31f00df17c2d601962ca9a5a110be1bd Mon Sep 17 00:00:00 2001
From: Sam Reed <reedy@wikimedia.org>
Date: Sun, 14 Jan 2024 09:40:05 +0000
Subject: [PATCH] Drop facebox integration (#107005)

---
 homeassistant/components/facebox/__init__.py  |   1 -
 homeassistant/components/facebox/const.py     |   4 -
 .../components/facebox/image_processing.py    | 282 ---------------
 .../components/facebox/manifest.json          |   7 -
 .../components/facebox/services.yaml          |  17 -
 homeassistant/components/facebox/strings.json |  22 --
 homeassistant/generated/integrations.json     |   6 -
 tests/components/facebox/__init__.py          |   1 -
 .../facebox/test_image_processing.py          | 341 ------------------
 9 files changed, 681 deletions(-)
 delete mode 100644 homeassistant/components/facebox/__init__.py
 delete mode 100644 homeassistant/components/facebox/const.py
 delete mode 100644 homeassistant/components/facebox/image_processing.py
 delete mode 100644 homeassistant/components/facebox/manifest.json
 delete mode 100644 homeassistant/components/facebox/services.yaml
 delete mode 100644 homeassistant/components/facebox/strings.json
 delete mode 100644 tests/components/facebox/__init__.py
 delete mode 100644 tests/components/facebox/test_image_processing.py

diff --git a/homeassistant/components/facebox/__init__.py b/homeassistant/components/facebox/__init__.py
deleted file mode 100644
index 9e5b6afb10b..00000000000
--- a/homeassistant/components/facebox/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The facebox component."""
diff --git a/homeassistant/components/facebox/const.py b/homeassistant/components/facebox/const.py
deleted file mode 100644
index 991ec925a98..00000000000
--- a/homeassistant/components/facebox/const.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""Constants for the Facebox component."""
-
-DOMAIN = "facebox"
-SERVICE_TEACH_FACE = "teach_face"
diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py
deleted file mode 100644
index 5584efb883a..00000000000
--- a/homeassistant/components/facebox/image_processing.py
+++ /dev/null
@@ -1,282 +0,0 @@
-"""Component for facial detection and identification via facebox."""
-from __future__ import annotations
-
-import base64
-from http import HTTPStatus
-import logging
-
-import requests
-import voluptuous as vol
-
-from homeassistant.components.image_processing import (
-    ATTR_CONFIDENCE,
-    PLATFORM_SCHEMA,
-    ImageProcessingFaceEntity,
-)
-from homeassistant.const import (
-    ATTR_ENTITY_ID,
-    ATTR_ID,
-    ATTR_NAME,
-    CONF_ENTITY_ID,
-    CONF_IP_ADDRESS,
-    CONF_NAME,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_SOURCE,
-    CONF_USERNAME,
-)
-from homeassistant.core import HomeAssistant, ServiceCall, split_entity_id
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-
-from .const import DOMAIN, SERVICE_TEACH_FACE
-
-_LOGGER = logging.getLogger(__name__)
-
-ATTR_BOUNDING_BOX = "bounding_box"
-ATTR_CLASSIFIER = "classifier"
-ATTR_IMAGE_ID = "image_id"
-ATTR_MATCHED = "matched"
-FACEBOX_NAME = "name"
-CLASSIFIER = "facebox"
-DATA_FACEBOX = "facebox_classifiers"
-FILE_PATH = "file_path"
-
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
-    {
-        vol.Required(CONF_IP_ADDRESS): cv.string,
-        vol.Required(CONF_PORT): cv.port,
-        vol.Optional(CONF_USERNAME): cv.string,
-        vol.Optional(CONF_PASSWORD): cv.string,
-    }
-)
-
-SERVICE_TEACH_SCHEMA = vol.Schema(
-    {
-        vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
-        vol.Required(ATTR_NAME): cv.string,
-        vol.Required(FILE_PATH): cv.string,
-    }
-)
-
-
-def check_box_health(url, username, password):
-    """Check the health of the classifier and return its id if healthy."""
-    kwargs = {}
-    if username:
-        kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password)
-    try:
-        response = requests.get(url, **kwargs, timeout=10)
-        if response.status_code == HTTPStatus.UNAUTHORIZED:
-            _LOGGER.error("AuthenticationError on %s", CLASSIFIER)
-            return None
-        if response.status_code == HTTPStatus.OK:
-            return response.json()["hostname"]
-    except requests.exceptions.ConnectionError:
-        _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
-        return None
-
-
-def encode_image(image):
-    """base64 encode an image stream."""
-    base64_img = base64.b64encode(image).decode("ascii")
-    return base64_img
-
-
-def get_matched_faces(faces):
-    """Return the name and rounded confidence of matched faces."""
-    return {
-        face["name"]: round(face["confidence"], 2) for face in faces if face["matched"]
-    }
-
-
-def parse_faces(api_faces):
-    """Parse the API face data into the format required."""
-    known_faces = []
-    for entry in api_faces:
-        face = {}
-        if entry["matched"]:  # This data is only in matched faces.
-            face[FACEBOX_NAME] = entry["name"]
-            face[ATTR_IMAGE_ID] = entry["id"]
-        else:  # Lets be explicit.
-            face[FACEBOX_NAME] = None
-            face[ATTR_IMAGE_ID] = None
-        face[ATTR_CONFIDENCE] = round(100.0 * entry["confidence"], 2)
-        face[ATTR_MATCHED] = entry["matched"]
-        face[ATTR_BOUNDING_BOX] = entry["rect"]
-        known_faces.append(face)
-    return known_faces
-
-
-def post_image(url, image, username, password):
-    """Post an image to the classifier."""
-    kwargs = {}
-    if username:
-        kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password)
-    try:
-        response = requests.post(
-            url, json={"base64": encode_image(image)}, timeout=10, **kwargs
-        )
-        if response.status_code == HTTPStatus.UNAUTHORIZED:
-            _LOGGER.error("AuthenticationError on %s", CLASSIFIER)
-            return None
-        return response
-    except requests.exceptions.ConnectionError:
-        _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
-        return None
-
-
-def teach_file(url, name, file_path, username, password):
-    """Teach the classifier a name associated with a file."""
-    kwargs = {}
-    if username:
-        kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password)
-    try:
-        with open(file_path, "rb") as open_file:
-            response = requests.post(
-                url,
-                data={FACEBOX_NAME: name, ATTR_ID: file_path},
-                files={"file": open_file},
-                timeout=10,
-                **kwargs,
-            )
-        if response.status_code == HTTPStatus.UNAUTHORIZED:
-            _LOGGER.error("AuthenticationError on %s", CLASSIFIER)
-        elif response.status_code == HTTPStatus.BAD_REQUEST:
-            _LOGGER.error(
-                "%s teaching of file %s failed with message:%s",
-                CLASSIFIER,
-                file_path,
-                response.text,
-            )
-    except requests.exceptions.ConnectionError:
-        _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
-
-
-def valid_file_path(file_path):
-    """Check that a file_path points to a valid file."""
-    try:
-        cv.isfile(file_path)
-        return True
-    except vol.Invalid:
-        _LOGGER.error("%s error: Invalid file path: %s", CLASSIFIER, file_path)
-        return False
-
-
-def setup_platform(
-    hass: HomeAssistant,
-    config: ConfigType,
-    add_entities: AddEntitiesCallback,
-    discovery_info: DiscoveryInfoType | None = None,
-) -> None:
-    """Set up the classifier."""
-    if DATA_FACEBOX not in hass.data:
-        hass.data[DATA_FACEBOX] = []
-
-    ip_address = config[CONF_IP_ADDRESS]
-    port = config[CONF_PORT]
-    username = config.get(CONF_USERNAME)
-    password = config.get(CONF_PASSWORD)
-    url_health = f"http://{ip_address}:{port}/healthz"
-    hostname = check_box_health(url_health, username, password)
-    if hostname is None:
-        return
-
-    entities = []
-    for camera in config[CONF_SOURCE]:
-        facebox = FaceClassifyEntity(
-            ip_address,
-            port,
-            username,
-            password,
-            hostname,
-            camera[CONF_ENTITY_ID],
-            camera.get(CONF_NAME),
-        )
-        entities.append(facebox)
-        hass.data[DATA_FACEBOX].append(facebox)
-    add_entities(entities)
-
-    def service_handle(service: ServiceCall) -> None:
-        """Handle for services."""
-        entity_ids = service.data.get("entity_id")
-
-        classifiers = hass.data[DATA_FACEBOX]
-        if entity_ids:
-            classifiers = [c for c in classifiers if c.entity_id in entity_ids]
-
-        for classifier in classifiers:
-            name = service.data.get(ATTR_NAME)
-            file_path = service.data.get(FILE_PATH)
-            classifier.teach(name, file_path)
-
-    hass.services.register(
-        DOMAIN, SERVICE_TEACH_FACE, service_handle, schema=SERVICE_TEACH_SCHEMA
-    )
-
-
-class FaceClassifyEntity(ImageProcessingFaceEntity):
-    """Perform a face classification."""
-
-    def __init__(
-        self, ip_address, port, username, password, hostname, camera_entity, name=None
-    ):
-        """Init with the API key and model id."""
-        super().__init__()
-        self._url_check = f"http://{ip_address}:{port}/{CLASSIFIER}/check"
-        self._url_teach = f"http://{ip_address}:{port}/{CLASSIFIER}/teach"
-        self._username = username
-        self._password = password
-        self._hostname = hostname
-        self._camera = camera_entity
-        if name:
-            self._name = name
-        else:
-            camera_name = split_entity_id(camera_entity)[1]
-            self._name = f"{CLASSIFIER} {camera_name}"
-        self._matched = {}
-
-    def process_image(self, image):
-        """Process an image."""
-        response = post_image(self._url_check, image, self._username, self._password)
-        if response:
-            response_json = response.json()
-            if response_json["success"]:
-                total_faces = response_json["facesCount"]
-                faces = parse_faces(response_json["faces"])
-                self._matched = get_matched_faces(faces)
-                self.process_faces(faces, total_faces)
-
-        else:
-            self.total_faces = None
-            self.faces = []
-            self._matched = {}
-
-    def teach(self, name, file_path):
-        """Teach classifier a face name."""
-        if not self.hass.config.is_allowed_path(file_path) or not valid_file_path(
-            file_path
-        ):
-            return
-        teach_file(self._url_teach, name, file_path, self._username, self._password)
-
-    @property
-    def camera_entity(self):
-        """Return camera entity id from process pictures."""
-        return self._camera
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def extra_state_attributes(self):
-        """Return the classifier attributes."""
-        return {
-            "matched_faces": self._matched,
-            "total_matched_faces": len(self._matched),
-            "hostname": self._hostname,
-        }
diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json
deleted file mode 100644
index f552fef1b87..00000000000
--- a/homeassistant/components/facebox/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "domain": "facebox",
-  "name": "Facebox",
-  "codeowners": [],
-  "documentation": "https://www.home-assistant.io/integrations/facebox",
-  "iot_class": "local_push"
-}
diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml
deleted file mode 100644
index 0438338f55e..00000000000
--- a/homeassistant/components/facebox/services.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-teach_face:
-  fields:
-    entity_id:
-      selector:
-        entity:
-          integration: facebox
-          domain: image_processing
-    name:
-      required: true
-      example: "my_name"
-      selector:
-        text:
-    file_path:
-      required: true
-      example: "/images/my_image.jpg"
-      selector:
-        text:
diff --git a/homeassistant/components/facebox/strings.json b/homeassistant/components/facebox/strings.json
deleted file mode 100644
index 1869673b643..00000000000
--- a/homeassistant/components/facebox/strings.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
-  "services": {
-    "teach_face": {
-      "name": "Teach face",
-      "description": "Teaches facebox a face using a file.",
-      "fields": {
-        "entity_id": {
-          "name": "Entity",
-          "description": "The facebox entity to teach."
-        },
-        "name": {
-          "name": "[%key:common::config_flow::data::name%]",
-          "description": "The name of the face to teach."
-        },
-        "file_path": {
-          "name": "File path",
-          "description": "The path to the image file."
-        }
-      }
-    }
-  }
-}
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index d5f8354574f..49527ba6dd0 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1680,12 +1680,6 @@
       "config_flow": false,
       "iot_class": "cloud_push"
     },
-    "facebox": {
-      "name": "Facebox",
-      "integration_type": "hub",
-      "config_flow": false,
-      "iot_class": "local_push"
-    },
     "fail2ban": {
       "name": "Fail2Ban",
       "integration_type": "hub",
diff --git a/tests/components/facebox/__init__.py b/tests/components/facebox/__init__.py
deleted file mode 100644
index fbbb6640e40..00000000000
--- a/tests/components/facebox/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the facebox component."""
diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py
deleted file mode 100644
index 4c6497b975b..00000000000
--- a/tests/components/facebox/test_image_processing.py
+++ /dev/null
@@ -1,341 +0,0 @@
-"""The tests for the facebox component."""
-from http import HTTPStatus
-from unittest.mock import Mock, mock_open, patch
-
-import pytest
-import requests
-import requests_mock
-
-import homeassistant.components.facebox.image_processing as fb
-import homeassistant.components.image_processing as ip
-from homeassistant.const import (
-    ATTR_ENTITY_ID,
-    ATTR_NAME,
-    CONF_FRIENDLY_NAME,
-    CONF_IP_ADDRESS,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_USERNAME,
-    STATE_UNKNOWN,
-)
-from homeassistant.core import HomeAssistant, callback
-from homeassistant.setup import async_setup_component
-
-MOCK_IP = "192.168.0.1"
-MOCK_PORT = "8080"
-
-# Mock data returned by the facebox API.
-MOCK_BOX_ID = "b893cc4f7fd6"
-MOCK_ERROR_NO_FACE = "No face found"
-MOCK_FACE = {
-    "confidence": 0.5812028911604818,
-    "id": "john.jpg",
-    "matched": True,
-    "name": "John Lennon",
-    "rect": {"height": 75, "left": 63, "top": 262, "width": 74},
-}
-
-MOCK_FILE_PATH = "/images/mock.jpg"
-
-MOCK_HEALTH = {
-    "success": True,
-    "hostname": "b893cc4f7fd6",
-    "metadata": {"boxname": "facebox", "build": "development"},
-    "errors": [],
-}
-
-MOCK_JSON = {"facesCount": 1, "success": True, "faces": [MOCK_FACE]}
-
-MOCK_NAME = "mock_name"
-MOCK_USERNAME = "mock_username"
-MOCK_PASSWORD = "mock_password"
-
-# Faces data after parsing.
-PARSED_FACES = [
-    {
-        fb.FACEBOX_NAME: "John Lennon",
-        fb.ATTR_IMAGE_ID: "john.jpg",
-        fb.ATTR_CONFIDENCE: 58.12,
-        fb.ATTR_MATCHED: True,
-        fb.ATTR_BOUNDING_BOX: {"height": 75, "left": 63, "top": 262, "width": 74},
-    }
-]
-
-MATCHED_FACES = {"John Lennon": 58.12}
-
-VALID_ENTITY_ID = "image_processing.facebox_demo_camera"
-VALID_CONFIG = {
-    ip.DOMAIN: {
-        "platform": "facebox",
-        CONF_IP_ADDRESS: MOCK_IP,
-        CONF_PORT: MOCK_PORT,
-        ip.CONF_SOURCE: {ip.CONF_ENTITY_ID: "camera.demo_camera"},
-    },
-    "camera": {"platform": "demo"},
-}
-
-
-@pytest.fixture(autouse=True)
-async def setup_homeassistant(hass: HomeAssistant):
-    """Set up the homeassistant integration."""
-    await async_setup_component(hass, "homeassistant", {})
-
-
-@pytest.fixture
-def mock_healthybox():
-    """Mock fb.check_box_health."""
-    check_box_health = (
-        "homeassistant.components.facebox.image_processing.check_box_health"
-    )
-    with patch(check_box_health, return_value=MOCK_BOX_ID) as _mock_healthybox:
-        yield _mock_healthybox
-
-
-@pytest.fixture
-def mock_isfile():
-    """Mock os.path.isfile."""
-    with patch(
-        "homeassistant.components.facebox.image_processing.cv.isfile", return_value=True
-    ) as _mock_isfile:
-        yield _mock_isfile
-
-
-@pytest.fixture
-def mock_image():
-    """Return a mock camera image."""
-    with patch(
-        "homeassistant.components.demo.camera.DemoCamera.camera_image",
-        return_value=b"Test",
-    ) as image:
-        yield image
-
-
-@pytest.fixture
-def mock_open_file():
-    """Mock open."""
-    mopen = mock_open()
-    with patch(
-        "homeassistant.components.facebox.image_processing.open", mopen, create=True
-    ) as _mock_open:
-        yield _mock_open
-
-
-def test_check_box_health(caplog: pytest.LogCaptureFixture) -> None:
-    """Test check box health."""
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/healthz"
-        mock_req.get(url, status_code=HTTPStatus.OK, json=MOCK_HEALTH)
-        assert fb.check_box_health(url, "user", "pass") == MOCK_BOX_ID
-
-        mock_req.get(url, status_code=HTTPStatus.UNAUTHORIZED)
-        assert fb.check_box_health(url, None, None) is None
-        assert "AuthenticationError on facebox" in caplog.text
-
-        mock_req.get(url, exc=requests.exceptions.ConnectTimeout)
-        fb.check_box_health(url, None, None)
-        assert "ConnectionError: Is facebox running?" in caplog.text
-
-
-def test_encode_image() -> None:
-    """Test that binary data is encoded correctly."""
-    assert fb.encode_image(b"test") == "dGVzdA=="
-
-
-def test_get_matched_faces() -> None:
-    """Test that matched_faces are parsed correctly."""
-    assert fb.get_matched_faces(PARSED_FACES) == MATCHED_FACES
-
-
-def test_parse_faces() -> None:
-    """Test parsing of raw face data, and generation of matched_faces."""
-    assert fb.parse_faces(MOCK_JSON["faces"]) == PARSED_FACES
-
-
-@patch("os.access", Mock(return_value=False))
-def test_valid_file_path() -> None:
-    """Test that an invalid file_path is caught."""
-    assert not fb.valid_file_path("test_path")
-
-
-async def test_setup_platform(hass: HomeAssistant, mock_healthybox) -> None:
-    """Set up platform with one entity."""
-    await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
-    await hass.async_block_till_done()
-    assert hass.states.get(VALID_ENTITY_ID)
-
-
-async def test_setup_platform_with_auth(hass: HomeAssistant, mock_healthybox) -> None:
-    """Set up platform with one entity and auth."""
-    valid_config_auth = VALID_CONFIG.copy()
-    valid_config_auth[ip.DOMAIN][CONF_USERNAME] = MOCK_USERNAME
-    valid_config_auth[ip.DOMAIN][CONF_PASSWORD] = MOCK_PASSWORD
-
-    await async_setup_component(hass, ip.DOMAIN, valid_config_auth)
-    await hass.async_block_till_done()
-    assert hass.states.get(VALID_ENTITY_ID)
-
-
-async def test_process_image(hass: HomeAssistant, mock_healthybox, mock_image) -> None:
-    """Test successful processing of an image."""
-    await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
-    await hass.async_block_till_done()
-    assert hass.states.get(VALID_ENTITY_ID)
-
-    face_events = []
-
-    @callback
-    def mock_face_event(event):
-        """Mock event."""
-        face_events.append(event)
-
-    hass.bus.async_listen("image_processing.detect_face", mock_face_event)
-
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/check"
-        mock_req.post(url, json=MOCK_JSON)
-        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 == "1"
-    assert state.attributes.get("matched_faces") == MATCHED_FACES
-    assert state.attributes.get("total_matched_faces") == 1
-
-    PARSED_FACES[0][ATTR_ENTITY_ID] = VALID_ENTITY_ID  # Update.
-    assert state.attributes.get("faces") == PARSED_FACES
-    assert state.attributes.get(CONF_FRIENDLY_NAME) == "facebox demo_camera"
-
-    assert len(face_events) == 1
-    assert face_events[0].data[ATTR_NAME] == PARSED_FACES[0][ATTR_NAME]
-    assert (
-        face_events[0].data[fb.ATTR_CONFIDENCE] == PARSED_FACES[0][fb.ATTR_CONFIDENCE]
-    )
-    assert face_events[0].data[ATTR_ENTITY_ID] == VALID_ENTITY_ID
-    assert face_events[0].data[fb.ATTR_IMAGE_ID] == PARSED_FACES[0][fb.ATTR_IMAGE_ID]
-    assert (
-        face_events[0].data[fb.ATTR_BOUNDING_BOX]
-        == PARSED_FACES[0][fb.ATTR_BOUNDING_BOX]
-    )
-
-
-async def test_process_image_errors(
-    hass: HomeAssistant, mock_healthybox, mock_image, caplog: pytest.LogCaptureFixture
-) -> None:
-    """Test process_image errors."""
-    await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
-    await hass.async_block_till_done()
-    assert hass.states.get(VALID_ENTITY_ID)
-
-    # Test connection error.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/check"
-        mock_req.register_uri("POST", url, exc=requests.exceptions.ConnectTimeout)
-        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()
-        assert "ConnectionError: Is facebox running?" in caplog.text
-
-    state = hass.states.get(VALID_ENTITY_ID)
-    assert state.state == STATE_UNKNOWN
-    assert state.attributes.get("faces") == []
-    assert state.attributes.get("matched_faces") == {}
-
-    # Now test with bad auth.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/check"
-        mock_req.register_uri("POST", url, status_code=HTTPStatus.UNAUTHORIZED)
-        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()
-        assert "AuthenticationError on facebox" in caplog.text
-
-
-async def test_teach_service(
-    hass: HomeAssistant,
-    mock_healthybox,
-    mock_image,
-    mock_isfile,
-    mock_open_file,
-    caplog: pytest.LogCaptureFixture,
-) -> None:
-    """Test teaching of facebox."""
-    await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG)
-    await hass.async_block_till_done()
-    assert hass.states.get(VALID_ENTITY_ID)
-
-    # Patch out 'is_allowed_path' as the mock files aren't allowed
-    hass.config.is_allowed_path = Mock(return_value=True)
-
-    # Test successful teach.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach"
-        mock_req.post(url, status_code=HTTPStatus.OK)
-        data = {
-            ATTR_ENTITY_ID: VALID_ENTITY_ID,
-            ATTR_NAME: MOCK_NAME,
-            fb.FILE_PATH: MOCK_FILE_PATH,
-        }
-        await hass.services.async_call(
-            fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data
-        )
-        await hass.async_block_till_done()
-
-    # Now test with bad auth.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach"
-        mock_req.post(url, status_code=HTTPStatus.UNAUTHORIZED)
-        data = {
-            ATTR_ENTITY_ID: VALID_ENTITY_ID,
-            ATTR_NAME: MOCK_NAME,
-            fb.FILE_PATH: MOCK_FILE_PATH,
-        }
-        await hass.services.async_call(
-            fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data
-        )
-        await hass.async_block_till_done()
-        assert "AuthenticationError on facebox" in caplog.text
-
-    # Now test the failed teaching.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach"
-        mock_req.post(url, status_code=HTTPStatus.BAD_REQUEST, text=MOCK_ERROR_NO_FACE)
-        data = {
-            ATTR_ENTITY_ID: VALID_ENTITY_ID,
-            ATTR_NAME: MOCK_NAME,
-            fb.FILE_PATH: MOCK_FILE_PATH,
-        }
-        await hass.services.async_call(
-            fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data
-        )
-        await hass.async_block_till_done()
-        assert MOCK_ERROR_NO_FACE in caplog.text
-
-    # Now test connection error.
-    with requests_mock.Mocker() as mock_req:
-        url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach"
-        mock_req.post(url, exc=requests.exceptions.ConnectTimeout)
-        data = {
-            ATTR_ENTITY_ID: VALID_ENTITY_ID,
-            ATTR_NAME: MOCK_NAME,
-            fb.FILE_PATH: MOCK_FILE_PATH,
-        }
-        await hass.services.async_call(
-            fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data
-        )
-        await hass.async_block_till_done()
-        assert "ConnectionError: Is facebox running?" in caplog.text
-
-
-async def test_setup_platform_with_name(hass: HomeAssistant, mock_healthybox) -> None:
-    """Set up platform with one entity and a name."""
-    named_entity_id = f"image_processing.{MOCK_NAME}"
-
-    valid_config_named = VALID_CONFIG.copy()
-    valid_config_named[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME
-
-    await async_setup_component(hass, ip.DOMAIN, valid_config_named)
-    await hass.async_block_till_done()
-    assert hass.states.get(named_entity_id)
-    state = hass.states.get(named_entity_id)
-    assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME
-- 
GitLab