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:
parent
3c1cdecb88
commit
d966e0cfce
3 changed files with 45 additions and 30 deletions
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue