Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Microsoft face recognition."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Coroutine
7 import json
8 import logging
9 from typing import Any
10 
11 import aiohttp
12 from aiohttp.hdrs import CONTENT_TYPE
13 import voluptuous as vol
14 
15 from homeassistant.components import camera
16 from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON
17 from homeassistant.core import HomeAssistant, ServiceCall
18 from homeassistant.exceptions import HomeAssistantError
19 from homeassistant.helpers.aiohttp_client import async_get_clientsession
21 from homeassistant.helpers.entity import Entity
22 from homeassistant.helpers.entity_component import EntityComponent
23 from homeassistant.helpers.typing import ConfigType
24 from homeassistant.util import slugify
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 ATTR_CAMERA_ENTITY = "camera_entity"
29 ATTR_GROUP = "group"
30 ATTR_PERSON = "person"
31 
32 CONF_AZURE_REGION = "azure_region"
33 
34 DATA_MICROSOFT_FACE = "microsoft_face"
35 DEFAULT_TIMEOUT = 10
36 DOMAIN = "microsoft_face"
37 
38 FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
39 
40 SERVICE_CREATE_GROUP = "create_group"
41 SERVICE_CREATE_PERSON = "create_person"
42 SERVICE_DELETE_GROUP = "delete_group"
43 SERVICE_DELETE_PERSON = "delete_person"
44 SERVICE_FACE_PERSON = "face_person"
45 SERVICE_TRAIN_GROUP = "train_group"
46 
47 CONFIG_SCHEMA = vol.Schema(
48  {
49  DOMAIN: vol.Schema(
50  {
51  vol.Required(CONF_API_KEY): cv.string,
52  vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string,
53  vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
54  }
55  )
56  },
57  extra=vol.ALLOW_EXTRA,
58 )
59 
60 SCHEMA_GROUP_SERVICE = vol.Schema({vol.Required(ATTR_NAME): cv.string})
61 
62 SCHEMA_PERSON_SERVICE = SCHEMA_GROUP_SERVICE.extend(
63  {vol.Required(ATTR_GROUP): cv.slugify}
64 )
65 
66 SCHEMA_FACE_SERVICE = vol.Schema(
67  {
68  vol.Required(ATTR_PERSON): cv.string,
69  vol.Required(ATTR_GROUP): cv.slugify,
70  vol.Required(ATTR_CAMERA_ENTITY): cv.entity_id,
71  }
72 )
73 
74 SCHEMA_TRAIN_SERVICE = vol.Schema({vol.Required(ATTR_GROUP): cv.slugify})
75 
76 
77 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
78  """Set up Microsoft Face."""
79  component = EntityComponent[MicrosoftFaceGroupEntity](
80  logging.getLogger(__name__), DOMAIN, hass
81  )
82  entities: dict[str, MicrosoftFaceGroupEntity] = {}
83  face = MicrosoftFace(
84  hass,
85  config[DOMAIN].get(CONF_AZURE_REGION),
86  config[DOMAIN].get(CONF_API_KEY),
87  config[DOMAIN].get(CONF_TIMEOUT),
88  component,
89  entities,
90  )
91 
92  try:
93  # read exists group/person from cloud and create entities
94  await face.update_store()
95  except HomeAssistantError as err:
96  _LOGGER.error("Can't load data from face api: %s", err)
97  return False
98 
99  hass.data[DATA_MICROSOFT_FACE] = face
100 
101  async def async_create_group(service: ServiceCall) -> None:
102  """Create a new person group."""
103  name = service.data[ATTR_NAME]
104  g_id = slugify(name)
105 
106  try:
107  await face.call_api("put", f"persongroups/{g_id}", {"name": name})
108  face.store[g_id] = {}
109  old_entity = entities.pop(g_id, None)
110  if old_entity:
111  await component.async_remove_entity(old_entity.entity_id)
112 
113  entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name)
114  await component.async_add_entities([entities[g_id]])
115  except HomeAssistantError as err:
116  _LOGGER.error("Can't create group '%s' with error: %s", g_id, err)
117 
118  hass.services.async_register(
119  DOMAIN, SERVICE_CREATE_GROUP, async_create_group, schema=SCHEMA_GROUP_SERVICE
120  )
121 
122  async def async_delete_group(service: ServiceCall) -> None:
123  """Delete a person group."""
124  g_id = slugify(service.data[ATTR_NAME])
125 
126  try:
127  await face.call_api("delete", f"persongroups/{g_id}")
128  face.store.pop(g_id)
129 
130  entity = entities.pop(g_id)
131  await component.async_remove_entity(entity.entity_id)
132  except HomeAssistantError as err:
133  _LOGGER.error("Can't delete group '%s' with error: %s", g_id, err)
134 
135  hass.services.async_register(
136  DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, schema=SCHEMA_GROUP_SERVICE
137  )
138 
139  async def async_train_group(service: ServiceCall) -> None:
140  """Train a person group."""
141  g_id = service.data[ATTR_GROUP]
142 
143  try:
144  await face.call_api("post", f"persongroups/{g_id}/train")
145  except HomeAssistantError as err:
146  _LOGGER.error("Can't train group '%s' with error: %s", g_id, err)
147 
148  hass.services.async_register(
149  DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, schema=SCHEMA_TRAIN_SERVICE
150  )
151 
152  async def async_create_person(service: ServiceCall) -> None:
153  """Create a person in a group."""
154  name = service.data[ATTR_NAME]
155  g_id = service.data[ATTR_GROUP]
156 
157  try:
158  user_data = await face.call_api(
159  "post", f"persongroups/{g_id}/persons", {"name": name}
160  )
161 
162  face.store[g_id][name] = user_data["personId"]
163  entities[g_id].async_write_ha_state()
164  except HomeAssistantError as err:
165  _LOGGER.error("Can't create person '%s' with error: %s", name, err)
166 
167  hass.services.async_register(
168  DOMAIN, SERVICE_CREATE_PERSON, async_create_person, schema=SCHEMA_PERSON_SERVICE
169  )
170 
171  async def async_delete_person(service: ServiceCall) -> None:
172  """Delete a person in a group."""
173  name = service.data[ATTR_NAME]
174  g_id = service.data[ATTR_GROUP]
175  p_id = face.store[g_id].get(name)
176 
177  try:
178  await face.call_api("delete", f"persongroups/{g_id}/persons/{p_id}")
179 
180  face.store[g_id].pop(name)
181  entities[g_id].async_write_ha_state()
182  except HomeAssistantError as err:
183  _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err)
184 
185  hass.services.async_register(
186  DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, schema=SCHEMA_PERSON_SERVICE
187  )
188 
189  async def async_face_person(service: ServiceCall) -> None:
190  """Add a new face picture to a person."""
191  g_id = service.data[ATTR_GROUP]
192  p_id = face.store[g_id].get(service.data[ATTR_PERSON])
193 
194  camera_entity = service.data[ATTR_CAMERA_ENTITY]
195 
196  try:
197  image = await camera.async_get_image(hass, camera_entity)
198 
199  await face.call_api(
200  "post",
201  f"persongroups/{g_id}/persons/{p_id}/persistedFaces",
202  image.content,
203  binary=True,
204  )
205  except HomeAssistantError as err:
206  _LOGGER.error(
207  "Can't add an image of a person '%s' with error: %s", p_id, err
208  )
209 
210  hass.services.async_register(
211  DOMAIN, SERVICE_FACE_PERSON, async_face_person, schema=SCHEMA_FACE_SERVICE
212  )
213 
214  return True
215 
216 
218  """Person-Group state/data Entity."""
219 
220  _attr_should_poll = False
221 
222  def __init__(self, hass, api, g_id, name):
223  """Initialize person/group entity."""
224  self.hasshasshass = hass
225  self._api_api = api
226  self._id_id = g_id
227  self._name_name = name
228 
229  @property
230  def name(self):
231  """Return the name of the entity."""
232  return self._name_name
233 
234  @property
235  def entity_id(self):
236  """Return entity id."""
237  return f"{DOMAIN}.{self._id}"
238 
239  @property
240  def state(self):
241  """Return the state of the entity."""
242  return len(self._api_api.store[self._id_id])
243 
244  @property
246  """Return device specific state attributes."""
247  return dict(self._api_api.store[self._id_id])
248 
249 
251  """Microsoft Face api for Home Assistant."""
252 
253  def __init__(self, hass, server_loc, api_key, timeout, component, entities):
254  """Initialize Microsoft Face api."""
255  self.hasshass = hass
256  self.websessionwebsession = async_get_clientsession(hass)
257  self.timeouttimeout = timeout
258  self._api_key_api_key = api_key
259  self._server_url_server_url = f"https://{server_loc}.{FACE_API_URL}"
260  self._store_store = {}
261  self._component: EntityComponent[MicrosoftFaceGroupEntity] = component
262  self._entities_entities = entities
263 
264  @property
265  def store(self):
266  """Store group/person data and IDs."""
267  return self._store_store
268 
269  async def update_store(self) -> None:
270  """Load all group/person data into local store."""
271  groups = await self.call_apicall_api("get", "persongroups")
272 
273  remove_tasks: list[Coroutine[Any, Any, None]] = []
274  new_entities = []
275  for group in groups:
276  g_id = group["personGroupId"]
277  self._store_store[g_id] = {}
278  old_entity = self._entities_entities.pop(g_id, None)
279  if old_entity:
280  remove_tasks.append(
281  self._component.async_remove_entity(old_entity.entity_id)
282  )
283 
284  self._entities_entities[g_id] = MicrosoftFaceGroupEntity(
285  self.hasshass, self, g_id, group["name"]
286  )
287  new_entities.append(self._entities_entities[g_id])
288 
289  persons = await self.call_apicall_api("get", f"persongroups/{g_id}/persons")
290 
291  for person in persons:
292  self._store_store[g_id][person["name"]] = person["personId"]
293 
294  if remove_tasks:
295  await asyncio.gather(*remove_tasks)
296  await self._component.async_add_entities(new_entities)
297 
298  async def call_api(self, method, function, data=None, binary=False, params=None):
299  """Make an api call."""
300  headers = {"Ocp-Apim-Subscription-Key": self._api_key_api_key}
301  url = self._server_url_server_url.format(function)
302 
303  payload = None
304  if binary:
305  headers[CONTENT_TYPE] = "application/octet-stream"
306  payload = data
307  else:
308  headers[CONTENT_TYPE] = CONTENT_TYPE_JSON
309  if data is not None:
310  payload = json.dumps(data).encode()
311  else:
312  payload = None
313 
314  try:
315  async with asyncio.timeout(self.timeouttimeout):
316  response = await getattr(self.websessionwebsession, method)(
317  url, data=payload, headers=headers, params=params
318  )
319 
320  answer = await response.json()
321 
322  _LOGGER.debug("Read from microsoft face api: %s", answer)
323  if response.status < 300:
324  return answer
325 
326  _LOGGER.warning(
327  "Error %d microsoft face api %s", response.status, response.url
328  )
329  raise HomeAssistantError(answer["error"]["message"])
330 
331  except aiohttp.ClientError:
332  _LOGGER.warning("Can't connect to microsoft face api")
333 
334  except TimeoutError:
335  _LOGGER.warning("Timeout from microsoft face api %s", response.url)
336 
337  raise HomeAssistantError("Network error on microsoft face api.")
def __init__(self, hass, server_loc, api_key, timeout, component, entities)
Definition: __init__.py:253
def call_api(self, method, function, data=None, binary=False, params=None)
Definition: __init__.py:298
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:77
None async_create_person(HomeAssistant hass, str name, *str|None user_id=None, list[str]|None device_trackers=None)
Definition: __init__.py:102
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
str slugify(str|None text, *str separator="_")
Definition: __init__.py:41