Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for Amcrest IP cameras."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable
7 from datetime import timedelta
8 import logging
9 from typing import TYPE_CHECKING, Any
10 
11 import aiohttp
12 from aiohttp import web
13 from amcrest import AmcrestError
14 from haffmpeg.camera import CameraMjpeg
15 import voluptuous as vol
16 
17 from homeassistant.components.camera import Camera, CameraEntityFeature
18 from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager
19 from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.helpers import config_validation as cv
23  async_aiohttp_proxy_stream,
24  async_aiohttp_proxy_web,
25  async_get_clientsession,
26 )
27 from homeassistant.helpers.dispatcher import async_dispatcher_connect
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
30 
31 from .const import (
32  CAMERA_WEB_SESSION_TIMEOUT,
33  CAMERAS,
34  COMM_TIMEOUT,
35  DATA_AMCREST,
36  DEVICES,
37  RESOLUTION_TO_STREAM,
38  SERVICE_UPDATE,
39  SNAPSHOT_TIMEOUT,
40 )
41 from .helpers import log_update_error, service_signal
42 
43 if TYPE_CHECKING:
44  from . import AmcrestDevice
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 SCAN_INTERVAL = timedelta(seconds=15)
49 
50 STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"]
51 
52 _SRV_EN_REC = "enable_recording"
53 _SRV_DS_REC = "disable_recording"
54 _SRV_EN_AUD = "enable_audio"
55 _SRV_DS_AUD = "disable_audio"
56 _SRV_EN_MOT_REC = "enable_motion_recording"
57 _SRV_DS_MOT_REC = "disable_motion_recording"
58 _SRV_GOTO = "goto_preset"
59 _SRV_CBW = "set_color_bw"
60 _SRV_TOUR_ON = "start_tour"
61 _SRV_TOUR_OFF = "stop_tour"
62 
63 _SRV_PTZ_CTRL = "ptz_control"
64 _ATTR_PTZ_TT = "travel_time"
65 _ATTR_PTZ_MOV = "movement"
66 _MOV = [
67  "zoom_out",
68  "zoom_in",
69  "right",
70  "left",
71  "up",
72  "down",
73  "right_down",
74  "right_up",
75  "left_down",
76  "left_up",
77 ]
78 _ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"]
79 _MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"]
80 _MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"]
81 _ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS
82 
83 _DEFAULT_TT = 0.2
84 
85 _ATTR_PRESET = "preset"
86 _ATTR_COLOR_BW = "color_bw"
87 
88 _CBW_COLOR = "color"
89 _CBW_AUTO = "auto"
90 _CBW_BW = "bw"
91 _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
92 
93 _SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids})
94 _SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend(
95  {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))}
96 )
97 _SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)})
98 _SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend(
99  {
100  vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV),
101  vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
102  }
103 )
104 
105 CAMERA_SERVICES = {
106  _SRV_EN_REC: (_SRV_SCHEMA, "async_enable_recording", ()),
107  _SRV_DS_REC: (_SRV_SCHEMA, "async_disable_recording", ()),
108  _SRV_EN_AUD: (_SRV_SCHEMA, "async_enable_audio", ()),
109  _SRV_DS_AUD: (_SRV_SCHEMA, "async_disable_audio", ()),
110  _SRV_EN_MOT_REC: (_SRV_SCHEMA, "async_enable_motion_recording", ()),
111  _SRV_DS_MOT_REC: (_SRV_SCHEMA, "async_disable_motion_recording", ()),
112  _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
113  _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
114  _SRV_TOUR_ON: (_SRV_SCHEMA, "async_start_tour", ()),
115  _SRV_TOUR_OFF: (_SRV_SCHEMA, "async_stop_tour", ()),
116  _SRV_PTZ_CTRL: (
117  _SRV_PTZ_SCHEMA,
118  "async_ptz_control",
119  (_ATTR_PTZ_MOV, _ATTR_PTZ_TT),
120  ),
121 }
122 
123 _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
124 
125 
127  hass: HomeAssistant,
128  config: ConfigType,
129  async_add_entities: AddEntitiesCallback,
130  discovery_info: DiscoveryInfoType | None = None,
131 ) -> None:
132  """Set up an Amcrest IP Camera."""
133  if discovery_info is None:
134  return
135 
136  name = discovery_info[CONF_NAME]
137  device = hass.data[DATA_AMCREST][DEVICES][name]
138  entity = AmcrestCam(name, device, get_ffmpeg_manager(hass))
139 
140  async_add_entities([entity], True)
141 
142 
143 class CannotSnapshot(Exception):
144  """Conditions are not valid for taking a snapshot."""
145 
146 
147 class AmcrestCommandFailed(Exception):
148  """Amcrest camera command did not work."""
149 
150 
152  """An implementation of an Amcrest IP camera."""
153 
154  _attr_should_poll = True # Cameras default to False
155  _attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
156 
157  def __init__(self, name: str, device: AmcrestDevice, ffmpeg: FFmpegManager) -> None:
158  """Initialize an Amcrest camera."""
159  super().__init__()
160  self._name_name = name
161  self._api_api = device.api
162  self._ffmpeg_ffmpeg = ffmpeg
163  self._ffmpeg_arguments_ffmpeg_arguments = device.ffmpeg_arguments
164  self._stream_source_stream_source = device.stream_source
165  self._resolution_resolution = device.resolution
166  self._channel_channel = device.channel
167  self._token_token = self._auth_auth = device.authentication
168  self._control_light_control_light = device.control_light
169  self._is_recording: bool = False
170  self._motion_detection_enabled: bool = False
171  self._brand_brand: str | None = None
172  self._model_model: str | None = None
173  self._audio_enabled: bool | None = None
174  self._motion_recording_enabled: bool | None = None
175  self._color_bw: str | None = None
176  self._rtsp_url_rtsp_url: str | None = None
177  self._snapshot_task_snapshot_task: asyncio.tasks.Task | None = None
178  self._unsub_dispatcher: list[Callable[[], None]] = []
179 
180  def _check_snapshot_ok(self) -> None:
181  available = self.availableavailableavailableavailable
182  if not available or not self.is_onis_onis_on:
183  _LOGGER.warning(
184  "Attempt to take snapshot when %s camera is %s",
185  self.namenamename,
186  "offline" if not available else "off",
187  )
188  raise CannotSnapshot
189 
190  async def _async_get_image(self) -> bytes | None:
191  try:
192  # Send the request to snap a picture and return raw jpg data
193  # Snapshot command needs a much longer read timeout than other commands.
194  return await self._api_api.async_snapshot(
195  timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT)
196  )
197  except AmcrestError as error:
198  log_update_error(_LOGGER, "get image from", self.namenamename, "camera", error)
199  return None
200  finally:
201  self._snapshot_task_snapshot_task = None
202 
204  self, width: int | None = None, height: int | None = None
205  ) -> bytes | None:
206  """Return a still image response from the camera."""
207  _LOGGER.debug("Take snapshot from %s", self._name_name)
208  try:
209  # Amcrest cameras only support one snapshot command at a time.
210  # Hence need to wait if a previous snapshot has not yet finished.
211  # Also need to check that camera is online and turned on before each wait
212  # and before initiating snapshot.
213  while self._snapshot_task_snapshot_task:
214  self._check_snapshot_ok_check_snapshot_ok()
215  _LOGGER.debug("Waiting for previous snapshot from %s", self._name_name)
216  await self._snapshot_task_snapshot_task
217  self._check_snapshot_ok_check_snapshot_ok()
218  # Run snapshot command in separate Task that can't be cancelled so
219  # 1) it's not possible to send another snapshot command while camera is
220  # still working on a previous one, and
221  # 2) someone will be around to catch any exceptions.
222  self._snapshot_task_snapshot_task = self.hasshass.async_create_task(self._async_get_image_async_get_image())
223  return await asyncio.shield(self._snapshot_task_snapshot_task)
224  except CannotSnapshot:
225  return None
226 
228  self, request: web.Request
229  ) -> web.StreamResponse | None:
230  """Return an MJPEG stream."""
231  # The snapshot implementation is handled by the parent class
232  if self._stream_source_stream_source == "snapshot":
233  return await super().handle_async_mjpeg_stream(request)
234 
235  if not self.availableavailableavailableavailable:
236  _LOGGER.warning(
237  "Attempt to stream %s when %s camera is offline",
238  self._stream_source_stream_source,
239  self.namenamename,
240  )
241  return None
242 
243  if self._stream_source_stream_source == "mjpeg":
244  # stream an MJPEG image stream directly from the camera
245  websession = async_get_clientsession(self.hasshass)
246  streaming_url = self._api_api.mjpeg_url(typeno=self._resolution_resolution)
247  stream_coro = websession.get(
248  streaming_url,
249  auth=self._token_token,
250  timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT),
251  )
252 
253  return await async_aiohttp_proxy_web(self.hasshass, request, stream_coro)
254 
255  # streaming via ffmpeg
256  assert self._rtsp_url_rtsp_url is not None
257  streaming_url = self._rtsp_url_rtsp_url
258  stream = CameraMjpeg(self._ffmpeg_ffmpeg.binary)
259  await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments_ffmpeg_arguments)
260 
261  try:
262  stream_reader = await stream.get_reader()
263  return await async_aiohttp_proxy_stream(
264  self.hasshass,
265  request,
266  stream_reader,
267  self._ffmpeg_ffmpeg.ffmpeg_stream_content_type,
268  )
269  finally:
270  await stream.close()
271 
272  # Entity property overrides
273 
274  @property
275  def name(self) -> str:
276  """Return the name of this camera."""
277  return self._name_name
278 
279  @property
280  def extra_state_attributes(self) -> dict[str, Any]:
281  """Return the Amcrest-specific camera state attributes."""
282  attr = {}
283  if self._audio_enabled is not None:
284  attr["audio"] = _BOOL_TO_STATE.get(self._audio_enabled)
285  if self._motion_recording_enabled is not None:
286  attr["motion_recording"] = _BOOL_TO_STATE.get(
287  self._motion_recording_enabled
288  )
289  if self._color_bw is not None:
290  attr[_ATTR_COLOR_BW] = self._color_bw
291  return attr
292 
293  @property
294  def available(self) -> bool:
295  """Return True if entity is available."""
296  return self._api_api.available
297 
298  # Camera property overrides
299 
300  @property
301  def is_recording(self) -> bool:
302  """Return true if the device is recording."""
303  return self._is_recording
304 
305  @property
306  def brand(self) -> str | None:
307  """Return the camera brand."""
308  return self._brand_brand
309 
310  @property
311  def motion_detection_enabled(self) -> bool:
312  """Return the camera motion detection status."""
313  return self._motion_detection_enabled
314 
315  @property
316  def model(self) -> str | None:
317  """Return the camera model."""
318  return self._model_model
319 
320  async def stream_source(self) -> str | None:
321  """Return the source of the stream."""
322  return self._rtsp_url_rtsp_url
323 
324  @property
325  def is_on(self) -> bool:
326  """Return true if on."""
327  return self.is_streamingis_streaming
328 
329  # Other Entity method overrides
330 
331  @callback
332  def async_on_demand_update(self) -> None:
333  """Update state."""
334  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
335 
336  async def async_added_to_hass(self) -> None:
337  """Subscribe to signals and add camera to list."""
338  self._unsub_dispatcher.extend(
340  self.hasshass,
341  service_signal(service, self.entity_identity_id),
342  getattr(self, callback_name),
343  )
344  for service, (_, callback_name, _) in CAMERA_SERVICES.items()
345  )
346  self._unsub_dispatcher.append(
348  self.hasshass,
349  service_signal(SERVICE_UPDATE, self.namenamename),
350  self.async_on_demand_updateasync_on_demand_update,
351  )
352  )
353  self.hasshass.data[DATA_AMCREST][CAMERAS].append(self.entity_identity_id)
354 
355  async def async_will_remove_from_hass(self) -> None:
356  """Remove camera from list and disconnect from signals."""
357  self.hasshass.data[DATA_AMCREST][CAMERAS].remove(self.entity_identity_id)
358  for unsub_dispatcher in self._unsub_dispatcher:
359  unsub_dispatcher()
360 
361  async def async_update(self) -> None:
362  """Update entity status."""
363  if not self.availableavailableavailableavailable:
364  return
365  _LOGGER.debug("Updating %s camera", self.namenamename)
366  try:
367  if self._brand_brand is None:
368  resp = await self._api_api.async_vendor_information
369  _LOGGER.debug("Assigned brand=%s", resp)
370  if resp:
371  self._brand_brand = resp
372  else:
373  self._brand_brand = "unknown"
374  if self._model_model is None:
375  resp = await self._api_api.async_device_type
376  _LOGGER.debug("Assigned model=%s", resp)
377  if resp:
378  self._model_model = resp
379  else:
380  self._model_model = "unknown"
381  if self._attr_unique_id_attr_unique_id is None:
382  serial_number = (await self._api_api.async_serial_number).strip()
383  if serial_number:
384  self._attr_unique_id_attr_unique_id = (
385  f"{serial_number}-{self._resolution}-{self._channel}"
386  )
387  _LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id_attr_unique_id)
388  if self._rtsp_url_rtsp_url is None:
389  self._rtsp_url_rtsp_url = await self._api_api.async_rtsp_url(typeno=self._resolution_resolution)
390 
391  (
392  self._attr_is_streaming,
393  self._is_recording,
394  self._motion_detection_enabled,
395  self._audio_enabled,
396  self._motion_recording_enabled,
397  self._color_bw,
398  ) = await asyncio.gather(
399  self._async_get_video_async_get_video(),
400  self._async_get_recording_async_get_recording(),
401  self._async_get_motion_detection_async_get_motion_detection(),
402  self._async_get_audio_async_get_audio(),
403  self._async_get_motion_recording_async_get_motion_recording(),
404  self._async_get_color_mode_async_get_color_mode(),
405  )
406  except AmcrestError as error:
407  log_update_error(_LOGGER, "get", self.namenamename, "camera attributes", error)
408 
409  # Other Camera method overrides
410 
411  async def async_turn_off(self) -> None:
412  """Turn off camera."""
413  await self._async_enable_video_async_enable_video(False)
414 
415  async def async_turn_on(self) -> None:
416  """Turn on camera."""
417  await self._async_enable_video_async_enable_video(True)
418 
419  async def async_enable_motion_detection(self) -> None:
420  """Enable motion detection in the camera."""
421  await self._async_enable_motion_detection_async_enable_motion_detection(True)
422 
423  async def async_disable_motion_detection(self) -> None:
424  """Disable motion detection in camera."""
425  await self._async_enable_motion_detection_async_enable_motion_detection(False)
426 
427  # Additional Amcrest Camera service methods
428 
429  async def async_enable_recording(self) -> None:
430  """Call the job and enable recording."""
431  await self._async_enable_recording_async_enable_recording(True)
432 
433  async def async_disable_recording(self) -> None:
434  """Call the job and disable recording."""
435  await self._async_enable_recording_async_enable_recording(False)
436 
437  async def async_enable_audio(self) -> None:
438  """Call the job and enable audio."""
439  await self._async_enable_audio_async_enable_audio(True)
440 
441  async def async_disable_audio(self) -> None:
442  """Call the job and disable audio."""
443  await self._async_enable_audio_async_enable_audio(False)
444 
445  async def async_enable_motion_recording(self) -> None:
446  """Call the job and enable motion recording."""
447  await self._async_enable_motion_recording_async_enable_motion_recording(True)
448 
449  async def async_disable_motion_recording(self) -> None:
450  """Call the job and disable motion recording."""
451  await self._async_enable_motion_recording_async_enable_motion_recording(False)
452 
453  async def async_goto_preset(self, preset: int) -> None:
454  """Call the job and move camera to preset position."""
455  await self._async_goto_preset_async_goto_preset(preset)
456 
457  async def async_set_color_bw(self, color_bw: str) -> None:
458  """Call the job and set camera color mode."""
459  await self._async_set_color_bw_async_set_color_bw(color_bw)
460 
461  async def async_start_tour(self) -> None:
462  """Call the job and start camera tour."""
463  await self._async_start_tour_async_start_tour(True)
464 
465  async def async_stop_tour(self) -> None:
466  """Call the job and stop camera tour."""
467  await self._async_start_tour_async_start_tour(False)
468 
469  async def async_ptz_control(self, movement: str, travel_time: float) -> None:
470  """Move or zoom camera in specified direction."""
471  code = _ACTION[_MOV.index(movement)]
472 
473  kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0}
474  if code in _MOVE_1_ACTIONS:
475  kwargs["arg2"] = 1
476  elif code in _MOVE_2_ACTIONS:
477  kwargs["arg1"] = kwargs["arg2"] = 1
478 
479  try:
480  await self._api_api.async_ptz_control_command(action="start", **kwargs) # type: ignore[arg-type]
481  await asyncio.sleep(travel_time)
482  await self._api_api.async_ptz_control_command(action="stop", **kwargs) # type: ignore[arg-type]
483  except AmcrestError as error:
485  _LOGGER, "move", self.namenamename, f"camera PTZ {movement}", error
486  )
487 
488  # Methods to send commands to Amcrest camera and handle errors
489 
491  self, value: str | bool, description: str, attr: str | None = None
492  ) -> None:
493  func = description.replace(" ", "_")
494  description = f"camera {description} to {value}"
495  action = "set"
496  max_tries = 3
497  for tries in range(max_tries, 0, -1):
498  try:
499  await getattr(self, f"_async_set_{func}")(value)
500  new_value = await getattr(self, f"_async_get_{func}")()
501  if new_value != value:
502  raise AmcrestCommandFailed # noqa: TRY301
503  except (AmcrestError, AmcrestCommandFailed) as error:
504  if tries == 1:
505  log_update_error(_LOGGER, action, self.namenamename, description, error)
506  return
508  _LOGGER, action, self.namenamename, description, error, logging.DEBUG
509  )
510  else:
511  if attr:
512  setattr(self, attr, new_value)
513  self.schedule_update_ha_stateschedule_update_ha_state()
514  return
515 
516  async def _async_get_video(self) -> bool:
517  return await self._api_api.async_is_video_enabled(
518  channel=0, stream=RESOLUTION_TO_STREAM[self._resolution_resolution]
519  )
520 
521  async def _async_set_video(self, enable: bool) -> None:
522  await self._api_api.async_set_video_enabled(
523  enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution_resolution]
524  )
525 
526  async def _async_enable_video(self, enable: bool) -> None:
527  """Enable or disable camera video stream."""
528  # Given the way the camera's state is determined by
529  # is_streaming and is_recording, we can't leave
530  # recording on if video stream is being turned off.
531  if self.is_recordingis_recordingis_recording and not enable:
532  await self._async_enable_recording_async_enable_recording(False)
533  await self._async_change_setting_async_change_setting(enable, "video", "_attr_is_streaming")
534  if self._control_light_control_light:
535  await self._async_change_light_async_change_light()
536 
537  async def _async_get_recording(self) -> bool:
538  return (await self._api_api.async_record_mode) == "Manual"
539 
540  async def _async_set_recording(self, enable: bool) -> None:
541  rec_mode = {"Automatic": 0, "Manual": 1}
542  # The property has a str type, but setter has int type, which causes mypy confusion
543  await self._api_api.async_set_record_mode(
544  rec_mode["Manual" if enable else "Automatic"]
545  )
546 
547  async def _async_enable_recording(self, enable: bool) -> None:
548  """Turn recording on or off."""
549  # Given the way the camera's state is determined by
550  # is_streaming and is_recording, we can't leave
551  # video stream off if recording is being turned on.
552  if not self.is_streamingis_streaming and enable:
553  await self._async_enable_video_async_enable_video(True)
554  await self._async_change_setting_async_change_setting(enable, "recording", "_is_recording")
555 
556  async def _async_get_motion_detection(self) -> bool:
557  return await self._api_api.async_is_motion_detector_on()
558 
559  async def _async_set_motion_detection(self, enable: bool) -> None:
560  # The property has a str type, but setter has bool type, which causes mypy confusion
561  await self._api_api.async_set_motion_detection(enable)
562 
563  async def _async_enable_motion_detection(self, enable: bool) -> None:
564  """Enable or disable motion detection."""
565  await self._async_change_setting_async_change_setting(
566  enable, "motion detection", "_motion_detection_enabled"
567  )
568 
569  async def _async_get_audio(self) -> bool:
570  return await self._api_api.async_is_audio_enabled(
571  channel=0, stream=RESOLUTION_TO_STREAM[self._resolution_resolution]
572  )
573 
574  async def _async_set_audio(self, enable: bool) -> None:
575  await self._api_api.async_set_audio_enabled(
576  enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution_resolution]
577  )
578 
579  async def _async_enable_audio(self, enable: bool) -> None:
580  """Enable or disable audio stream."""
581  await self._async_change_setting_async_change_setting(enable, "audio", "_audio_enabled")
582  if self._control_light_control_light:
583  await self._async_change_light_async_change_light()
584 
585  async def _async_get_indicator_light(self) -> bool:
586  return (
587  "true"
588  in (
589  await self._api_api.async_command(
590  "configManager.cgi?action=getConfig&name=LightGlobal"
591  )
592  ).content.decode()
593  )
594 
595  async def _async_set_indicator_light(self, enable: bool) -> None:
596  await self._api_api.async_command(
597  f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
598  )
599 
600  async def _async_change_light(self) -> None:
601  """Enable or disable indicator light."""
602  await self._async_change_setting_async_change_setting(
603  self._audio_enabled or self.is_streamingis_streaming, "indicator light"
604  )
605 
606  async def _async_get_motion_recording(self) -> bool:
607  return await self._api_api.async_is_record_on_motion_detection()
608 
609  async def _async_set_motion_recording(self, enable: bool) -> None:
610  await self._api_api.async_set_motion_recording(enable)
611 
612  async def _async_enable_motion_recording(self, enable: bool) -> None:
613  """Enable or disable motion recording."""
614  await self._async_change_setting_async_change_setting(
615  enable, "motion recording", "_motion_recording_enabled"
616  )
617 
618  async def _async_goto_preset(self, preset: int) -> None:
619  """Move camera position and zoom to preset."""
620  try:
621  await self._api_api.async_go_to_preset(preset_point_number=preset)
622  except AmcrestError as error:
624  _LOGGER, "move", self.namenamename, f"camera to preset {preset}", error
625  )
626 
627  async def _async_get_color_mode(self) -> str:
628  return _CBW[await self._api_api.async_day_night_color]
629 
630  async def _async_set_color_mode(self, cbw: str) -> None:
631  await self._api_api.async_set_day_night_color(_CBW.index(cbw), channel=0)
632 
633  async def _async_set_color_bw(self, cbw: str) -> None:
634  """Set camera color mode."""
635  await self._async_change_setting_async_change_setting(cbw, "color mode", "_color_bw")
636 
637  async def _async_start_tour(self, start: bool) -> None:
638  """Start camera tour."""
639  try:
640  await self._api_api.async_tour(start=start)
641  except AmcrestError as error:
643  _LOGGER, "start" if start else "stop", self.namenamename, "camera tour", error
644  )
None _async_enable_motion_recording(self, bool enable)
Definition: camera.py:612
web.StreamResponse|None handle_async_mjpeg_stream(self, web.Request request)
Definition: camera.py:229
None _async_set_motion_recording(self, bool enable)
Definition: camera.py:609
None __init__(self, str name, AmcrestDevice device, FFmpegManager ffmpeg)
Definition: camera.py:157
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:205
None async_ptz_control(self, str movement, float travel_time)
Definition: camera.py:469
None _async_set_recording(self, bool enable)
Definition: camera.py:540
None _async_set_motion_detection(self, bool enable)
Definition: camera.py:559
None _async_enable_recording(self, bool enable)
Definition: camera.py:547
None _async_change_setting(self, str|bool value, str description, str|None attr=None)
Definition: camera.py:492
None _async_set_indicator_light(self, bool enable)
Definition: camera.py:595
None _async_enable_motion_detection(self, bool enable)
Definition: camera.py:563
None async_set_color_bw(self, str color_bw)
Definition: camera.py:457
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
None schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1244
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: camera.py:131
None log_update_error(logging.Logger logger, str action, str|UndefinedType|None name, str entity_type, Exception error, int level=logging.ERROR)
Definition: helpers.py:24
str service_signal(str service, *str args)
Definition: helpers.py:12
bool remove(self, _T matcher)
Definition: match.py:214
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
Definition: __init__.py:106
web.StreamResponse async_aiohttp_proxy_stream(HomeAssistant hass, web.BaseRequest request, aiohttp.StreamReader stream, str|None content_type, int buffer_size=102400, int timeout=10)
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)
web.StreamResponse|None async_aiohttp_proxy_web(HomeAssistant hass, web.BaseRequest request, Awaitable[aiohttp.ClientResponse] web_coro, int buffer_size=102400, int timeout=10)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103