Add control of Amcrest indicator light (#23986)

Enable feature by default but allow it to be disabled by "control_light: false" in config.

Get brand from camera instead of assuming Amcrest (since this works with other cameras, too.)

Retrieve RTSP URL in update method instead of in stream_source property and in handle_async_mjpeg_stream method.

Move amcrest imports from methods to global.
This commit is contained in:
Phil Bruckner 2019-05-31 15:41:48 -05:00 committed by Paulus Schoutsen
parent 3c1cdecb88
commit d966e0cfce
3 changed files with 45 additions and 30 deletions

View file

@ -3,6 +3,7 @@ import logging
from datetime import timedelta from datetime import timedelta
import aiohttp import aiohttp
from amcrest import AmcrestCamera, AmcrestError
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.auth.permissions.const import POLICY_CONTROL
@ -32,6 +33,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution' CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source' CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
CONF_CONTROL_LIGHT = 'control_light'
DEFAULT_NAME = 'Amcrest Camera' DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80 DEFAULT_PORT = 80
@ -103,6 +105,7 @@ AMCREST_SCHEMA = vol.All(
_deprecated_sensor_values), _deprecated_sensor_values),
vol.Optional(CONF_SWITCHES): vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]), vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean,
}), }),
_deprecated_switches _deprecated_switches
) )
@ -114,8 +117,6 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Set up the Amcrest IP Camera component.""" """Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera, AmcrestError
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []}) hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
devices = config[DOMAIN] devices = config[DOMAIN]
@ -149,6 +150,7 @@ def setup(hass, config):
sensors = device.get(CONF_SENSORS) sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES) switches = device.get(CONF_SWITCHES)
stream_source = device[CONF_STREAM_SOURCE] stream_source = device[CONF_STREAM_SOURCE]
control_light = device.get(CONF_CONTROL_LIGHT)
# currently aiohttp only works with basic authentication # currently aiohttp only works with basic authentication
# only valid for mjpeg streaming # only valid for mjpeg streaming
@ -159,7 +161,7 @@ def setup(hass, config):
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice( hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
api, authentication, ffmpeg_arguments, stream_source, api, authentication, ffmpeg_arguments, stream_source,
resolution) resolution, control_light)
discovery.load_platform( discovery.load_platform(
hass, CAMERA, DOMAIN, { hass, CAMERA, DOMAIN, {
@ -245,10 +247,11 @@ class AmcrestDevice:
"""Representation of a base Amcrest discovery device.""" """Representation of a base Amcrest discovery device."""
def __init__(self, api, authentication, ffmpeg_arguments, def __init__(self, api, authentication, ffmpeg_arguments,
stream_source, resolution): stream_source, resolution, control_light):
"""Initialize the entity.""" """Initialize the entity."""
self.api = api self.api = api
self.authentication = authentication self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source self.stream_source = stream_source
self.resolution = resolution self.resolution = resolution
self.control_light = control_light

View file

@ -2,6 +2,8 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from amcrest import AmcrestError
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASS_MOTION) BinarySensorDevice, DEVICE_CLASS_MOTION)
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
@ -58,8 +60,6 @@ class AmcrestBinarySensor(BinarySensorDevice):
def update(self): def update(self):
"""Update entity.""" """Update entity."""
from amcrest import AmcrestError
_LOGGER.debug('Pulling data from %s binary sensor', self._name) _LOGGER.debug('Pulling data from %s binary sensor', self._name)
try: try:

View file

@ -2,6 +2,7 @@
import asyncio import asyncio
import logging import logging
from amcrest import AmcrestError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import ( from homeassistant.components.camera import (
@ -94,19 +95,20 @@ class AmcrestCam(Camera):
self._stream_source = device.stream_source self._stream_source = device.stream_source
self._resolution = device.resolution self._resolution = device.resolution
self._token = self._auth = device.authentication self._token = self._auth = device.authentication
self._control_light = device.control_light
self._is_recording = False self._is_recording = False
self._motion_detection_enabled = None self._motion_detection_enabled = None
self._brand = None
self._model = None self._model = None
self._audio_enabled = None self._audio_enabled = None
self._motion_recording_enabled = None self._motion_recording_enabled = None
self._color_bw = None self._color_bw = None
self._rtsp_url = None
self._snapshot_lock = asyncio.Lock() self._snapshot_lock = asyncio.Lock()
self._unsub_dispatcher = [] self._unsub_dispatcher = []
async def async_camera_image(self): async def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from amcrest import AmcrestError
if not self.is_on: if not self.is_on:
_LOGGER.error( _LOGGER.error(
'Attempt to take snaphot when %s camera is off', self.name) 'Attempt to take snaphot when %s camera is off', self.name)
@ -143,7 +145,7 @@ class AmcrestCam(Camera):
# streaming via ffmpeg # streaming via ffmpeg
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
streaming_url = self._api.rtsp_url(typeno=self._resolution) streaming_url = self._rtsp_url
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera( await stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments) streaming_url, extra_cmd=self._ffmpeg_arguments)
@ -191,7 +193,7 @@ class AmcrestCam(Camera):
@property @property
def brand(self): def brand(self):
"""Return the camera brand.""" """Return the camera brand."""
return 'Amcrest' return self._brand
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
@ -205,7 +207,7 @@ class AmcrestCam(Camera):
async def stream_source(self): async def stream_source(self):
"""Return the source of the stream.""" """Return the source of the stream."""
return self._api.rtsp_url(typeno=self._resolution) return self._rtsp_url
@property @property
def is_on(self): def is_on(self):
@ -231,9 +233,19 @@ class AmcrestCam(Camera):
def update(self): def update(self):
"""Update entity status.""" """Update entity status."""
from amcrest import AmcrestError
_LOGGER.debug('Pulling data from %s camera', self.name) _LOGGER.debug('Pulling data from %s camera', self.name)
if self._brand is None:
try:
resp = self._api.vendor_information.strip()
if resp.startswith('vendor='):
self._brand = resp.split('=')[-1]
else:
self._brand = 'unknown'
except AmcrestError as error:
_LOGGER.error(
'Could not get %s camera brand due to error: %s',
self.name, error)
self._brand = 'unknwown'
if self._model is None: if self._model is None:
try: try:
self._model = self._api.device_type.split('=')[-1].strip() self._model = self._api.device_type.split('=')[-1].strip()
@ -241,7 +253,7 @@ class AmcrestCam(Camera):
_LOGGER.error( _LOGGER.error(
'Could not get %s camera model due to error: %s', 'Could not get %s camera model due to error: %s',
self.name, error) self.name, error)
self._model = '' self._model = 'unknown'
try: try:
self.is_streaming = self._api.video_enabled self.is_streaming = self._api.video_enabled
self._is_recording = self._api.record_mode == 'Manual' self._is_recording = self._api.record_mode == 'Manual'
@ -251,6 +263,7 @@ class AmcrestCam(Camera):
self._motion_recording_enabled = ( self._motion_recording_enabled = (
self._api.is_record_on_motion_detection()) self._api.is_record_on_motion_detection())
self._color_bw = _CBW[self._api.day_night_color] self._color_bw = _CBW[self._api.day_night_color]
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
'Could not get %s camera attributes due to error: %s', 'Could not get %s camera attributes due to error: %s',
@ -322,8 +335,6 @@ class AmcrestCam(Camera):
def _enable_video_stream(self, enable): def _enable_video_stream(self, enable):
"""Enable or disable camera video stream.""" """Enable or disable camera video stream."""
from amcrest import AmcrestError
# Given the way the camera's state is determined by # Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave # is_streaming and is_recording, we can't leave
# recording on if video stream is being turned off. # recording on if video stream is being turned off.
@ -338,11 +349,11 @@ class AmcrestCam(Camera):
else: else:
self.is_streaming = enable self.is_streaming = enable
self.schedule_update_ha_state() self.schedule_update_ha_state()
if self._control_light:
self._enable_light(self._audio_enabled or self.is_streaming)
def _enable_recording(self, enable): def _enable_recording(self, enable):
"""Turn recording on or off.""" """Turn recording on or off."""
from amcrest import AmcrestError
# Given the way the camera's state is determined by # Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave # is_streaming and is_recording, we can't leave
# video stream off if recording is being turned on. # video stream off if recording is being turned on.
@ -362,8 +373,6 @@ class AmcrestCam(Camera):
def _enable_motion_detection(self, enable): def _enable_motion_detection(self, enable):
"""Enable or disable motion detection.""" """Enable or disable motion detection."""
from amcrest import AmcrestError
try: try:
self._api.motion_detection = str(enable).lower() self._api.motion_detection = str(enable).lower()
except AmcrestError as error: except AmcrestError as error:
@ -376,8 +385,6 @@ class AmcrestCam(Camera):
def _enable_audio(self, enable): def _enable_audio(self, enable):
"""Enable or disable audio stream.""" """Enable or disable audio stream."""
from amcrest import AmcrestError
try: try:
self._api.audio_enabled = enable self._api.audio_enabled = enable
except AmcrestError as error: except AmcrestError as error:
@ -387,11 +394,22 @@ class AmcrestCam(Camera):
else: else:
self._audio_enabled = enable self._audio_enabled = enable
self.schedule_update_ha_state() self.schedule_update_ha_state()
if self._control_light:
self._enable_light(self._audio_enabled or self.is_streaming)
def _enable_light(self, enable):
"""Enable or disable indicator light."""
try:
self._api.command(
'configManager.cgi?action=setConfig&LightGlobal[0].Enable={}'
.format(str(enable).lower()))
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera indicator light due to error: %s',
'enable' if enable else 'disable', self.name, error)
def _enable_motion_recording(self, enable): def _enable_motion_recording(self, enable):
"""Enable or disable motion recording.""" """Enable or disable motion recording."""
from amcrest import AmcrestError
try: try:
self._api.motion_recording = str(enable).lower() self._api.motion_recording = str(enable).lower()
except AmcrestError as error: except AmcrestError as error:
@ -404,8 +422,6 @@ class AmcrestCam(Camera):
def _goto_preset(self, preset): def _goto_preset(self, preset):
"""Move camera position and zoom to preset.""" """Move camera position and zoom to preset."""
from amcrest import AmcrestError
try: try:
self._api.go_to_preset( self._api.go_to_preset(
action='start', preset_point_number=preset) action='start', preset_point_number=preset)
@ -416,8 +432,6 @@ class AmcrestCam(Camera):
def _set_color_bw(self, cbw): def _set_color_bw(self, cbw):
"""Set camera color mode.""" """Set camera color mode."""
from amcrest import AmcrestError
try: try:
self._api.day_night_color = _CBW.index(cbw) self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error: except AmcrestError as error:
@ -430,8 +444,6 @@ class AmcrestCam(Camera):
def _start_tour(self, start): def _start_tour(self, start):
"""Start camera tour.""" """Start camera tour."""
from amcrest import AmcrestError
try: try:
self._api.tour(start=start) self._api.tour(start=start)
except AmcrestError as error: except AmcrestError as error: