Home Assistant Unofficial Reference 2024.12.1
services.py
Go to the documentation of this file.
1 """UniFi Protect Integration services."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import functools
7 from typing import Any, cast
8 
9 from pydantic import ValidationError
10 from uiprotect.api import ProtectApiClient
11 from uiprotect.data import Camera, Chime
12 from uiprotect.exceptions import ClientError
13 import voluptuous as vol
14 
15 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
16 from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME, Platform
17 from homeassistant.core import HomeAssistant, ServiceCall, callback
18 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
19 from homeassistant.helpers import (
20  config_validation as cv,
21  device_registry as dr,
22  entity_registry as er,
23 )
24 from homeassistant.helpers.service import async_extract_referenced_entity_ids
25 from homeassistant.util.read_only_dict import ReadOnlyDict
26 
27 from .const import ATTR_MESSAGE, DOMAIN
28 from .data import async_ufp_instance_for_config_entry_ids
29 
30 SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
31 SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
32 SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone"
33 SERVICE_REMOVE_PRIVACY_ZONE = "remove_privacy_zone"
34 SERVICE_SET_CHIME_PAIRED = "set_chime_paired_doorbells"
35 
36 ALL_GLOBAL_SERIVCES = [
37  SERVICE_ADD_DOORBELL_TEXT,
38  SERVICE_REMOVE_DOORBELL_TEXT,
39  SERVICE_SET_CHIME_PAIRED,
40  SERVICE_REMOVE_PRIVACY_ZONE,
41 ]
42 
43 DOORBELL_TEXT_SCHEMA = vol.All(
44  vol.Schema(
45  {
46  **cv.ENTITY_SERVICE_FIELDS,
47  vol.Required(ATTR_MESSAGE): cv.string,
48  },
49  ),
50  cv.has_at_least_one_key(ATTR_DEVICE_ID),
51 )
52 
53 CHIME_PAIRED_SCHEMA = vol.All(
54  vol.Schema(
55  {
56  **cv.ENTITY_SERVICE_FIELDS,
57  "doorbells": cv.TARGET_SERVICE_FIELDS,
58  },
59  ),
60  cv.has_at_least_one_key(ATTR_DEVICE_ID),
61 )
62 
63 REMOVE_PRIVACY_ZONE_SCHEMA = vol.All(
64  vol.Schema(
65  {
66  **cv.ENTITY_SERVICE_FIELDS,
67  vol.Required(ATTR_NAME): cv.string,
68  },
69  ),
70  cv.has_at_least_one_key(ATTR_DEVICE_ID),
71 )
72 
73 
74 @callback
75 def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient:
76  device_registry = dr.async_get(hass)
77  if not (device_entry := device_registry.async_get(device_id)):
78  raise HomeAssistantError(f"No device found for device id: {device_id}")
79 
80  if device_entry.via_device_id is not None:
81  return _async_get_ufp_instance(hass, device_entry.via_device_id)
82 
83  config_entry_ids = device_entry.config_entries
84  if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
85  return ufp_instance
86 
87  raise HomeAssistantError(f"No device found for device id: {device_id}")
88 
89 
90 @callback
91 def _async_get_ufp_camera(hass: HomeAssistant, call: ServiceCall) -> Camera:
92  ref = async_extract_referenced_entity_ids(hass, call)
93  entity_registry = er.async_get(hass)
94 
95  entity_id = ref.indirectly_referenced.pop()
96  camera_entity = entity_registry.async_get(entity_id)
97  assert camera_entity is not None
98  assert camera_entity.device_id is not None
99  camera_mac = _async_unique_id_to_mac(camera_entity.unique_id)
100 
101  instance = _async_get_ufp_instance(hass, camera_entity.device_id)
102  return cast(Camera, instance.bootstrap.get_device_from_mac(camera_mac))
103 
104 
105 @callback
107  hass: HomeAssistant, call: ServiceCall
108 ) -> set[ProtectApiClient]:
109  return {
110  _async_get_ufp_instance(hass, device_id)
111  for device_id in async_extract_referenced_entity_ids(
112  hass, call
113  ).referenced_devices
114  }
115 
116 
118  hass: HomeAssistant,
119  call: ServiceCall,
120  method: str,
121  *args: Any,
122  **kwargs: Any,
123 ) -> None:
124  instances = _async_get_protect_from_call(hass, call)
125  try:
126  await asyncio.gather(
127  *(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for i in instances)
128  )
129  except (ClientError, ValidationError) as err:
130  raise HomeAssistantError(str(err)) from err
131 
132 
133 async def add_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None:
134  """Add a custom doorbell text message."""
135  message: str = call.data[ATTR_MESSAGE]
136  await _async_service_call_nvr(hass, call, "add_custom_doorbell_message", message)
137 
138 
139 async def remove_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None:
140  """Remove a custom doorbell text message."""
141  message: str = call.data[ATTR_MESSAGE]
142  await _async_service_call_nvr(hass, call, "remove_custom_doorbell_message", message)
143 
144 
145 async def remove_privacy_zone(hass: HomeAssistant, call: ServiceCall) -> None:
146  """Remove privacy zone from camera."""
147 
148  name: str = call.data[ATTR_NAME]
149  camera = _async_get_ufp_camera(hass, call)
150 
151  remove_index: int | None = None
152  for index, zone in enumerate(camera.privacy_zones):
153  if zone.name == name:
154  remove_index = index
155  break
156 
157  if remove_index is None:
159  f"Could not find privacy zone with name {name} on camera {camera.display_name}."
160  )
161 
162  def remove_zone() -> None:
163  camera.privacy_zones.pop(remove_index)
164 
165  await camera.queue_update(remove_zone)
166 
167 
168 @callback
169 def _async_unique_id_to_mac(unique_id: str) -> str:
170  """Extract the MAC address from the registry entry unique id."""
171  return unique_id.split("_")[0]
172 
173 
174 async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) -> None:
175  """Set paired doorbells on chime."""
176  ref = async_extract_referenced_entity_ids(hass, call)
177  entity_registry = er.async_get(hass)
178 
179  entity_id = ref.indirectly_referenced.pop()
180  chime_button = entity_registry.async_get(entity_id)
181  assert chime_button is not None
182  assert chime_button.device_id is not None
183  chime_mac = _async_unique_id_to_mac(chime_button.unique_id)
184 
185  instance = _async_get_ufp_instance(hass, chime_button.device_id)
186  chime = instance.bootstrap.get_device_from_mac(chime_mac)
187  chime = cast(Chime, chime)
188  assert chime is not None
189 
190  call.data = ReadOnlyDict(call.data.get("doorbells") or {})
191  doorbell_refs = async_extract_referenced_entity_ids(hass, call)
192  doorbell_ids: set[str] = set()
193  for camera_id in doorbell_refs.referenced | doorbell_refs.indirectly_referenced:
194  doorbell_sensor = entity_registry.async_get(camera_id)
195  assert doorbell_sensor is not None
196  if (
197  doorbell_sensor.platform != DOMAIN
198  or doorbell_sensor.domain != Platform.BINARY_SENSOR
199  or doorbell_sensor.original_device_class
200  != BinarySensorDeviceClass.OCCUPANCY
201  ):
202  continue
203  doorbell_mac = _async_unique_id_to_mac(doorbell_sensor.unique_id)
204  camera = instance.bootstrap.get_device_from_mac(doorbell_mac)
205  assert camera is not None
206  doorbell_ids.add(camera.id)
207  data_before_changed = chime.dict_with_excludes()
208  chime.camera_ids = sorted(doorbell_ids)
209  await chime.save_device(data_before_changed)
210 
211 
212 def async_setup_services(hass: HomeAssistant) -> None:
213  """Set up the global UniFi Protect services."""
214  services = [
215  (
216  SERVICE_ADD_DOORBELL_TEXT,
217  functools.partial(add_doorbell_text, hass),
218  DOORBELL_TEXT_SCHEMA,
219  ),
220  (
221  SERVICE_REMOVE_DOORBELL_TEXT,
222  functools.partial(remove_doorbell_text, hass),
223  DOORBELL_TEXT_SCHEMA,
224  ),
225  (
226  SERVICE_SET_CHIME_PAIRED,
227  functools.partial(set_chime_paired_doorbells, hass),
228  CHIME_PAIRED_SCHEMA,
229  ),
230  (
231  SERVICE_REMOVE_PRIVACY_ZONE,
232  functools.partial(remove_privacy_zone, hass),
233  REMOVE_PRIVACY_ZONE_SCHEMA,
234  ),
235  ]
236  for name, method, schema in services:
237  if hass.services.has_service(DOMAIN, name):
238  continue
239  hass.services.async_register(DOMAIN, name, method, schema=schema)
ProtectApiClient|None async_ufp_instance_for_config_entry_ids(HomeAssistant hass, set[str] config_entry_ids)
Definition: data.py:345
None remove_privacy_zone(HomeAssistant hass, ServiceCall call)
Definition: services.py:145
None add_doorbell_text(HomeAssistant hass, ServiceCall call)
Definition: services.py:133
None async_setup_services(HomeAssistant hass)
Definition: services.py:212
ProtectApiClient _async_get_ufp_instance(HomeAssistant hass, str device_id)
Definition: services.py:75
None set_chime_paired_doorbells(HomeAssistant hass, ServiceCall call)
Definition: services.py:174
Camera _async_get_ufp_camera(HomeAssistant hass, ServiceCall call)
Definition: services.py:91
None remove_doorbell_text(HomeAssistant hass, ServiceCall call)
Definition: services.py:139
None _async_service_call_nvr(HomeAssistant hass, ServiceCall call, str method, *Any args, **Any kwargs)
Definition: services.py:123
set[ProtectApiClient] _async_get_protect_from_call(HomeAssistant hass, ServiceCall call)
Definition: services.py:108
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Definition: service.py:507