Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for Blink system camera."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 from requests.exceptions import ChunkedEncodingError
10 import voluptuous as vol
11 
12 from homeassistant.components.camera import Camera
13 from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
14 from homeassistant.core import HomeAssistant
15 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
16 from homeassistant.helpers import entity_platform
18 from homeassistant.helpers.device_registry import DeviceInfo
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.helpers.update_coordinator import CoordinatorEntity
21 
22 from .const import (
23  DEFAULT_BRAND,
24  DOMAIN,
25  SERVICE_RECORD,
26  SERVICE_SAVE_RECENT_CLIPS,
27  SERVICE_SAVE_VIDEO,
28  SERVICE_TRIGGER,
29 )
30 from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 ATTR_VIDEO_CLIP = "video"
35 ATTR_IMAGE = "image"
36 PARALLEL_UPDATES = 1
37 
38 
40  hass: HomeAssistant,
41  config_entry: BlinkConfigEntry,
42  async_add_entities: AddEntitiesCallback,
43 ) -> None:
44  """Set up a Blink Camera."""
45 
46  coordinator = config_entry.runtime_data
47  entities = [
48  BlinkCamera(coordinator, name, camera)
49  for name, camera in coordinator.api.cameras.items()
50  ]
51 
52  async_add_entities(entities)
53 
54  platform = entity_platform.async_get_current_platform()
55  platform.async_register_entity_service(SERVICE_RECORD, None, "record")
56  platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
57  platform.async_register_entity_service(
58  SERVICE_SAVE_RECENT_CLIPS,
59  {vol.Required(CONF_FILE_PATH): cv.string},
60  "save_recent_clips",
61  )
62  platform.async_register_entity_service(
63  SERVICE_SAVE_VIDEO,
64  {vol.Required(CONF_FILENAME): cv.string},
65  "save_video",
66  )
67 
68 
69 class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
70  """An implementation of a Blink Camera."""
71 
72  _attr_has_entity_name = True
73  _attr_name = None
74 
75  def __init__(self, coordinator: BlinkUpdateCoordinator, name, camera) -> None:
76  """Initialize a camera."""
77  super().__init__(coordinator)
78  Camera.__init__(self)
79  self._camera_camera = camera
80  self._attr_unique_id_attr_unique_id = f"{camera.serial}-camera"
81  self._attr_device_info_attr_device_info = DeviceInfo(
82  identifiers={(DOMAIN, camera.serial)},
83  serial_number=camera.serial,
84  sw_version=camera.version,
85  name=name,
86  manufacturer=DEFAULT_BRAND,
87  model=camera.camera_type,
88  )
89  _LOGGER.debug("Initialized blink camera %s", self._camera_camera.name)
90 
91  @property
92  def extra_state_attributes(self) -> Mapping[str, Any] | None:
93  """Return the camera attributes."""
94  return self._camera_camera.attributes
95 
96  async def async_enable_motion_detection(self) -> None:
97  """Enable motion detection for the camera."""
98  try:
99  await self._camera_camera.async_arm(True)
100  except TimeoutError as er:
101  raise HomeAssistantError(
102  translation_domain=DOMAIN,
103  translation_key="failed_arm",
104  ) from er
105 
106  self._camera_camera.motion_enabled = True
107  await self.coordinator.async_refresh()
108 
109  async def async_disable_motion_detection(self) -> None:
110  """Disable motion detection for the camera."""
111  try:
112  await self._camera_camera.async_arm(False)
113  except TimeoutError as er:
114  raise HomeAssistantError(
115  translation_domain=DOMAIN,
116  translation_key="failed_disarm",
117  ) from er
118 
119  self._camera_camera.motion_enabled = False
120  await self.coordinator.async_refresh()
121 
122  @property
123  def motion_detection_enabled(self) -> bool:
124  """Return the state of the camera."""
125  return self._camera_camera.arm
126 
127  @property
128  def brand(self) -> str | None:
129  """Return the camera brand."""
130  return DEFAULT_BRAND
131 
132  async def record(self) -> None:
133  """Trigger camera to record a clip."""
134  try:
135  await self._camera_camera.record()
136  except TimeoutError as er:
137  raise HomeAssistantError(
138  translation_domain=DOMAIN,
139  translation_key="failed_clip",
140  ) from er
141 
142  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
143 
144  async def trigger_camera(self) -> None:
145  """Trigger camera to take a snapshot."""
146  try:
147  await self._camera_camera.snap_picture()
148  except TimeoutError as er:
149  raise HomeAssistantError(
150  translation_domain=DOMAIN,
151  translation_key="failed_snap",
152  ) from er
153 
154  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
155 
157  self, width: int | None = None, height: int | None = None
158  ) -> bytes | None:
159  """Return a still image response from the camera."""
160  try:
161  return self._camera_camera.image_from_cache
162  except ChunkedEncodingError:
163  _LOGGER.debug("Could not retrieve image for %s", self._camera_camera.name)
164  return None
165  except TypeError:
166  _LOGGER.debug("No cached image for %s", self._camera_camera.name)
167  return None
168 
169  async def save_recent_clips(self, file_path) -> None:
170  """Save multiple recent clips to output directory."""
171  if not self.hasshasshass.config.is_allowed_path(file_path):
173  translation_domain=DOMAIN,
174  translation_key="no_path",
175  translation_placeholders={"target": file_path},
176  )
177 
178  try:
179  await self._camera_camera.save_recent_clips(output_dir=file_path)
180  except OSError as err:
182  str(err),
183  translation_domain=DOMAIN,
184  translation_key="cant_write",
185  ) from err
186 
187  async def save_video(self, filename) -> None:
188  """Handle save video service calls."""
189  if not self.hasshasshass.config.is_allowed_path(filename):
191  translation_domain=DOMAIN,
192  translation_key="no_path",
193  translation_placeholders={"target": filename},
194  )
195 
196  try:
197  await self._camera_camera.video_to_file(filename)
198  except OSError as err:
200  str(err),
201  translation_domain=DOMAIN,
202  translation_key="cant_write",
203  ) from err