[image_processing/microsoft_face_identify] face recognition for automation (#5472)
* [image_processing/microsoft_face_verify] face recognition for automation * Add platform for microsoft face identify * add unittest for demo * Add unittest for platformpull/5135/merge
parent
c355def154
commit
b57f5728c5
|
@ -36,6 +36,7 @@ CONF_SOURCE = 'source'
|
|||
CONF_CONFIDENCE = 'confidence'
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
SOURCE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
|
@ -44,6 +45,8 @@ SOURCE_SCHEMA = vol.Schema({
|
|||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
|
||||
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||
})
|
||||
|
||||
SERVICE_SCAN_SCHEMA = vol.Schema({
|
||||
|
@ -95,6 +98,11 @@ class ImageProcessingEntity(Entity):
|
|||
"""Return camera entity id from process pictures."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for do some things."""
|
||||
return None
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -8,13 +8,17 @@ https://home-assistant.io/components/demo/
|
|||
from homeassistant.components.image_processing import ImageProcessingEntity
|
||||
from homeassistant.components.image_processing.openalpr_local import (
|
||||
ImageProcessingAlprEntity)
|
||||
from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
ImageProcessingFaceIdentifyEntity)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the demo image_processing platform."""
|
||||
add_devices([
|
||||
DemoImageProcessing('camera.demo_camera', "Demo"),
|
||||
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr")
|
||||
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
|
||||
DemoImageProcessingFaceIdentify(
|
||||
'camera.demo_camera', "Demo Face Identify")
|
||||
])
|
||||
|
||||
|
||||
|
@ -82,3 +86,39 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
|
|||
}
|
||||
|
||||
self.process_plates(demo_data, 1)
|
||||
|
||||
|
||||
class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity):
|
||||
"""Demo face identify image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
"""Initialize demo alpr."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
self._camera = camera_entity
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for send events."""
|
||||
return 80
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
demo_data = {
|
||||
'Hans': 98.34,
|
||||
'Helena': 82.53,
|
||||
'Luna': 62.53,
|
||||
}
|
||||
|
||||
self.process_faces(demo_data, 4)
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
"""
|
||||
Component that will help set the microsoft face for verify processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.microsoft_face_identify/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import split_entity_id, callback
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
|
||||
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
DEPENDENCIES = ['microsoft_face']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_IDENTIFY_FACE = 'identify_face'
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_TOTAL_FACES = 'total_faces'
|
||||
ATTR_KNOWN_FACES = 'known_faces'
|
||||
CONF_GROUP = 'group'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_GROUP): cv.slugify,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the microsoft face identify platform."""
|
||||
api = hass.data[DATA_MICROSOFT_FACE]
|
||||
face_group = config[CONF_GROUP]
|
||||
confidence = config[CONF_CONFIDENCE]
|
||||
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(MicrosoftFaceIdentifyEntity(
|
||||
camera[CONF_ENTITY_ID], api, face_group, confidence,
|
||||
camera.get(CONF_NAME)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
|
||||
|
||||
class ImageProcessingFaceIdentifyEntity(ImageProcessingEntity):
|
||||
"""Base entity class for face identify/verify image processing."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base face identify/verify entity."""
|
||||
self.known_faces = {} # last scan data
|
||||
self.total_faces = 0 # face count
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
confidence = 0
|
||||
face_name = STATE_UNKNOWN
|
||||
|
||||
# search high verify face
|
||||
for i_name, i_co in self.known_faces.items():
|
||||
if i_co > confidence:
|
||||
confidence = i_co
|
||||
face_name = i_name
|
||||
return face_name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {
|
||||
ATTR_KNOWN_FACES: self.known_faces,
|
||||
ATTR_TOTAL_FACES: self.total_faces,
|
||||
}
|
||||
|
||||
return attr
|
||||
|
||||
def process_faces(self, known, total):
|
||||
"""Send event with detected faces and store data."""
|
||||
run_callback_threadsafe(
|
||||
self.hass.loop, self.async_process_faces, known, total
|
||||
).result()
|
||||
|
||||
@callback
|
||||
def async_process_faces(self, known, total):
|
||||
"""Send event with detected faces and store data.
|
||||
|
||||
known are a dict in follow format:
|
||||
{ 'name': confidence }
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
detect = {name: confidence for name, confidence in known.items()
|
||||
if confidence >= self.confidence}
|
||||
|
||||
# send events
|
||||
for name, confidence in detect.items():
|
||||
self.hass.async_add_job(
|
||||
self.hass.bus.async_fire, EVENT_IDENTIFY_FACE, {
|
||||
ATTR_NAME: name,
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_CONFIDENCE: confidence,
|
||||
}
|
||||
)
|
||||
|
||||
# update entity store
|
||||
self.known_faces = detect
|
||||
self.total_faces = total
|
||||
|
||||
|
||||
class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity):
|
||||
"""Microsoft face api entity for identify."""
|
||||
|
||||
def __init__(self, camera_entity, api, face_group, confidence, name=None):
|
||||
"""Initialize openalpr local api."""
|
||||
super().__init__()
|
||||
|
||||
self._api = api
|
||||
self._camera = camera_entity
|
||||
self._confidence = confidence
|
||||
self._face_group = face_group
|
||||
|
||||
if name:
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "MicrosoftFace {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for send events."""
|
||||
return self._confidence
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_process_image(self, image):
|
||||
"""Process image.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
detect = None
|
||||
try:
|
||||
face_data = yield from self._api.call_api(
|
||||
'post', 'detect', image, binary=True)
|
||||
|
||||
face_ids = [data['faceId'] for data in face_data]
|
||||
|
||||
detect = yield from 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
|
||||
knwon_faces = {}
|
||||
total = 0
|
||||
for face in detect:
|
||||
total += 1
|
||||
if len(face['candidates']) == 0:
|
||||
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
|
||||
|
||||
knwon_faces[name] = data['confidence'] * 100
|
||||
|
||||
# process data
|
||||
self.async_process_faces(knwon_faces, total)
|
|
@ -41,14 +41,11 @@ OPENALPR_REGIONS = [
|
|||
]
|
||||
|
||||
CONF_REGION = 'region'
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_REGION):
|
||||
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -50,14 +50,11 @@ CONF_REGION = 'region'
|
|||
CONF_ALPR_BIN = 'alp_bin'
|
||||
|
||||
DEFAULT_BINARY = 'alpr'
|
||||
DEFAULT_CONFIDENCE = 80
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_REGION):
|
||||
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||
})
|
||||
|
||||
|
||||
|
@ -84,11 +81,6 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
|
|||
self.plates = {} # last scan data
|
||||
self.vehicles = 0 # vehicles count
|
||||
|
||||
@property
|
||||
def confidence(self):
|
||||
"""Return minimum confidence for send events."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
|
|
|
@ -0,0 +1,388 @@
|
|||
"""
|
||||
Support for microsoft face recognition.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/microsoft_face/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'microsoft_face'
|
||||
DEPENDENCIES = ['camera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FACE_API_URL = "https://westus.api.cognitive.microsoft.com/face/v1.0/{0}"
|
||||
|
||||
DATA_MICROSOFT_FACE = 'microsoft_face'
|
||||
|
||||
SERVICE_CREATE_GROUP = 'create_group'
|
||||
SERVICE_DELETE_GROUP = 'delete_group'
|
||||
SERVICE_TRAIN_GROUP = 'train_group'
|
||||
SERVICE_CREATE_PERSON = 'create_person'
|
||||
SERVICE_DELETE_PERSON = 'delete_person'
|
||||
SERVICE_FACE_PERSON = 'face_person'
|
||||
|
||||
ATTR_GROUP = 'group'
|
||||
ATTR_PERSON = 'person'
|
||||
ATTR_CAMERA_ENTITY = 'camera_entity'
|
||||
ATTR_NAME = 'name'
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): 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,
|
||||
})
|
||||
|
||||
|
||||
def create_group(hass, name):
|
||||
"""Create a new person group."""
|
||||
data = {ATTR_NAME: name}
|
||||
hass.services.call(DOMAIN, SERVICE_CREATE_GROUP, data)
|
||||
|
||||
|
||||
def delete_group(hass, name):
|
||||
"""Delete a person group."""
|
||||
data = {ATTR_NAME: name}
|
||||
hass.services.call(DOMAIN, SERVICE_DELETE_GROUP, data)
|
||||
|
||||
|
||||
def train_group(hass, group):
|
||||
"""Train a person group."""
|
||||
data = {ATTR_GROUP: group}
|
||||
hass.services.call(DOMAIN, SERVICE_TRAIN_GROUP, data)
|
||||
|
||||
|
||||
def create_person(hass, group, name):
|
||||
"""Create a person in a group."""
|
||||
data = {ATTR_GROUP: group, ATTR_NAME: name}
|
||||
hass.services.call(DOMAIN, SERVICE_CREATE_PERSON, data)
|
||||
|
||||
|
||||
def delete_person(hass, group, name):
|
||||
"""Delete a person in a group."""
|
||||
data = {ATTR_GROUP: group, ATTR_NAME: name}
|
||||
hass.services.call(DOMAIN, SERVICE_DELETE_PERSON, data)
|
||||
|
||||
|
||||
def face_person(hass, group, person, camera_entity):
|
||||
"""Add a new face picture to a person."""
|
||||
data = {ATTR_GROUP: group, ATTR_PERSON: person,
|
||||
ATTR_CAMERA_ENTITY: camera_entity}
|
||||
hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup microsoft face."""
|
||||
entities = {}
|
||||
face = MicrosoftFace(
|
||||
hass,
|
||||
config[DOMAIN].get(CONF_API_KEY),
|
||||
config[DOMAIN].get(CONF_TIMEOUT),
|
||||
entities
|
||||
)
|
||||
|
||||
try:
|
||||
# read exists group/person from cloud and create entities
|
||||
yield from 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
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_create_group(service):
|
||||
"""Create a new person group."""
|
||||
name = service.data[ATTR_NAME]
|
||||
g_id = slugify(name)
|
||||
|
||||
try:
|
||||
yield from face.call_api(
|
||||
'put', "persongroups/{0}".format(g_id), {'name': name})
|
||||
face.store[g_id] = {}
|
||||
|
||||
entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name)
|
||||
yield from entities[g_id].async_update_ha_state()
|
||||
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,
|
||||
descriptions[DOMAIN].get(SERVICE_CREATE_GROUP),
|
||||
schema=SCHEMA_GROUP_SERVICE)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_delete_group(service):
|
||||
"""Delete a person group."""
|
||||
g_id = slugify(service.data[ATTR_NAME])
|
||||
|
||||
try:
|
||||
yield from face.call_api('delete', "persongroups/{0}".format(g_id))
|
||||
face.store.pop(g_id)
|
||||
|
||||
entity = entities.pop(g_id)
|
||||
yield from entity.async_remove()
|
||||
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,
|
||||
descriptions[DOMAIN].get(SERVICE_DELETE_GROUP),
|
||||
schema=SCHEMA_GROUP_SERVICE)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_train_group(service):
|
||||
"""Train a person group."""
|
||||
g_id = service.data[ATTR_GROUP]
|
||||
|
||||
try:
|
||||
yield from face.call_api(
|
||||
'post', "persongroups/{0}/train".format(g_id))
|
||||
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,
|
||||
descriptions[DOMAIN].get(SERVICE_TRAIN_GROUP),
|
||||
schema=SCHEMA_TRAIN_SERVICE)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_create_person(service):
|
||||
"""Create a person in a group."""
|
||||
name = service.data[ATTR_NAME]
|
||||
g_id = service.data[ATTR_GROUP]
|
||||
|
||||
try:
|
||||
user_data = yield from face.call_api(
|
||||
'post', "persongroups/{0}/persons".format(g_id), {'name': name}
|
||||
)
|
||||
|
||||
face.store[g_id][name] = user_data['personId']
|
||||
yield from entities[g_id].async_update_ha_state()
|
||||
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,
|
||||
descriptions[DOMAIN].get(SERVICE_CREATE_PERSON),
|
||||
schema=SCHEMA_PERSON_SERVICE)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_delete_person(service):
|
||||
"""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:
|
||||
yield from face.call_api(
|
||||
'delete', "persongroups/{0}/persons/{1}".format(g_id, p_id))
|
||||
|
||||
face.store[g_id].pop(name)
|
||||
yield from entities[g_id].async_update_ha_state()
|
||||
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,
|
||||
descriptions[DOMAIN].get(SERVICE_DELETE_PERSON),
|
||||
schema=SCHEMA_PERSON_SERVICE)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_face_person(service):
|
||||
"""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]
|
||||
camera = get_component('camera')
|
||||
|
||||
try:
|
||||
image = yield from camera.async_get_image(hass, camera_entity)
|
||||
|
||||
yield from face.call_api(
|
||||
'post',
|
||||
"persongroups/{0}/persons/{1}/persistedFaces".format(
|
||||
g_id, p_id),
|
||||
image,
|
||||
binary=True
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't delete person '%s' with error: %s", p_id, err)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_FACE_PERSON, async_face_person,
|
||||
descriptions[DOMAIN].get(SERVICE_FACE_PERSON),
|
||||
schema=SCHEMA_FACE_SERVICE)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MicrosoftFaceGroupEntity(Entity):
|
||||
"""Person-Group state/data Entity."""
|
||||
|
||||
def __init__(self, hass, api, g_id, name):
|
||||
"""Initialize person/group entity."""
|
||||
self.hass = hass
|
||||
self._api = api
|
||||
self._id = g_id
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return entity id."""
|
||||
return "{0}.{1}".format(DOMAIN, self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return len(self._api.store[self._id])
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
for name, p_id in self._api.store[self._id].items():
|
||||
attr[name] = p_id
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class MicrosoftFace(object):
|
||||
"""Microsoft Face api for HomeAssistant."""
|
||||
|
||||
def __init__(self, hass, api_key, timeout, entities):
|
||||
"""Initialize Microsoft Face api."""
|
||||
self.hass = hass
|
||||
self.websession = async_get_clientsession(hass)
|
||||
self.timeout = timeout
|
||||
self._api_key = api_key
|
||||
self._store = {}
|
||||
self._entities = entities
|
||||
|
||||
@property
|
||||
def store(self):
|
||||
"""Store group/person data and IDs."""
|
||||
return self._store
|
||||
|
||||
@asyncio.coroutine
|
||||
def update_store(self):
|
||||
"""Load all group/person data into local store."""
|
||||
groups = yield from self.call_api('get', 'persongroups')
|
||||
|
||||
tasks = []
|
||||
for group in groups:
|
||||
g_id = group['personGroupId']
|
||||
self._store[g_id] = {}
|
||||
self._entities[g_id] = MicrosoftFaceGroupEntity(
|
||||
self.hass, self, g_id, group['name'])
|
||||
|
||||
persons = yield from self.call_api(
|
||||
'get', "persongroups/{0}/persons".format(g_id))
|
||||
|
||||
for person in persons:
|
||||
self._store[g_id][person['name']] = person['personId']
|
||||
|
||||
tasks.append(self._entities[g_id].async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def call_api(self, method, function, data=None, binary=False,
|
||||
params=None):
|
||||
"""Make a api call."""
|
||||
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
|
||||
url = FACE_API_URL.format(function)
|
||||
|
||||
payload = None
|
||||
if binary:
|
||||
headers[CONTENT_TYPE] = "application/octet-stream"
|
||||
payload = data
|
||||
else:
|
||||
headers[CONTENT_TYPE] = "application/json"
|
||||
if data is not None:
|
||||
payload = json.dumps(data).encode()
|
||||
else:
|
||||
payload = None
|
||||
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
|
||||
response = yield from getattr(self.websession, method)(
|
||||
url, data=payload, headers=headers, params=params)
|
||||
|
||||
answer = yield from response.json()
|
||||
_LOGGER.debug("Read from microsoft face api: %s", answer)
|
||||
if response.status == 200:
|
||||
return answer
|
||||
|
||||
_LOGGER.warning("Error %d microsoft face api %s",
|
||||
response.status, response.url)
|
||||
raise HomeAssistantError(answer['error']['message'])
|
||||
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError):
|
||||
_LOGGER.warning("Can't connect to microsoft face api")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Timeout from microsoft face api %s", response.url)
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
raise HomeAssistantError("Network error on microsoft face api.")
|
|
@ -133,17 +133,70 @@ homematic:
|
|||
reconnect:
|
||||
description: Reconnect to all Homematic Hubs.
|
||||
|
||||
openalpr:
|
||||
scan:
|
||||
description: Scan immediately a device.
|
||||
microsoft_face:
|
||||
create_group:
|
||||
description: Create a new person group.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to scan
|
||||
example: 'openalpr.garage'
|
||||
name:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
restart:
|
||||
description: Restart ffmpeg process of device.
|
||||
delete_group:
|
||||
description: Delete a new person group.
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
train_group:
|
||||
description: Train a person group.
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
create_person:
|
||||
description: Create a new person in the group.
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: Name of the person
|
||||
example: 'Hans'
|
||||
|
||||
group:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
delete_person:
|
||||
description: Delete a person in the group.
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: Name of the person
|
||||
example: 'Hans'
|
||||
|
||||
group:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
face_person:
|
||||
description: Add a new picture to a person.
|
||||
|
||||
fields:
|
||||
name:
|
||||
description: Name of the person
|
||||
example: 'Hans'
|
||||
|
||||
group:
|
||||
description: Name of the group
|
||||
example: 'family'
|
||||
|
||||
camera_entity:
|
||||
description: Camera to take a picture
|
||||
example: camera.door
|
||||
|
||||
verisure:
|
||||
capture_smartcam:
|
||||
|
|
|
@ -213,3 +213,64 @@ class TestImageProcessingAlpr(object):
|
|||
assert event_data[0]['plate'] == 'AC3829'
|
||||
assert event_data[0]['confidence'] == 98.3
|
||||
assert event_data[0]['entity_id'] == 'image_processing.demo_alpr'
|
||||
|
||||
|
||||
class TestImageProcessingFaceIdentify(object):
|
||||
"""Test class for image processing."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
config = {
|
||||
ip.DOMAIN: {
|
||||
'platform': 'demo'
|
||||
},
|
||||
'camera': {
|
||||
'platform': 'demo'
|
||||
},
|
||||
}
|
||||
|
||||
with patch('homeassistant.components.image_processing.demo.'
|
||||
'DemoImageProcessingFaceIdentify.should_poll',
|
||||
new_callable=PropertyMock(return_value=False)):
|
||||
setup_component(self.hass, ip.DOMAIN, config)
|
||||
|
||||
state = self.hass.states.get('camera.demo_camera')
|
||||
self.url = "{0}{1}".format(
|
||||
self.hass.config.api.base_url,
|
||||
state.attributes.get(ATTR_ENTITY_PICTURE))
|
||||
|
||||
self.face_events = []
|
||||
|
||||
@callback
|
||||
def mock_face_event(event):
|
||||
"""Mock event."""
|
||||
self.face_events.append(event)
|
||||
|
||||
self.hass.bus.listen('identify_face', mock_face_event)
|
||||
|
||||
def teardown_method(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_face_event_call(self, aioclient_mock):
|
||||
"""Setup and scan a picture and test faces from event."""
|
||||
aioclient_mock.get(self.url, content=b'image')
|
||||
|
||||
ip.scan(self.hass, entity_id='image_processing.demo_face_identify')
|
||||
self.hass.block_till_done()
|
||||
|
||||
state = self.hass.states.get('image_processing.demo_face_identify')
|
||||
|
||||
assert len(self.face_events) == 2
|
||||
assert state.state == 'Hans'
|
||||
assert state.attributes['total_faces'] == 4
|
||||
|
||||
event_data = [event.data for event in self.face_events if
|
||||
event.data.get('name') == 'Hans']
|
||||
assert len(event_data) == 1
|
||||
assert event_data[0]['name'] == 'Hans'
|
||||
assert event_data[0]['confidence'] == 98.34
|
||||
assert event_data[0]['entity_id'] == \
|
||||
'image_processing.demo_face_identify'
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
"""The tests for the microsoft face identify platform."""
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.bootstrap import setup_component
|
||||
import homeassistant.components.image_processing as ip
|
||||
import homeassistant.components.microsoft_face as mf
|
||||
|
||||
from tests.common import (
|
||||
get_test_home_assistant, assert_setup_component, load_fixture, mock_coro)
|
||||
|
||||
|
||||
class TestMicrosoftFaceIdentifySetup(object):
|
||||
"""Test class for image processing."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_setup_platform(self, store_mock):
|
||||
"""Setup 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):
|
||||
setup_component(self.hass, ip.DOMAIN, config)
|
||||
|
||||
assert self.hass.states.get(
|
||||
'image_processing.microsoftface_demo_camera')
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_setup_platform_name(self, store_mock):
|
||||
"""Setup 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):
|
||||
setup_component(self.hass, ip.DOMAIN, config)
|
||||
|
||||
assert self.hass.states.get('image_processing.test_local')
|
||||
|
||||
|
||||
class TestMicrosoftFaceIdentify(object):
|
||||
"""Test class for image processing."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
self.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',
|
||||
}
|
||||
}
|
||||
|
||||
def teardown_method(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@patch('homeassistant.components.image_processing.microsoft_face_identify.'
|
||||
'MicrosoftFaceIdentifyEntity.should_poll',
|
||||
new_callable=PropertyMock(return_value=False))
|
||||
def test_openalpr_process_image(self, poll_mock, aioclient_mock):
|
||||
"""Setup and scan a picture and test plates from event."""
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups"),
|
||||
text=load_fixture('microsoft_face_persongroups.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group1/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group2/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
|
||||
setup_component(self.hass, ip.DOMAIN, self.config)
|
||||
|
||||
state = self.hass.states.get('camera.demo_camera')
|
||||
url = "{0}{1}".format(
|
||||
self.hass.config.api.base_url,
|
||||
state.attributes.get(ATTR_ENTITY_PICTURE))
|
||||
|
||||
face_events = []
|
||||
|
||||
@callback
|
||||
def mock_face_event(event):
|
||||
"""Mock event."""
|
||||
face_events.append(event)
|
||||
|
||||
self.hass.bus.listen('identify_face', mock_face_event)
|
||||
|
||||
aioclient_mock.get(url, content=b'image')
|
||||
|
||||
aioclient_mock.post(
|
||||
mf.FACE_API_URL.format("detect"),
|
||||
text=load_fixture('microsoft_face_detect.json')
|
||||
)
|
||||
aioclient_mock.post(
|
||||
mf.FACE_API_URL.format("identify"),
|
||||
text=load_fixture('microsoft_face_identify.json')
|
||||
)
|
||||
|
||||
ip.scan(self.hass, entity_id='image_processing.test_local')
|
||||
self.hass.block_till_done()
|
||||
|
||||
state = self.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'
|
|
@ -0,0 +1,263 @@
|
|||
"""The tests for the microsoft face platform."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
import homeassistant.components.microsoft_face as mf
|
||||
from homeassistant.bootstrap import setup_component
|
||||
|
||||
from tests.common import (
|
||||
get_test_home_assistant, assert_setup_component, mock_coro, load_fixture)
|
||||
|
||||
|
||||
class TestMicrosoftFaceSetup(object):
|
||||
"""Test the microsoft face component."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
self.config = {
|
||||
mf.DOMAIN: {
|
||||
'api_key': '12345678abcdef',
|
||||
}
|
||||
}
|
||||
|
||||
def teardown_method(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_setup_component(self, mock_update):
|
||||
"""Setup component."""
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_setup_component_wrong_api_key(self, mock_update):
|
||||
"""Setup component without api key."""
|
||||
with assert_setup_component(0, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, {mf.DOMAIN: {}})
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_setup_component_test_service(self, mock_update):
|
||||
"""Setup component."""
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'create_group')
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'delete_group')
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'train_group')
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'create_person')
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'delete_person')
|
||||
assert self.hass.services.has_service(mf.DOMAIN, 'face_person')
|
||||
|
||||
def test_setup_component_test_entities(self, aioclient_mock):
|
||||
"""Setup component."""
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups"),
|
||||
text=load_fixture('microsoft_face_persongroups.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group1/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group2/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 3
|
||||
|
||||
entity_group1 = self.hass.states.get('microsoft_face.test_group1')
|
||||
entity_group2 = self.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'
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_service_groups(self, mock_update, aioclient_mock):
|
||||
"""Setup component, test groups services."""
|
||||
aioclient_mock.put(
|
||||
mf.FACE_API_URL.format("persongroups/service_group"),
|
||||
status=200, text="{}"
|
||||
)
|
||||
aioclient_mock.delete(
|
||||
mf.FACE_API_URL.format("persongroups/service_group"),
|
||||
status=200, text="{}"
|
||||
)
|
||||
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
mf.create_group(self.hass, 'Service Group')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity = self.hass.states.get('microsoft_face.service_group')
|
||||
assert entity is not None
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
mf.delete_group(self.hass, 'Service Group')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity = self.hass.states.get('microsoft_face.service_group')
|
||||
assert entity is None
|
||||
assert len(aioclient_mock.mock_calls) == 2
|
||||
|
||||
def test_service_person(self, aioclient_mock):
|
||||
"""Setup component, test person services."""
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups"),
|
||||
text=load_fixture('microsoft_face_persongroups.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group1/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group2/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 3
|
||||
|
||||
aioclient_mock.post(
|
||||
mf.FACE_API_URL.format("persongroups/test_group1/persons"),
|
||||
text=load_fixture('microsoft_face_create_person.json')
|
||||
)
|
||||
aioclient_mock.delete(
|
||||
mf.FACE_API_URL.format(
|
||||
"persongroups/test_group1/persons/"
|
||||
"25985303-c537-4467-b41d-bdb45cd95ca1"),
|
||||
status=200, text="{}"
|
||||
)
|
||||
|
||||
mf.create_person(self.hass, 'test group1', 'Hans')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity_group1 = self.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'
|
||||
|
||||
mf.delete_person(self.hass, 'test group1', 'Hans')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity_group1 = self.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
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_service_train(self, mock_update, aioclient_mock):
|
||||
"""Setup component, test train groups services."""
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
aioclient_mock.post(
|
||||
mf.FACE_API_URL.format("persongroups/service_group/train"),
|
||||
status=200, text="{}"
|
||||
)
|
||||
|
||||
mf.train_group(self.hass, 'Service Group')
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
@patch('homeassistant.components.camera.async_get_image',
|
||||
return_value=mock_coro(return_value=b'Test')())
|
||||
def test_service_face(self, camera_mock, aioclient_mock):
|
||||
"""Setup component, test person face services."""
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups"),
|
||||
text=load_fixture('microsoft_face_persongroups.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group1/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mf.FACE_API_URL.format("persongroups/test_group2/persons"),
|
||||
text=load_fixture('microsoft_face_persons.json')
|
||||
)
|
||||
|
||||
self.config['camera'] = {'platform': 'demo'}
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 3
|
||||
|
||||
aioclient_mock.post(
|
||||
mf.FACE_API_URL.format(
|
||||
"persongroups/test_group2/persons/"
|
||||
"2ae4935b-9659-44c3-977f-61fac20d0538/persistedFaces"),
|
||||
status=200, text="{}"
|
||||
)
|
||||
|
||||
mf.face_person(
|
||||
self.hass, 'test_group2', 'David', 'camera.demo_camera')
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 4
|
||||
assert aioclient_mock.mock_calls[3][2] == b'Test'
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_service_status_400(self, mock_update, aioclient_mock):
|
||||
"""Setup component, test groups services with error."""
|
||||
aioclient_mock.put(
|
||||
mf.FACE_API_URL.format("persongroups/service_group"),
|
||||
status=400, text="{'error': {'message': 'Error'}}"
|
||||
)
|
||||
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
mf.create_group(self.hass, 'Service Group')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity = self.hass.states.get('microsoft_face.service_group')
|
||||
assert entity is None
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
|
||||
@patch('homeassistant.components.microsoft_face.'
|
||||
'MicrosoftFace.update_store', return_value=mock_coro()())
|
||||
def test_service_status_timeout(self, mock_update, aioclient_mock):
|
||||
"""Setup component, test groups services with timeout."""
|
||||
aioclient_mock.put(
|
||||
mf.FACE_API_URL.format("persongroups/service_group"),
|
||||
status=400, exc=asyncio.TimeoutError()
|
||||
)
|
||||
|
||||
with assert_setup_component(2, mf.DOMAIN):
|
||||
setup_component(self.hass, mf.DOMAIN, self.config)
|
||||
|
||||
mf.create_group(self.hass, 'Service Group')
|
||||
self.hass.block_till_done()
|
||||
|
||||
entity = self.hass.states.get('microsoft_face.service_group')
|
||||
assert entity is None
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"personId":"25985303-c537-4467-b41d-bdb45cd95ca1"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"personGroupId":"test_group1",
|
||||
"name":"test group1",
|
||||
"userData":"test"
|
||||
},
|
||||
{
|
||||
"personGroupId":"test_group2",
|
||||
"name":"test group2",
|
||||
"userData":"test"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue