1 """Support for Amcrest IP cameras."""
3 from __future__
import annotations
6 from collections.abc
import Callable
7 from datetime
import timedelta
9 from typing
import TYPE_CHECKING, Any
12 from aiohttp
import web
13 from amcrest
import AmcrestError
14 from haffmpeg.camera
import CameraMjpeg
15 import voluptuous
as vol
23 async_aiohttp_proxy_stream,
24 async_aiohttp_proxy_web,
25 async_get_clientsession,
32 CAMERA_WEB_SESSION_TIMEOUT,
41 from .helpers
import log_update_error, service_signal
44 from .
import AmcrestDevice
46 _LOGGER = logging.getLogger(__name__)
50 STREAM_SOURCE_LIST = [
"snapshot",
"mjpeg",
"rtsp"]
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"
63 _SRV_PTZ_CTRL =
"ptz_control"
64 _ATTR_PTZ_TT =
"travel_time"
65 _ATTR_PTZ_MOV =
"movement"
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
85 _ATTR_PRESET =
"preset"
86 _ATTR_COLOR_BW =
"color_bw"
91 _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
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))}
97 _SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)})
98 _SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend(
100 vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV),
101 vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float,
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", ()),
119 (_ATTR_PTZ_MOV, _ATTR_PTZ_TT),
123 _BOOL_TO_STATE = {
True: STATE_ON,
False: STATE_OFF}
129 async_add_entities: AddEntitiesCallback,
130 discovery_info: DiscoveryInfoType |
None =
None,
132 """Set up an Amcrest IP Camera."""
133 if discovery_info
is None:
136 name = discovery_info[CONF_NAME]
137 device = hass.data[DATA_AMCREST][DEVICES][name]
144 """Conditions are not valid for taking a snapshot."""
148 """Amcrest camera command did not work."""
152 """An implementation of an Amcrest IP camera."""
154 _attr_should_poll =
True
155 _attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM
157 def __init__(self, name: str, device: AmcrestDevice, ffmpeg: FFmpegManager) ->
None:
158 """Initialize an Amcrest camera."""
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]] = []
184 "Attempt to take snapshot when %s camera is %s",
186 "offline" if not available
else "off",
194 return await self.
_api_api.async_snapshot(
195 timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT)
197 except AmcrestError
as error:
204 self, width: int |
None =
None, height: int |
None =
None
206 """Return a still image response from the camera."""
207 _LOGGER.debug(
"Take snapshot from %s", self.
_name_name)
215 _LOGGER.debug(
"Waiting for previous snapshot from %s", self.
_name_name)
224 except CannotSnapshot:
228 self, request: web.Request
229 ) -> web.StreamResponse |
None:
230 """Return an MJPEG stream."""
237 "Attempt to stream %s when %s camera is offline",
246 streaming_url = self.
_api_api.mjpeg_url(typeno=self.
_resolution_resolution)
247 stream_coro = websession.get(
250 timeout=aiohttp.ClientTimeout(total=CAMERA_WEB_SESSION_TIMEOUT),
256 assert self.
_rtsp_url_rtsp_url
is not None
258 stream = CameraMjpeg(self.
_ffmpeg_ffmpeg.binary)
259 await stream.open_camera(streaming_url, extra_cmd=self.
_ffmpeg_arguments_ffmpeg_arguments)
262 stream_reader = await stream.get_reader()
267 self.
_ffmpeg_ffmpeg.ffmpeg_stream_content_type,
276 """Return the name of this camera."""
277 return self.
_name_name
281 """Return the Amcrest-specific camera state attributes."""
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
289 if self._color_bw
is not None:
290 attr[_ATTR_COLOR_BW] = self._color_bw
295 """Return True if entity is available."""
296 return self.
_api_api.available
302 """Return true if the device is recording."""
303 return self._is_recording
307 """Return the camera brand."""
312 """Return the camera motion detection status."""
313 return self._motion_detection_enabled
317 """Return the camera model."""
321 """Return the source of the stream."""
326 """Return true if on."""
337 """Subscribe to signals and add camera to list."""
338 self._unsub_dispatcher.extend(
342 getattr(self, callback_name),
344 for service, (_, callback_name, _)
in CAMERA_SERVICES.items()
346 self._unsub_dispatcher.append(
353 self.
hasshass.data[DATA_AMCREST][CAMERAS].append(self.
entity_identity_id)
356 """Remove camera from list and disconnect from signals."""
358 for unsub_dispatcher
in self._unsub_dispatcher:
362 """Update entity status."""
365 _LOGGER.debug(
"Updating %s camera", self.
namenamename)
367 if self.
_brand_brand
is None:
368 resp = await self.
_api_api.async_vendor_information
369 _LOGGER.debug(
"Assigned brand=%s", resp)
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)
380 self.
_model_model =
"unknown"
382 serial_number = (await self.
_api_api.async_serial_number).strip()
385 f
"{serial_number}-{self._resolution}-{self._channel}"
387 _LOGGER.debug(
"Assigned unique_id=%s", self.
_attr_unique_id_attr_unique_id)
392 self._attr_is_streaming,
394 self._motion_detection_enabled,
396 self._motion_recording_enabled,
398 ) = await asyncio.gather(
406 except AmcrestError
as error:
412 """Turn off camera."""
416 """Turn on camera."""
420 """Enable motion detection in the camera."""
424 """Disable motion detection in camera."""
430 """Call the job and enable recording."""
434 """Call the job and disable recording."""
438 """Call the job and enable audio."""
442 """Call the job and disable audio."""
446 """Call the job and enable motion recording."""
450 """Call the job and disable motion recording."""
454 """Call the job and move camera to preset position."""
458 """Call the job and set camera color mode."""
462 """Call the job and start camera tour."""
466 """Call the job and stop camera tour."""
470 """Move or zoom camera in specified direction."""
471 code = _ACTION[_MOV.index(movement)]
473 kwargs = {
"code": code,
"arg1": 0,
"arg2": 0,
"arg3": 0}
474 if code
in _MOVE_1_ACTIONS:
476 elif code
in _MOVE_2_ACTIONS:
477 kwargs[
"arg1"] = kwargs[
"arg2"] = 1
480 await self.
_api_api.async_ptz_control_command(action=
"start", **kwargs)
481 await asyncio.sleep(travel_time)
482 await self.
_api_api.async_ptz_control_command(action=
"stop", **kwargs)
483 except AmcrestError
as error:
485 _LOGGER,
"move", self.
namenamename, f
"camera PTZ {movement}", error
491 self, value: str | bool, description: str, attr: str |
None =
None
493 func = description.replace(
" ",
"_")
494 description = f
"camera {description} to {value}"
497 for tries
in range(max_tries, 0, -1):
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
503 except (AmcrestError, AmcrestCommandFailed)
as error:
508 _LOGGER, action, self.
namenamename, description, error, logging.DEBUG
512 setattr(self, attr, new_value)
517 return await self.
_api_api.async_is_video_enabled(
518 channel=0, stream=RESOLUTION_TO_STREAM[self.
_resolution_resolution]
522 await self.
_api_api.async_set_video_enabled(
523 enable, channel=0, stream=RESOLUTION_TO_STREAM[self.
_resolution_resolution]
527 """Enable or disable camera video stream."""
538 return (await self.
_api_api.async_record_mode) ==
"Manual"
541 rec_mode = {
"Automatic": 0,
"Manual": 1}
543 await self.
_api_api.async_set_record_mode(
544 rec_mode[
"Manual" if enable
else "Automatic"]
548 """Turn recording on or off."""
557 return await self.
_api_api.async_is_motion_detector_on()
561 await self.
_api_api.async_set_motion_detection(enable)
564 """Enable or disable motion detection."""
566 enable,
"motion detection",
"_motion_detection_enabled"
570 return await self.
_api_api.async_is_audio_enabled(
571 channel=0, stream=RESOLUTION_TO_STREAM[self.
_resolution_resolution]
575 await self.
_api_api.async_set_audio_enabled(
576 enable, channel=0, stream=RESOLUTION_TO_STREAM[self.
_resolution_resolution]
580 """Enable or disable audio stream."""
589 await self.
_api_api.async_command(
590 "configManager.cgi?action=getConfig&name=LightGlobal"
596 await self.
_api_api.async_command(
597 f
"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
601 """Enable or disable indicator light."""
603 self._audio_enabled
or self.
is_streamingis_streaming,
"indicator light"
607 return await self.
_api_api.async_is_record_on_motion_detection()
610 await self.
_api_api.async_set_motion_recording(enable)
613 """Enable or disable motion recording."""
615 enable,
"motion recording",
"_motion_recording_enabled"
619 """Move camera position and zoom to preset."""
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
628 return _CBW[await self.
_api_api.async_day_night_color]
631 await self.
_api_api.async_set_day_night_color(_CBW.index(cbw), channel=0)
634 """Set camera color mode."""
638 """Start camera tour."""
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
None async_turn_off(self)
None async_disable_recording(self)
None _async_set_color_mode(self, str cbw)
None async_disable_motion_recording(self)
bool _async_get_video(self)
None _async_change_light(self)
bool _async_get_indicator_light(self)
bool _async_get_motion_recording(self)
None _async_enable_motion_recording(self, bool enable)
None _async_set_audio(self, bool enable)
None _async_set_video(self, bool enable)
None _async_enable_audio(self, bool enable)
None async_stop_tour(self)
web.StreamResponse|None handle_async_mjpeg_stream(self, web.Request request)
None async_enable_motion_recording(self)
None async_enable_audio(self)
None _async_set_motion_recording(self, bool enable)
None __init__(self, str name, AmcrestDevice device, FFmpegManager ffmpeg)
None async_enable_recording(self)
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
None async_ptz_control(self, str movement, float travel_time)
bytes|None _async_get_image(self)
None _async_set_recording(self, bool enable)
bool _async_get_audio(self)
None _async_set_motion_detection(self, bool enable)
None _async_enable_video(self, bool enable)
bool motion_detection_enabled(self)
dict[str, Any] extra_state_attributes(self)
bool _async_get_motion_detection(self)
None _check_snapshot_ok(self)
None _async_enable_recording(self, bool enable)
None async_will_remove_from_hass(self)
None _async_change_setting(self, str|bool value, str description, str|None attr=None)
None async_start_tour(self)
None async_on_demand_update(self)
None _async_set_color_bw(self, str cbw)
None async_enable_motion_detection(self)
None async_added_to_hass(self)
str|None stream_source(self)
None _async_set_indicator_light(self, bool enable)
None async_disable_audio(self)
None async_disable_motion_detection(self)
None _async_goto_preset(self, int preset)
None _async_enable_motion_detection(self, bool enable)
None async_goto_preset(self, int preset)
bool _async_get_recording(self)
None async_set_color_bw(self, str color_bw)
str _async_get_color_mode(self)
None _async_start_tour(self, bool start)
None async_schedule_update_ha_state(self, bool force_refresh=False)
None schedule_update_ha_state(self, bool force_refresh=False)
str|UndefinedType|None name(self)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
None log_update_error(logging.Logger logger, str action, str|UndefinedType|None name, str entity_type, Exception error, int level=logging.ERROR)
str service_signal(str service, *str args)
bool remove(self, _T matcher)
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
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)