mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Remove Microsoft Face (#174977)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user