Remove Microsoft Face (#174977)

This commit is contained in:
Manu
2026-06-30 13:20:37 +00:00
committed by Franck Nijhof
parent a82bed44fb
commit 8cfc152725
31 changed files with 0 additions and 1680 deletions
-3
View File
@@ -7,9 +7,6 @@
"azure_event_hub",
"azure_service_bus",
"azure_storage",
"microsoft_face_detect",
"microsoft_face_identify",
"microsoft_face",
"microsoft",
"onedrive",
"onedrive_for_business",
@@ -1,344 +0,0 @@
"""Support for Microsoft face recognition."""
import asyncio
from collections.abc import Coroutine
import json
import logging
from typing import Any, override
import aiohttp
from aiohttp.hdrs import CONTENT_TYPE
import voluptuous as vol
from homeassistant.components import camera
from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
ATTR_CAMERA_ENTITY = "camera_entity"
ATTR_GROUP = "group"
ATTR_PERSON = "person"
CONF_AZURE_REGION = "azure_region"
DEFAULT_TIMEOUT = 10
DOMAIN = "microsoft_face"
DATA_MICROSOFT_FACE: HassKey[MicrosoftFace] = HassKey(DOMAIN)
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
SERVICE_CREATE_GROUP = "create_group"
SERVICE_CREATE_PERSON = "create_person"
SERVICE_DELETE_GROUP = "delete_group"
SERVICE_DELETE_PERSON = "delete_person"
SERVICE_FACE_PERSON = "face_person"
SERVICE_TRAIN_GROUP = "train_group"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
SCHEMA_GROUP_SERVICE = vol.Schema({vol.Required(ATTR_NAME): cv.string})
SCHEMA_PERSON_SERVICE = SCHEMA_GROUP_SERVICE.extend(
{vol.Required(ATTR_GROUP): cv.slugify}
)
SCHEMA_FACE_SERVICE = vol.Schema(
{
vol.Required(ATTR_PERSON): cv.string,
vol.Required(ATTR_GROUP): cv.slugify,
vol.Required(ATTR_CAMERA_ENTITY): cv.entity_id,
}
)
SCHEMA_TRAIN_SERVICE = vol.Schema({vol.Required(ATTR_GROUP): cv.slugify})
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Microsoft Face."""
component = EntityComponent[MicrosoftFaceGroupEntity](
logging.getLogger(__name__), DOMAIN, hass
)
entities: dict[str, MicrosoftFaceGroupEntity] = {}
domain_config: dict[str, Any] = config[DOMAIN]
azure_region: str = domain_config[CONF_AZURE_REGION]
api_key: str = domain_config[CONF_API_KEY]
timeout: int = domain_config[CONF_TIMEOUT]
face = MicrosoftFace(
hass,
azure_region,
api_key,
timeout,
component,
entities,
)
try:
# read exists group/person from cloud and create entities
await face.update_store()
except HomeAssistantError as err:
_LOGGER.error("Can't load data from face api: %s", err)
return False
hass.data[DATA_MICROSOFT_FACE] = face
async def async_create_group(service: ServiceCall) -> None:
"""Create a new person group."""
name = service.data[ATTR_NAME]
g_id = slugify(name)
try:
await face.call_api("put", f"persongroups/{g_id}", {"name": name})
face.store[g_id] = {}
old_entity = entities.pop(g_id, None)
if old_entity:
await component.async_remove_entity(old_entity.entity_id)
entities[g_id] = MicrosoftFaceGroupEntity(face, g_id, name)
await component.async_add_entities([entities[g_id]])
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error("Can't create group '%s' with error: %s", g_id, err)
hass.services.async_register(
DOMAIN, SERVICE_CREATE_GROUP, async_create_group, schema=SCHEMA_GROUP_SERVICE
)
async def async_delete_group(service: ServiceCall) -> None:
"""Delete a person group."""
g_id = slugify(service.data[ATTR_NAME])
try:
await face.call_api("delete", f"persongroups/{g_id}")
face.store.pop(g_id)
entity = entities.pop(g_id)
await component.async_remove_entity(entity.entity_id)
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error("Can't delete group '%s' with error: %s", g_id, err)
hass.services.async_register(
DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, schema=SCHEMA_GROUP_SERVICE
)
async def async_train_group(service: ServiceCall) -> None:
"""Train a person group."""
g_id = service.data[ATTR_GROUP]
try:
await face.call_api("post", f"persongroups/{g_id}/train")
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error("Can't train group '%s' with error: %s", g_id, err)
hass.services.async_register(
DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, schema=SCHEMA_TRAIN_SERVICE
)
async def async_create_person(service: ServiceCall) -> None:
"""Create a person in a group."""
name = service.data[ATTR_NAME]
g_id = service.data[ATTR_GROUP]
try:
user_data = await face.call_api(
"post", f"persongroups/{g_id}/persons", {"name": name}
)
face.store[g_id][name] = user_data["personId"]
entities[g_id].async_write_ha_state()
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error("Can't create person '%s' with error: %s", name, err)
hass.services.async_register(
DOMAIN, SERVICE_CREATE_PERSON, async_create_person, schema=SCHEMA_PERSON_SERVICE
)
async def async_delete_person(service: ServiceCall) -> None:
"""Delete a person in a group."""
name = service.data[ATTR_NAME]
g_id = service.data[ATTR_GROUP]
p_id = face.store[g_id].get(name)
try:
await face.call_api("delete", f"persongroups/{g_id}/persons/{p_id}")
face.store[g_id].pop(name)
entities[g_id].async_write_ha_state()
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error("Can't delete person '%s' with error: %s", p_id, err)
hass.services.async_register(
DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, schema=SCHEMA_PERSON_SERVICE
)
async def async_face_person(service: ServiceCall) -> None:
"""Add a new face picture to a person."""
g_id = service.data[ATTR_GROUP]
p_id = face.store[g_id].get(service.data[ATTR_PERSON])
camera_entity = service.data[ATTR_CAMERA_ENTITY]
try:
image = await camera.async_get_image(hass, camera_entity)
await face.call_api(
"post",
f"persongroups/{g_id}/persons/{p_id}/persistedFaces",
image.content,
binary=True,
)
# pylint: disable-next=home-assistant-action-swallowed-exception
except HomeAssistantError as err:
_LOGGER.error(
"Can't add an image of a person '%s' with error: %s", p_id, err
)
hass.services.async_register(
DOMAIN, SERVICE_FACE_PERSON, async_face_person, schema=SCHEMA_FACE_SERVICE
)
return True
class MicrosoftFaceGroupEntity(Entity):
"""Person-Group state/data Entity."""
_attr_should_poll = False
def __init__(self, api: MicrosoftFace, g_id: str, name: str) -> None:
"""Initialize person/group entity."""
self.entity_id = f"{DOMAIN}.{g_id}"
self._api = api
self._id = g_id
self._attr_name = name
@property
@override
def state(self) -> int:
"""Return the state of the entity."""
return len(self._api.store[self._id])
@property
@override
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific state attributes."""
return dict(self._api.store[self._id])
class MicrosoftFace:
"""Microsoft Face api for Home Assistant."""
def __init__(
self,
hass: HomeAssistant,
server_loc: str,
api_key: str,
timeout: int,
component: EntityComponent[MicrosoftFaceGroupEntity],
entities: dict[str, MicrosoftFaceGroupEntity],
) -> None:
"""Initialize Microsoft Face api."""
self.hass = hass
self.websession = async_get_clientsession(hass)
self.timeout = timeout
self._api_key = api_key
self._server_url = f"https://{server_loc}.{FACE_API_URL}"
self._store: dict[str, dict[str, Any]] = {}
self._component = component
self._entities = entities
@property
def store(self) -> dict[str, dict[str, Any]]:
"""Store group/person data and IDs."""
return self._store
async def update_store(self) -> None:
"""Load all group/person data into local store."""
groups = await self.call_api("get", "persongroups")
remove_tasks: list[Coroutine[Any, Any, None]] = []
new_entities = []
for group in groups:
g_id = group["personGroupId"]
self._store[g_id] = {}
old_entity = self._entities.pop(g_id, None)
if old_entity:
remove_tasks.append(
self._component.async_remove_entity(old_entity.entity_id)
)
self._entities[g_id] = MicrosoftFaceGroupEntity(self, g_id, group["name"])
new_entities.append(self._entities[g_id])
persons = await self.call_api("get", f"persongroups/{g_id}/persons")
for person in persons:
self._store[g_id][person["name"]] = person["personId"]
if remove_tasks:
await asyncio.gather(*remove_tasks)
await self._component.async_add_entities(new_entities)
async def call_api(self, method, function, data=None, binary=False, params=None):
"""Make an api call."""
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
url = self._server_url.format(function)
payload = None
if binary:
headers[CONTENT_TYPE] = "application/octet-stream"
payload = data
else:
headers[CONTENT_TYPE] = CONTENT_TYPE_JSON
if data is not None:
payload = json.dumps(data).encode()
else:
payload = None
try:
async with asyncio.timeout(self.timeout):
response = await self.websession.request(
method, url, data=payload, headers=headers, params=params
)
answer = await response.json()
_LOGGER.debug("Read from microsoft face api: %s", answer)
if response.status < 300:
return answer
_LOGGER.warning(
"Error %d microsoft face api %s", response.status, response.url
)
raise HomeAssistantError(answer["error"]["message"])
except aiohttp.ClientError:
_LOGGER.warning("Can't connect to microsoft face api")
except TimeoutError:
_LOGGER.warning("Timeout from microsoft face api %s", response.url)
raise HomeAssistantError("Network error on microsoft face api.")
@@ -1,22 +0,0 @@
{
"services": {
"create_group": {
"service": "mdi:account-multiple-plus"
},
"create_person": {
"service": "mdi:account-plus"
},
"delete_group": {
"service": "mdi:account-multiple-remove"
},
"delete_person": {
"service": "mdi:account-remove"
},
"face_person": {
"service": "mdi:face-man"
},
"train_group": {
"service": "mdi:account-multiple-check"
}
}
}
@@ -1,9 +0,0 @@
{
"domain": "microsoft_face",
"name": "Microsoft Face",
"codeowners": [],
"dependencies": ["camera"],
"documentation": "https://www.home-assistant.io/integrations/microsoft_face",
"iot_class": "cloud_push",
"quality_scale": "legacy"
}
@@ -1,62 +0,0 @@
create_group:
fields:
name:
required: true
example: family
selector:
text:
create_person:
fields:
group:
required: true
example: family
selector:
text:
name:
required: true
example: Hans
selector:
text:
delete_group:
fields:
name:
required: true
example: family
selector:
text:
delete_person:
fields:
group:
required: true
example: family
selector:
text:
name:
required: true
example: Hans
selector:
text:
face_person:
fields:
camera_entity:
required: true
example: camera.door
selector:
text:
group:
required: true
example: family
selector:
text:
person:
required: true
example: Hans
selector:
text:
train_group:
fields:
group:
required: true
example: family
selector:
text:
@@ -1,80 +0,0 @@
{
"services": {
"create_group": {
"description": "Creates a new person group.",
"fields": {
"name": {
"description": "Name of the group.",
"name": "[%key:common::config_flow::data::name%]"
}
},
"name": "Create group"
},
"create_person": {
"description": "Creates a new person in the group.",
"fields": {
"group": {
"description": "Name of the group.",
"name": "Group"
},
"name": {
"description": "Name of the person.",
"name": "[%key:common::config_flow::data::name%]"
}
},
"name": "Create person"
},
"delete_group": {
"description": "Deletes a new person group.",
"fields": {
"name": {
"description": "Name of the group.",
"name": "[%key:common::config_flow::data::name%]"
}
},
"name": "Delete group"
},
"delete_person": {
"description": "Deletes a person in the group.",
"fields": {
"group": {
"description": "Name of the group.",
"name": "Group"
},
"name": {
"description": "[%key:component::microsoft_face::services::create_person::fields::name::description%]",
"name": "[%key:common::config_flow::data::name%]"
}
},
"name": "Delete person"
},
"face_person": {
"description": "Adds a new picture to a person.",
"fields": {
"camera_entity": {
"description": "Camera to take a picture.",
"name": "Camera entity"
},
"group": {
"description": "Name of the group.",
"name": "Group"
},
"person": {
"description": "[%key:component::microsoft_face::services::create_person::fields::name::description%]",
"name": "Person"
}
},
"name": "Face person"
},
"train_group": {
"description": "Trains a person group.",
"fields": {
"group": {
"description": "Name of the group.",
"name": "Group"
}
},
"name": "Train group"
}
}
}
@@ -1 +0,0 @@
"""The microsoft_face_detect component."""
@@ -1,125 +0,0 @@
"""Component that will help set the Microsoft face detect processing."""
import logging
from typing import TYPE_CHECKING, override
import voluptuous as vol
from homeassistant.components.image_processing import (
ATTR_AGE,
ATTR_GENDER,
ATTR_GLASSES,
PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA,
FaceInformation,
ImageProcessingFaceEntity,
)
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE, MicrosoftFace
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
SUPPORTED_ATTRIBUTES = [ATTR_AGE, ATTR_GENDER, ATTR_GLASSES]
CONF_ATTRIBUTES = "attributes"
DEFAULT_ATTRIBUTES = [ATTR_AGE, ATTR_GENDER]
def validate_attributes(list_attributes):
"""Validate face attributes."""
for attr in list_attributes:
if attr not in SUPPORTED_ATTRIBUTES:
raise vol.Invalid(f"Invalid attribute {attr}")
return list_attributes
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ATTRIBUTES, default=DEFAULT_ATTRIBUTES): vol.All(
cv.ensure_list, validate_attributes
)
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Microsoft Face detection platform."""
api = hass.data[DATA_MICROSOFT_FACE]
attributes: list[str] = config[CONF_ATTRIBUTES]
source: list[dict[str, str]] = config[CONF_SOURCE]
async_add_entities(
MicrosoftFaceDetectEntity(
camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME)
)
for camera in source
)
class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity):
"""Microsoft Face API entity for identify."""
def __init__(
self,
camera_entity: str,
api: MicrosoftFace,
attributes: list[str],
name: str | None,
) -> None:
"""Initialize Microsoft Face."""
super().__init__()
self._api = api
self._attr_camera_entity = camera_entity
self._attributes = attributes
if name:
self._attr_name = name
else:
self._attr_name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}"
@override
async def async_process_image(self, image: bytes) -> None:
"""Process image.
This method is a coroutine.
"""
face_data = None
try:
face_data = await self._api.call_api(
"post",
"detect",
image,
binary=True,
params={"returnFaceAttributes": ",".join(self._attributes)},
)
except HomeAssistantError as err:
_LOGGER.error("Can't process image on microsoft face: %s", err)
return
if not face_data:
face_data = []
faces: list[FaceInformation] = []
for face in face_data:
face_attr = FaceInformation()
for attr in self._attributes:
if TYPE_CHECKING:
assert attr in SUPPORTED_ATTRIBUTES
if attr in face["faceAttributes"]:
face_attr[attr] = face["faceAttributes"][attr] # type: ignore[literal-required]
if face_attr:
faces.append(face_attr)
self.async_process_faces(faces, len(face_data))
@@ -1,9 +0,0 @@
{
"domain": "microsoft_face_detect",
"name": "Microsoft Face Detect",
"codeowners": [],
"dependencies": ["microsoft_face"],
"documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect",
"iot_class": "cloud_push",
"quality_scale": "legacy"
}
@@ -1 +0,0 @@
"""The microsoft_face_identify component."""
@@ -1,121 +0,0 @@
"""Component that will help set the Microsoft face for verify processing."""
import logging
from typing import override
import voluptuous as vol
from homeassistant.components.image_processing import (
ATTR_CONFIDENCE,
CONF_CONFIDENCE,
PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA,
FaceInformation,
ImageProcessingFaceEntity,
)
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE, MicrosoftFace
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
CONF_GROUP = "group"
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_GROUP): cv.slugify}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Microsoft Face identify platform."""
api = hass.data[DATA_MICROSOFT_FACE]
face_group: str = config[CONF_GROUP]
confidence: float = config[CONF_CONFIDENCE]
source: list[dict[str, str]] = config[CONF_SOURCE]
async_add_entities(
MicrosoftFaceIdentifyEntity(
camera[CONF_ENTITY_ID],
api,
face_group,
confidence,
camera.get(CONF_NAME),
)
for camera in source
)
class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
"""Representation of the Microsoft Face API entity for identify."""
def __init__(
self,
camera_entity: str,
api: MicrosoftFace,
face_group: str,
confidence: float,
name: str | None,
) -> None:
"""Initialize the Microsoft Face API."""
super().__init__()
self._api = api
self._attr_camera_entity = camera_entity
self._attr_confidence = confidence
self._face_group = face_group
if name:
self._attr_name = name
else:
self._attr_name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}"
@override
async def async_process_image(self, image: bytes) -> None:
"""Process image.
This method is a coroutine.
"""
detect = []
try:
face_data = await self._api.call_api("post", "detect", image, binary=True)
if face_data:
face_ids = [data["faceId"] for data in face_data]
detect = await self._api.call_api(
"post",
"identify",
{"faceIds": face_ids, "personGroupId": self._face_group},
)
except HomeAssistantError as err:
_LOGGER.error("Can't process image on Microsoft face: %s", err)
return
# Parse data
known_faces: list[FaceInformation] = []
total = 0
for face in detect:
total += 1
if not face["candidates"]:
continue
data = face["candidates"][0]
name = ""
for s_name, s_id in self._api.store[self._face_group].items():
if data["personId"] == s_id:
name = s_name
break
known_faces.append(
{ATTR_NAME: name, ATTR_CONFIDENCE: data["confidence"] * 100}
)
self.async_process_faces(known_faces, total)
@@ -1,9 +0,0 @@
{
"domain": "microsoft_face_identify",
"name": "Microsoft Face Identify",
"codeowners": [],
"dependencies": ["microsoft_face"],
"documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify",
"iot_class": "cloud_push",
"quality_scale": "legacy"
}
-18
View File
@@ -4257,24 +4257,6 @@
"iot_class": "cloud_polling",
"name": "Azure Storage"
},
"microsoft_face_detect": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face Detect"
},
"microsoft_face_identify": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face Identify"
},
"microsoft_face": {
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push",
"name": "Microsoft Face"
},
"microsoft": {
"integration_type": "hub",
"config_flow": false,
@@ -93,7 +93,6 @@ _ENTITY_COMPONENTS: set[str] = set(ENTITY_COMPONENTS).union(
"input_number",
"input_select",
"input_text",
"microsoft_face",
"person",
"plant",
"remember_the_milk",
-6
View File
@@ -584,9 +584,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"mfi",
"microbees",
"microsoft",
"microsoft_face",
"microsoft_face_detect",
"microsoft_face_identify",
"mikrotik",
"mill",
"min_max",
@@ -1541,9 +1538,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"mfi",
"microbees",
"microsoft",
"microsoft_face",
"microsoft_face_detect",
"microsoft_face_identify",
"mikrotik",
"mill",
"min_max",
@@ -1 +0,0 @@
"""Tests for the microsoft_face component."""
@@ -1,3 +0,0 @@
{
"personId": "25985303-c537-4467-b41d-bdb45cd95ca1"
}
@@ -1,12 +0,0 @@
[
{
"personGroupId": "test_group1",
"name": "test group1",
"userData": "test"
},
{
"personGroupId": "test_group2",
"name": "test group2",
"userData": "test"
}
]
@@ -1,21 +0,0 @@
[
{
"personId": "25985303-c537-4467-b41d-bdb45cd95ca1",
"name": "Ryan",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"015839fb-fbd9-4f79-ace9-7675fc2f1dd9",
"fce92aed-d578-4d2e-8114-068f8af4492e",
"b64d5e15-8257-4af2-b20a-5a750f8940e7"
]
},
{
"personId": "2ae4935b-9659-44c3-977f-61fac20d0538",
"name": "David",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"30ea1073-cc9e-4652-b1e3-d08fb7b95315",
"fbd2a038-dbff-452c-8e79-2ee81b1aa84e"
]
}
]
@@ -1,355 +0,0 @@
"""The tests for the microsoft face platform."""
from unittest.mock import patch
import pytest
from homeassistant.components import camera, microsoft_face as mf
from homeassistant.components.microsoft_face import (
ATTR_CAMERA_ENTITY,
ATTR_GROUP,
ATTR_PERSON,
DOMAIN,
SERVICE_CREATE_GROUP,
SERVICE_CREATE_PERSON,
SERVICE_DELETE_GROUP,
SERVICE_DELETE_PERSON,
SERVICE_FACE_PERSON,
SERVICE_TRAIN_GROUP,
)
from homeassistant.const import ATTR_NAME
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component, async_load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
def create_group(hass: HomeAssistant, name: str) -> None:
"""Create a new person group.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_NAME: name}
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_CREATE_GROUP, data))
def delete_group(hass: HomeAssistant, name: str) -> None:
"""Delete a person group.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_NAME: name}
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DELETE_GROUP, data))
def train_group(hass: HomeAssistant, group: str) -> None:
"""Train a person group.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_GROUP: group}
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TRAIN_GROUP, data))
def create_person(hass: HomeAssistant, group: str, name: str) -> None:
"""Create a person in a group.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_GROUP: group, ATTR_NAME: name}
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_CREATE_PERSON, data)
)
def delete_person(hass: HomeAssistant, group: str, name: str) -> None:
"""Delete a person in a group.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_GROUP: group, ATTR_NAME: name}
hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_DELETE_PERSON, data)
)
def face_person(
hass: HomeAssistant, group: str, person: str, camera_entity: str
) -> None:
"""Add a new face picture to a person.
This is a legacy helper method. Do not use it for new tests.
"""
data = {ATTR_GROUP: group, ATTR_PERSON: person, ATTR_CAMERA_ENTITY: camera_entity}
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_FACE_PERSON, data))
CONFIG = {mf.DOMAIN: {"api_key": "12345678abcdef"}}
ENDPOINT_URL = f"https://westus.{mf.FACE_API_URL}"
@pytest.fixture
def mock_update():
"""Mock update store."""
with patch(
"homeassistant.components.microsoft_face.MicrosoftFace.update_store",
return_value=None,
) as mock_update_store:
yield mock_update_store
async def test_setup_component(hass: HomeAssistant, mock_update) -> None:
"""Set up component."""
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
async def test_setup_component_wrong_api_key(hass: HomeAssistant, mock_update) -> None:
"""Set up component without api key."""
with assert_setup_component(0, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, {mf.DOMAIN: {}})
async def test_setup_component_test_service(hass: HomeAssistant, mock_update) -> None:
"""Set up component."""
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
assert hass.services.has_service(mf.DOMAIN, "create_group")
assert hass.services.has_service(mf.DOMAIN, "delete_group")
assert hass.services.has_service(mf.DOMAIN, "train_group")
assert hass.services.has_service(mf.DOMAIN, "create_person")
assert hass.services.has_service(mf.DOMAIN, "delete_person")
assert hass.services.has_service(mf.DOMAIN, "face_person")
async def test_setup_component_test_entities(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component."""
aioclient_mock.get(
ENDPOINT_URL.format("persongroups"),
text=await async_load_fixture(hass, "persongroups.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group2/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
assert len(aioclient_mock.mock_calls) == 3
entity_group1 = hass.states.get("microsoft_face.test_group1")
entity_group2 = hass.states.get("microsoft_face.test_group2")
assert entity_group1 is not None
assert entity_group2 is not None
assert entity_group1.attributes["Ryan"] == "25985303-c537-4467-b41d-bdb45cd95ca1"
assert entity_group1.attributes["David"] == "2ae4935b-9659-44c3-977f-61fac20d0538"
assert entity_group2.attributes["Ryan"] == "25985303-c537-4467-b41d-bdb45cd95ca1"
assert entity_group2.attributes["David"] == "2ae4935b-9659-44c3-977f-61fac20d0538"
async def test_service_groups(
hass: HomeAssistant, mock_update, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test groups services."""
aioclient_mock.put(
ENDPOINT_URL.format("persongroups/service_group"),
status=200,
text="{}",
)
aioclient_mock.delete(
ENDPOINT_URL.format("persongroups/service_group"),
status=200,
text="{}",
)
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
create_group(hass, "Service Group")
await hass.async_block_till_done()
entity = hass.states.get("microsoft_face.service_group")
assert entity is not None
assert len(aioclient_mock.mock_calls) == 1
delete_group(hass, "Service Group")
await hass.async_block_till_done()
entity = hass.states.get("microsoft_face.service_group")
assert entity is None
assert len(aioclient_mock.mock_calls) == 2
async def test_service_person(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test person services."""
aioclient_mock.get(
ENDPOINT_URL.format("persongroups"),
text=await async_load_fixture(hass, "persongroups.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group2/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
assert len(aioclient_mock.mock_calls) == 3
aioclient_mock.post(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "create_person.json", DOMAIN),
)
aioclient_mock.delete(
ENDPOINT_URL.format(
"persongroups/test_group1/persons/25985303-c537-4467-b41d-bdb45cd95ca1"
),
status=200,
text="{}",
)
create_person(hass, "test group1", "Hans")
await hass.async_block_till_done()
entity_group1 = hass.states.get("microsoft_face.test_group1")
assert len(aioclient_mock.mock_calls) == 4
assert entity_group1 is not None
assert entity_group1.attributes["Hans"] == "25985303-c537-4467-b41d-bdb45cd95ca1"
delete_person(hass, "test group1", "Hans")
await hass.async_block_till_done()
entity_group1 = hass.states.get("microsoft_face.test_group1")
assert len(aioclient_mock.mock_calls) == 5
assert entity_group1 is not None
assert "Hans" not in entity_group1.attributes
async def test_service_train(
hass: HomeAssistant, mock_update, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test train groups services."""
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
aioclient_mock.post(
ENDPOINT_URL.format("persongroups/service_group/train"),
status=200,
text="{}",
)
train_group(hass, "Service Group")
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 1
async def test_service_face(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test person face services."""
aioclient_mock.get(
ENDPOINT_URL.format("persongroups"),
text=await async_load_fixture(hass, "persongroups.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group2/persons"),
text=await async_load_fixture(hass, "persons.json", DOMAIN),
)
CONFIG["camera"] = {"platform": "demo"}
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
assert len(aioclient_mock.mock_calls) == 3
aioclient_mock.post(
ENDPOINT_URL.format(
"persongroups/test_group2/persons/"
"2ae4935b-9659-44c3-977f-61fac20d0538/persistedFaces"
),
status=200,
text="{}",
)
with patch(
"homeassistant.components.camera.async_get_image",
return_value=camera.Image("image/jpeg", b"Test"),
):
face_person(hass, "test_group2", "David", "camera.demo_camera")
await hass.async_block_till_done()
assert len(aioclient_mock.mock_calls) == 4
assert aioclient_mock.mock_calls[3][2] == b"Test"
async def test_service_status_400(
hass: HomeAssistant, mock_update, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test groups services with error."""
aioclient_mock.put(
ENDPOINT_URL.format("persongroups/service_group"),
status=400,
text="{'error': {'message': 'Error'}}",
)
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
create_group(hass, "Service Group")
await hass.async_block_till_done()
entity = hass.states.get("microsoft_face.service_group")
assert entity is None
assert len(aioclient_mock.mock_calls) == 1
async def test_service_status_timeout(
hass: HomeAssistant, mock_update, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up component, test groups services with timeout."""
aioclient_mock.put(
ENDPOINT_URL.format("persongroups/service_group"),
status=400,
exc=TimeoutError(),
)
with assert_setup_component(3, mf.DOMAIN):
await async_setup_component(hass, mf.DOMAIN, CONFIG)
create_group(hass, "Service Group")
await hass.async_block_till_done()
entity = hass.states.get("microsoft_face.service_group")
assert entity is None
assert len(aioclient_mock.mock_calls) == 1
@@ -1 +0,0 @@
"""Tests for the microsoft_face_detect component."""
@@ -1,27 +0,0 @@
[
{
"faceId": "c5c24a82-6845-4031-9d5d-978df9175426",
"faceRectangle": {
"width": 78,
"height": 78,
"left": 394,
"top": 54
},
"faceAttributes": {
"age": 71.0,
"gender": "male",
"smile": 0.88,
"facialHair": {
"mustache": 0.8,
"beard": 0.1,
"sideburns": 0.02
},
"glasses": "sunglasses",
"headPose": {
"roll": 2.1,
"yaw": 3,
"pitch": 0
}
}
}
]
@@ -1,12 +0,0 @@
[
{
"personGroupId": "test_group1",
"name": "test group1",
"userData": "test"
},
{
"personGroupId": "test_group2",
"name": "test group2",
"userData": "test"
}
]
@@ -1,21 +0,0 @@
[
{
"personId": "25985303-c537-4467-b41d-bdb45cd95ca1",
"name": "Ryan",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"015839fb-fbd9-4f79-ace9-7675fc2f1dd9",
"fce92aed-d578-4d2e-8114-068f8af4492e",
"b64d5e15-8257-4af2-b20a-5a750f8940e7"
]
},
{
"personId": "2ae4935b-9659-44c3-977f-61fac20d0538",
"name": "David",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"30ea1073-cc9e-4652-b1e3-d08fb7b95315",
"fbd2a038-dbff-452c-8e79-2ee81b1aa84e"
]
}
]
@@ -1,167 +0,0 @@
"""The tests for the microsoft face detect platform."""
from unittest.mock import PropertyMock, patch
import pytest
from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN
from homeassistant.components.microsoft_face import DOMAIN as MF_DOMAIN, FACE_API_URL
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component, async_load_fixture
from tests.components.image_processing import common
from tests.test_util.aiohttp import AiohttpClientMocker
CONFIG = {
IP_DOMAIN: {
"platform": "microsoft_face_detect",
"source": {"entity_id": "camera.demo_camera", "name": "test local"},
"attributes": ["age", "gender"],
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
ENDPOINT_URL = f"https://westus.{FACE_API_URL}"
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""
with patch(
"homeassistant.components.microsoft_face.MicrosoftFace.update_store",
return_value=None,
) as mock_update_store:
yield mock_update_store
@pytest.fixture
def poll_mock():
"""Disable polling."""
with patch(
"homeassistant.components.microsoft_face_detect.image_processing."
"MicrosoftFaceDetectEntity.should_poll",
new_callable=PropertyMock(return_value=False),
):
yield
async def test_setup_platform(hass: HomeAssistant, store_mock) -> None:
"""Set up platform with one entity."""
config = {
IP_DOMAIN: {
"platform": "microsoft_face_detect",
"source": {"entity_id": "camera.demo_camera"},
"attributes": ["age", "gender"],
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
with assert_setup_component(1, IP_DOMAIN):
await async_setup_component(hass, IP_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get("image_processing.microsoftface_demo_camera")
async def test_setup_platform_name(hass: HomeAssistant, store_mock) -> None:
"""Set up platform with one entity and set name."""
config = {
IP_DOMAIN: {
"platform": "microsoft_face_detect",
"source": {"entity_id": "camera.demo_camera", "name": "test local"},
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
with assert_setup_component(1, IP_DOMAIN):
await async_setup_component(hass, IP_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get("image_processing.test_local")
async def test_ms_detect_process_image(
hass: HomeAssistant, poll_mock, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up and scan a picture and test plates from event."""
aioclient_mock.get(
ENDPOINT_URL.format("persongroups"),
text=await async_load_fixture(
hass, "persongroups.json", "microsoft_face_detect"
),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "persons.json", "microsoft_face_detect"),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group2/persons"),
text=await async_load_fixture(hass, "persons.json", "microsoft_face_detect"),
)
await async_setup_component(hass, IP_DOMAIN, CONFIG)
await hass.async_block_till_done()
state = hass.states.get("camera.demo_camera")
url = f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}"
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)
aioclient_mock.get(url, content=b"image")
aioclient_mock.post(
ENDPOINT_URL.format("detect"),
text=await async_load_fixture(hass, "detect.json", "microsoft_face_detect"),
params={"returnFaceAttributes": "age,gender"},
)
common.async_scan(hass, entity_id="image_processing.test_local")
await hass.async_block_till_done()
state = hass.states.get("image_processing.test_local")
assert len(face_events) == 1
assert state.attributes.get("total_faces") == 1
assert state.state == "1"
assert face_events[0].data["age"] == 71.0
assert face_events[0].data["gender"] == "male"
assert face_events[0].data["entity_id"] == "image_processing.test_local"
# Test that later, if a request is made that results in no face
# being detected, that this is reflected in the state object
aioclient_mock.clear_requests()
aioclient_mock.post(
ENDPOINT_URL.format("detect"),
text="[]",
params={"returnFaceAttributes": "age,gender"},
)
common.async_scan(hass, entity_id="image_processing.test_local")
await hass.async_block_till_done()
state = hass.states.get("image_processing.test_local")
# No more face events were fired
assert len(face_events) == 1
# Total faces and actual qualified number of faces reset to zero
assert state.attributes.get("total_faces") == 0
assert state.state == "0"
@@ -1 +0,0 @@
"""Tests for the microsoft_face_identify component."""
@@ -1,27 +0,0 @@
[
{
"faceId": "c5c24a82-6845-4031-9d5d-978df9175426",
"faceRectangle": {
"width": 78,
"height": 78,
"left": 394,
"top": 54
},
"faceAttributes": {
"age": 71.0,
"gender": "male",
"smile": 0.88,
"facialHair": {
"mustache": 0.8,
"beard": 0.1,
"sideburns": 0.02
},
"glasses": "sunglasses",
"headPose": {
"roll": 2.1,
"yaw": 3,
"pitch": 0
}
}
}
]
@@ -1,20 +0,0 @@
[
{
"faceId": "c5c24a82-6845-4031-9d5d-978df9175426",
"candidates": [
{
"personId": "2ae4935b-9659-44c3-977f-61fac20d0538",
"confidence": 0.92
}
]
},
{
"faceId": "c5c24a82-6825-4031-9d5d-978df0175426",
"candidates": [
{
"personId": "25985303-c537-4467-b41d-bdb45cd95ca1",
"confidence": 0.32
}
]
}
]
@@ -1,12 +0,0 @@
[
{
"personGroupId": "test_group1",
"name": "test group1",
"userData": "test"
},
{
"personGroupId": "test_group2",
"name": "test group2",
"userData": "test"
}
]
@@ -1,21 +0,0 @@
[
{
"personId": "25985303-c537-4467-b41d-bdb45cd95ca1",
"name": "Ryan",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"015839fb-fbd9-4f79-ace9-7675fc2f1dd9",
"fce92aed-d578-4d2e-8114-068f8af4492e",
"b64d5e15-8257-4af2-b20a-5a750f8940e7"
]
},
{
"personId": "2ae4935b-9659-44c3-977f-61fac20d0538",
"name": "David",
"userData": "User-provided data attached to the person",
"persistedFaceIds": [
"30ea1073-cc9e-4652-b1e3-d08fb7b95315",
"fbd2a038-dbff-452c-8e79-2ee81b1aa84e"
]
}
]
@@ -1,168 +0,0 @@
"""The tests for the microsoft face identify platform."""
from unittest.mock import PropertyMock, patch
import pytest
from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN
from homeassistant.components.microsoft_face import DOMAIN as MF_DOMAIN, FACE_API_URL
from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component, async_load_fixture
from tests.components.image_processing import common
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""
await async_setup_component(hass, "homeassistant", {})
@pytest.fixture
def store_mock():
"""Mock update store."""
with patch(
"homeassistant.components.microsoft_face.MicrosoftFace.update_store",
return_value=None,
) as mock_update_store:
yield mock_update_store
@pytest.fixture
def poll_mock():
"""Disable polling."""
with patch(
"homeassistant.components.microsoft_face_identify.image_processing."
"MicrosoftFaceIdentifyEntity.should_poll",
new_callable=PropertyMock(return_value=False),
):
yield
CONFIG = {
IP_DOMAIN: {
"platform": "microsoft_face_identify",
"source": {"entity_id": "camera.demo_camera", "name": "test local"},
"group": "Test Group1",
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
ENDPOINT_URL = f"https://westus.{FACE_API_URL}"
async def test_setup_platform(hass: HomeAssistant, store_mock) -> None:
"""Set up platform with one entity."""
config = {
IP_DOMAIN: {
"platform": "microsoft_face_identify",
"source": {"entity_id": "camera.demo_camera"},
"group": "Test Group1",
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
with assert_setup_component(1, IP_DOMAIN):
await async_setup_component(hass, IP_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get("image_processing.microsoftface_demo_camera")
async def test_setup_platform_name(hass: HomeAssistant, store_mock) -> None:
"""Set up platform with one entity and set name."""
config = {
IP_DOMAIN: {
"platform": "microsoft_face_identify",
"source": {"entity_id": "camera.demo_camera", "name": "test local"},
"group": "Test Group1",
},
"camera": {"platform": "demo"},
MF_DOMAIN: {"api_key": "12345678abcdef6"},
}
with assert_setup_component(1, IP_DOMAIN):
await async_setup_component(hass, IP_DOMAIN, config)
await hass.async_block_till_done()
assert hass.states.get("image_processing.test_local")
async def test_ms_identify_process_image(
hass: HomeAssistant, poll_mock, aioclient_mock: AiohttpClientMocker
) -> None:
"""Set up and scan a picture and test plates from event."""
aioclient_mock.get(
ENDPOINT_URL.format("persongroups"),
text=await async_load_fixture(
hass, "persongroups.json", "microsoft_face_identify"
),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group1/persons"),
text=await async_load_fixture(hass, "persons.json", "microsoft_face_identify"),
)
aioclient_mock.get(
ENDPOINT_URL.format("persongroups/test_group2/persons"),
text=await async_load_fixture(hass, "persons.json", "microsoft_face_identify"),
)
await async_setup_component(hass, IP_DOMAIN, CONFIG)
await hass.async_block_till_done()
state = hass.states.get("camera.demo_camera")
url = f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}"
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)
aioclient_mock.get(url, content=b"image")
aioclient_mock.post(
ENDPOINT_URL.format("detect"),
text=await async_load_fixture(hass, "detect.json", "microsoft_face_identify"),
)
aioclient_mock.post(
ENDPOINT_URL.format("identify"),
text=await async_load_fixture(hass, "identify.json", "microsoft_face_identify"),
)
common.async_scan(hass, entity_id="image_processing.test_local")
await hass.async_block_till_done()
state = hass.states.get("image_processing.test_local")
assert len(face_events) == 1
assert state.attributes.get("total_faces") == 2
assert state.state == "David"
assert face_events[0].data["name"] == "David"
assert face_events[0].data["confidence"] == float(92)
assert face_events[0].data["entity_id"] == "image_processing.test_local"
# Test that later, if a request is made that results in no face
# being detected, that this is reflected in the state object
aioclient_mock.clear_requests()
aioclient_mock.post(ENDPOINT_URL.format("detect"), text="[]")
common.async_scan(hass, entity_id="image_processing.test_local")
await hass.async_block_till_done()
state = hass.states.get("image_processing.test_local")
# No more face events were fired
assert len(face_events) == 1
# Total faces and actual qualified number of faces reset to zero
assert state.attributes.get("total_faces") == 0
assert state.state == STATE_UNKNOWN