Home Assistant Unofficial Reference 2024.12.1
select.py
Go to the documentation of this file.
1 """Component providing select entities for UniFi Protect."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Sequence
6 from dataclasses import dataclass
7 from enum import Enum
8 import logging
9 from typing import Any, Final
10 
11 from uiprotect.api import ProtectApiClient
12 from uiprotect.data import (
13  Camera,
14  ChimeType,
15  DoorbellMessageType,
16  Doorlock,
17  IRLEDMode,
18  Light,
19  LightModeEnableType,
20  LightModeType,
21  ModelType,
22  MountType,
23  ProtectAdoptableDeviceModel,
24  RecordingMode,
25  Sensor,
26  Viewer,
27 )
28 
29 from homeassistant.components.select import SelectEntity, SelectEntityDescription
30 from homeassistant.const import EntityCategory
31 from homeassistant.core import HomeAssistant, callback
32 from homeassistant.helpers.entity_platform import AddEntitiesCallback
33 
34 from .const import TYPE_EMPTY_VALUE
35 from .data import ProtectData, ProtectDeviceType, UFPConfigEntry
36 from .entity import (
37  PermRequired,
38  ProtectDeviceEntity,
39  ProtectEntityDescription,
40  ProtectSetableKeysMixin,
41  T,
42  async_all_device_entities,
43 )
44 from .utils import async_get_light_motion_current
45 
46 _LOGGER = logging.getLogger(__name__)
47 _KEY_LIGHT_MOTION = "light_motion"
48 
49 HDR_MODES = [
50  {"id": "always", "name": "Always On"},
51  {"id": "off", "name": "Always Off"},
52  {"id": "auto", "name": "Auto"},
53 ]
54 
55 INFRARED_MODES = [
56  {"id": IRLEDMode.AUTO.value, "name": "Auto"},
57  {"id": IRLEDMode.ON.value, "name": "Always Enable"},
58  {"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
59  {"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
60  {"id": IRLEDMode.OFF.value, "name": "Always Disable"},
61 ]
62 
63 CHIME_TYPES = [
64  {"id": ChimeType.NONE.value, "name": "None"},
65  {"id": ChimeType.MECHANICAL.value, "name": "Mechanical"},
66  {"id": ChimeType.DIGITAL.value, "name": "Digital"},
67 ]
68 
69 MOUNT_TYPES = [
70  {"id": MountType.NONE.value, "name": "None"},
71  {"id": MountType.DOOR.value, "name": "Door"},
72  {"id": MountType.WINDOW.value, "name": "Window"},
73  {"id": MountType.GARAGE.value, "name": "Garage"},
74  {"id": MountType.LEAK.value, "name": "Leak"},
75 ]
76 
77 LIGHT_MODE_MOTION = "On Motion - Always"
78 LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
79 LIGHT_MODE_DARK = "When Dark"
80 LIGHT_MODE_OFF = "Manual"
81 LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
82 
83 LIGHT_MODE_TO_SETTINGS = {
84  LIGHT_MODE_MOTION: (LightModeType.MOTION.value, LightModeEnableType.ALWAYS.value),
85  LIGHT_MODE_MOTION_DARK: (
86  LightModeType.MOTION.value,
87  LightModeEnableType.DARK.value,
88  ),
89  LIGHT_MODE_DARK: (LightModeType.WHEN_DARK.value, LightModeEnableType.DARK.value),
90  LIGHT_MODE_OFF: (LightModeType.MANUAL.value, None),
91 }
92 
93 MOTION_MODE_TO_LIGHT_MODE = [
94  {"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
95  {"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK},
96  {"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
97  {"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
98 ]
99 
100 DEVICE_RECORDING_MODES = [
101  {"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
102 ]
103 
104 DEVICE_CLASS_LCD_MESSAGE: Final = "unifiprotect__lcd_message"
105 
106 
107 @dataclass(frozen=True, kw_only=True)
109  ProtectSetableKeysMixin[T], SelectEntityDescription
110 ):
111  """Describes UniFi Protect Select entity."""
112 
113  ufp_options: list[dict[str, Any]] | None = None
114  ufp_options_fn: Callable[[ProtectApiClient], list[dict[str, Any]]] | None = None
115  ufp_enum_type: type[Enum] | None = None
116 
117 
118 def _get_viewer_options(api: ProtectApiClient) -> list[dict[str, Any]]:
119  return [
120  {"id": item.id, "name": item.name} for item in api.bootstrap.liveviews.values()
121  ]
122 
123 
124 def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]:
125  default_message = api.bootstrap.nvr.doorbell_settings.default_message_text
126  messages = api.bootstrap.nvr.doorbell_settings.all_messages
127  built_messages: list[dict[str, str]] = []
128 
129  for item in messages:
130  msg_type = item.type.value
131  if item.type is DoorbellMessageType.CUSTOM_MESSAGE:
132  msg_type = f"{DoorbellMessageType.CUSTOM_MESSAGE.value}:{item.text}"
133 
134  built_messages.append({"id": msg_type, "name": item.text})
135 
136  return [
137  {"id": "", "name": f"Default Message ({default_message})"},
138  *built_messages,
139  ]
140 
141 
142 def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
143  options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
144  options.extend(
145  {"id": camera.id, "name": camera.display_name or camera.type}
146  for camera in api.bootstrap.cameras.values()
147  )
148 
149  return options
150 
151 
152 def _get_viewer_current(obj: Viewer) -> str:
153  return obj.liveview_id
154 
155 
156 def _get_doorbell_current(obj: Camera) -> str | None:
157  if obj.lcd_message is None:
158  return None
159  return obj.lcd_message.text
160 
161 
162 async def _set_light_mode(obj: Light, mode: str) -> None:
163  lightmode, timing = LIGHT_MODE_TO_SETTINGS[mode]
164  await obj.set_light_settings(
165  LightModeType(lightmode),
166  enable_at=None if timing is None else LightModeEnableType(timing),
167  )
168 
169 
170 async def _set_paired_camera(obj: Light | Sensor | Doorlock, camera_id: str) -> None:
171  if camera_id == TYPE_EMPTY_VALUE:
172  camera: Camera | None = None
173  else:
174  camera = obj.api.bootstrap.cameras.get(camera_id)
175  await obj.set_paired_camera(camera)
176 
177 
178 async def _set_doorbell_message(obj: Camera, message: str) -> None:
179  if message.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
180  message = message.split(":")[-1]
181  await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message)
182  elif message == TYPE_EMPTY_VALUE:
183  await obj.set_lcd_text(None)
184  else:
185  await obj.set_lcd_text(DoorbellMessageType(message))
186 
187 
188 async def _set_liveview(obj: Viewer, liveview_id: str) -> None:
189  liveview = obj.api.bootstrap.liveviews[liveview_id]
190  await obj.set_liveview(liveview)
191 
192 
193 CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
195  key="recording_mode",
196  name="Recording mode",
197  icon="mdi:video-outline",
198  entity_category=EntityCategory.CONFIG,
199  ufp_options=DEVICE_RECORDING_MODES,
200  ufp_enum_type=RecordingMode,
201  ufp_value="recording_settings.mode",
202  ufp_set_method="set_recording_mode",
203  ufp_perm=PermRequired.WRITE,
204  ),
206  key="infrared",
207  name="Infrared mode",
208  icon="mdi:circle-opacity",
209  entity_category=EntityCategory.CONFIG,
210  ufp_required_field="feature_flags.has_led_ir",
211  ufp_options=INFRARED_MODES,
212  ufp_enum_type=IRLEDMode,
213  ufp_value="isp_settings.ir_led_mode",
214  ufp_set_method="set_ir_led_model",
215  ufp_perm=PermRequired.WRITE,
216  ),
217  ProtectSelectEntityDescription[Camera](
218  key="doorbell_text",
219  name="Doorbell text",
220  icon="mdi:card-text",
221  entity_category=EntityCategory.CONFIG,
222  device_class=DEVICE_CLASS_LCD_MESSAGE,
223  ufp_required_field="feature_flags.has_lcd_screen",
224  ufp_value_fn=_get_doorbell_current,
225  ufp_options_fn=_get_doorbell_options,
226  ufp_set_method_fn=_set_doorbell_message,
227  ufp_perm=PermRequired.WRITE,
228  ),
230  key="chime_type",
231  name="Chime type",
232  icon="mdi:bell",
233  entity_category=EntityCategory.CONFIG,
234  ufp_required_field="feature_flags.has_chime",
235  ufp_options=CHIME_TYPES,
236  ufp_enum_type=ChimeType,
237  ufp_value="chime_type",
238  ufp_set_method="set_chime_type",
239  ufp_perm=PermRequired.WRITE,
240  ),
242  key="hdr_mode",
243  name="HDR mode",
244  icon="mdi:brightness-7",
245  entity_category=EntityCategory.CONFIG,
246  ufp_required_field="feature_flags.has_hdr",
247  ufp_options=HDR_MODES,
248  ufp_value="hdr_mode_display",
249  ufp_set_method="set_hdr_mode",
250  ufp_perm=PermRequired.WRITE,
251  ),
252 )
253 
254 LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
255  ProtectSelectEntityDescription[Light](
256  key=_KEY_LIGHT_MOTION,
257  name="Light mode",
258  icon="mdi:spotlight",
259  entity_category=EntityCategory.CONFIG,
260  ufp_options=MOTION_MODE_TO_LIGHT_MODE,
261  ufp_value_fn=async_get_light_motion_current,
262  ufp_set_method_fn=_set_light_mode,
263  ufp_perm=PermRequired.WRITE,
264  ),
265  ProtectSelectEntityDescription[Light](
266  key="paired_camera",
267  name="Paired camera",
268  icon="mdi:cctv",
269  entity_category=EntityCategory.CONFIG,
270  ufp_value="camera_id",
271  ufp_options_fn=_get_paired_camera_options,
272  ufp_set_method_fn=_set_paired_camera,
273  ufp_perm=PermRequired.WRITE,
274  ),
275 )
276 
277 SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
279  key="mount_type",
280  name="Mount type",
281  icon="mdi:screwdriver",
282  entity_category=EntityCategory.CONFIG,
283  ufp_options=MOUNT_TYPES,
284  ufp_enum_type=MountType,
285  ufp_value="mount_type",
286  ufp_set_method="set_mount_type",
287  ufp_perm=PermRequired.WRITE,
288  ),
289  ProtectSelectEntityDescription[Sensor](
290  key="paired_camera",
291  name="Paired camera",
292  icon="mdi:cctv",
293  entity_category=EntityCategory.CONFIG,
294  ufp_value="camera_id",
295  ufp_options_fn=_get_paired_camera_options,
296  ufp_set_method_fn=_set_paired_camera,
297  ufp_perm=PermRequired.WRITE,
298  ),
299 )
300 
301 DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
302  ProtectSelectEntityDescription[Doorlock](
303  key="paired_camera",
304  name="Paired camera",
305  icon="mdi:cctv",
306  entity_category=EntityCategory.CONFIG,
307  ufp_value="camera_id",
308  ufp_options_fn=_get_paired_camera_options,
309  ufp_set_method_fn=_set_paired_camera,
310  ufp_perm=PermRequired.WRITE,
311  ),
312 )
313 
314 VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
315  ProtectSelectEntityDescription[Viewer](
316  key="viewer",
317  name="Liveview",
318  icon="mdi:view-dashboard",
319  entity_category=None,
320  ufp_options_fn=_get_viewer_options,
321  ufp_value_fn=_get_viewer_current,
322  ufp_set_method_fn=_set_liveview,
323  ufp_perm=PermRequired.WRITE,
324  ),
325 )
326 
327 _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = {
328  ModelType.CAMERA: CAMERA_SELECTS,
329  ModelType.LIGHT: LIGHT_SELECTS,
330  ModelType.SENSOR: SENSE_SELECTS,
331  ModelType.VIEWPORT: VIEWER_SELECTS,
332  ModelType.DOORLOCK: DOORLOCK_SELECTS,
333 }
334 
335 
337  hass: HomeAssistant, entry: UFPConfigEntry, async_add_entities: AddEntitiesCallback
338 ) -> None:
339  """Set up number entities for UniFi Protect integration."""
340  data = entry.runtime_data
341 
342  @callback
343  def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
346  data,
347  ProtectSelects,
348  model_descriptions=_MODEL_DESCRIPTIONS,
349  ufp_device=device,
350  )
351  )
352 
353  data.async_subscribe_adopt(_add_new_device)
356  data, ProtectSelects, model_descriptions=_MODEL_DESCRIPTIONS
357  )
358  )
359 
360 
362  """A UniFi Protect Select Entity."""
363 
364  device: Camera | Light | Viewer
365  entity_description: ProtectSelectEntityDescription
366  _state_attrs = ("_attr_available", "_attr_options", "_attr_current_option")
367 
368  def __init__(
369  self,
370  data: ProtectData,
371  device: Camera | Light | Viewer,
372  description: ProtectSelectEntityDescription,
373  ) -> None:
374  """Initialize the unifi protect select entity."""
375  self._async_set_options_async_set_options(data, description)
376  super().__init__(data, device, description)
377 
378  @callback
379  def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
380  super()._async_update_device_from_protect(device)
381  entity_description = self.entity_descriptionentity_description
382  # entities with categories are not exposed for voice
383  # and safe to update dynamically
384  if (
385  entity_description.entity_category is not None
386  and entity_description.ufp_options_fn is not None
387  ):
388  _LOGGER.debug(
389  "Updating dynamic select options for %s", entity_description.name
390  )
391  self._async_set_options_async_set_options(self.datadata, entity_description)
392  if (unifi_value := entity_description.get_ufp_value(device)) is None:
393  unifi_value = TYPE_EMPTY_VALUE
394  self._attr_current_option_attr_current_option = self._unifi_to_hass_options_unifi_to_hass_options.get(
395  unifi_value, unifi_value
396  )
397 
398  @callback
400  self, data: ProtectData, description: ProtectSelectEntityDescription
401  ) -> None:
402  """Set options attributes from UniFi Protect device."""
403  if (ufp_options := description.ufp_options) is not None:
404  options = ufp_options
405  else:
406  assert description.ufp_options_fn is not None
407  options = description.ufp_options_fn(data.api)
408 
409  self._attr_options_attr_options = [item["name"] for item in options]
410  self._hass_to_unifi_options_hass_to_unifi_options = {item["name"]: item["id"] for item in options}
411  self._unifi_to_hass_options_unifi_to_hass_options = {item["id"]: item["name"] for item in options}
412 
413  async def async_select_option(self, option: str) -> None:
414  """Change the Select Entity Option."""
415 
416  # Light Motion is a bit different
417  if self.entity_descriptionentity_description.key == _KEY_LIGHT_MOTION:
418  assert self.entity_descriptionentity_description.ufp_set_method_fn is not None
419  await self.entity_descriptionentity_description.ufp_set_method_fn(self.devicedevice, option)
420  return
421 
422  unifi_value = self._hass_to_unifi_options_hass_to_unifi_options[option]
423  if self.entity_descriptionentity_description.ufp_enum_type is not None:
424  unifi_value = self.entity_descriptionentity_description.ufp_enum_type(unifi_value)
425  await self.entity_descriptionentity_description.ufp_set(self.devicedevice, unifi_value)
None __init__(self, ProtectData data, Camera|Light|Viewer device, ProtectSelectEntityDescription description)
Definition: select.py:373
None _async_update_device_from_protect(self, ProtectDeviceType device)
Definition: select.py:379
None _async_set_options(self, ProtectData data, ProtectSelectEntityDescription description)
Definition: select.py:401
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[BaseProtectEntity] async_all_device_entities(ProtectData data, type[BaseProtectEntity] klass, dict[ModelType, Sequence[ProtectEntityDescription]]|None model_descriptions=None, Sequence[ProtectEntityDescription]|None all_descs=None, list[ProtectEntityDescription]|None unadopted_descs=None, ProtectAdoptableDeviceModel|None ufp_device=None)
Definition: entity.py:153
None _set_liveview(Viewer obj, str liveview_id)
Definition: select.py:188
list[dict[str, Any]] _get_paired_camera_options(ProtectApiClient api)
Definition: select.py:142
None async_setup_entry(HomeAssistant hass, UFPConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: select.py:338
list[dict[str, Any]] _get_viewer_options(ProtectApiClient api)
Definition: select.py:118
None _set_doorbell_message(Camera obj, str message)
Definition: select.py:178
str|None _get_doorbell_current(Camera obj)
Definition: select.py:156
list[dict[str, Any]] _get_doorbell_options(ProtectApiClient api)
Definition: select.py:124
None _set_light_mode(Light obj, str mode)
Definition: select.py:162
None _set_paired_camera(Light|Sensor|Doorlock obj, str camera_id)
Definition: select.py:170