Add amcrest camera services and deprecate switches (#22949)
* Add amcrest camera services and deprecate switches - Implement enabling and disabling motion detection from camera platform. - Add amcrest specific camera services for controlling audio stream, motion recording, continuous recording and camera color mode, as well as moving camera to PTZ preset and starting and stopping PTZ tour function. - Add camera attributes to indicate the state of the various camera settings controlled by the new services. - Deprecate switches in favor of camera services and attributes. * Rename services and move service handling to __init__.py Rename services from 'camera.amcrest_xxx' to 'amcrest.xxx'. This allows services to be documented in services.yaml. Add services.yaml. Reorganize hass.data[DATA_AMCREST] and do some general cleanup to make various platform modules more consistent. Move service handling code to __init__.py from camera.py. * Update per review comments, part 1 - Rebase - Add permission checking to services - Change cv.ensure_list_csv to cv.ensure_list - Add comment for "pointless-statement" in setup - Change handler_services to handled_services - Remove check if services have alreaday been registered - Pass ffmpeg instead of hass to AmcrestCam __init__ - Remove writing motion_detection attr from device_state_attributes - Change service methods from callbacks to coroutines * Update per review comments, part 2 - Use dispatcher to signal camera entities to run services. - Reorganize a bit, including moving a few things to new modules const.py & helpers.py. * Update per review comments, part 3 Move call data extraction from camera.py to __init__.py.
This commit is contained in:
parent
c216ac7260
commit
86b017e2f0
8 changed files with 582 additions and 182 deletions
|
@ -5,16 +5,30 @@ from datetime import timedelta
|
|||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION)
|
||||
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
|
||||
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .binary_sensor import BINARY_SENSORS
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .const import DOMAIN, DATA_AMCREST
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_MOTION_DETECTOR, SENSORS
|
||||
from .switch import SWITCHES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
|||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
@ -43,70 +52,60 @@ AUTHENTICATION_LIST = {
|
|||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_sensors(value):
|
||||
if SENSOR_MOTION_DETECTOR in value:
|
||||
def _deprecated_sensor_values(sensors):
|
||||
if SENSOR_MOTION_DETECTOR in sensors:
|
||||
_LOGGER.warning(
|
||||
'sensors option %s is deprecated. '
|
||||
'Please remove from your configuration and '
|
||||
'use binary_sensors option motion_detected instead.',
|
||||
SENSOR_MOTION_DETECTOR)
|
||||
return value
|
||||
"The 'sensors' option value '%s' is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"the 'binary_sensors' option with value 'motion_detected' "
|
||||
"instead.", SENSOR_MOTION_DETECTOR)
|
||||
return sensors
|
||||
|
||||
|
||||
def _has_unique_names(value):
|
||||
names = [camera[CONF_NAME] for camera in value]
|
||||
def _deprecated_switches(config):
|
||||
if CONF_SWITCHES in config:
|
||||
_LOGGER.warning(
|
||||
"The 'switches' option (with value %s) is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"camera services and attributes instead.",
|
||||
config[CONF_SWITCHES])
|
||||
return config
|
||||
|
||||
|
||||
def _has_unique_names(devices):
|
||||
names = [device[CONF_NAME] for device in devices]
|
||||
vol.Schema(vol.Unique())(names)
|
||||
return value
|
||||
return devices
|
||||
|
||||
|
||||
AMCREST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})
|
||||
AMCREST_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)],
|
||||
_deprecated_sensor_values),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
}),
|
||||
_deprecated_switches
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
|
||||
|
@ -117,21 +116,22 @@ def setup(hass, config):
|
|||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera, AmcrestError
|
||||
|
||||
hass.data.setdefault(DATA_AMCREST, {})
|
||||
amcrest_cams = config[DOMAIN]
|
||||
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
|
||||
devices = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
for device in devices:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
camera = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
api = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
# Test camera communications.
|
||||
api.current_time
|
||||
|
||||
except AmcrestError as ex:
|
||||
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
|
||||
|
@ -148,7 +148,7 @@ def setup(hass, config):
|
|||
binary_sensors = device.get(CONF_BINARY_SENSORS)
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
|
||||
stream_source = device[CONF_STREAM_SOURCE]
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
|
@ -157,47 +157,97 @@ def setup(hass, config):
|
|||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
|
||||
api, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
hass, CAMERA, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
|
||||
if binary_sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
hass, BINARY_SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_BINARY_SENSORS: binary_sensors
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
hass, SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
hass, SWITCH, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return len(hass.data[DATA_AMCREST]) >= 1
|
||||
if not hass.data[DATA_AMCREST]['devices']:
|
||||
return False
|
||||
|
||||
def have_permission(user, entity_id):
|
||||
return not user or user.permissions.check_entity(
|
||||
entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call):
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id for entity_id in hass.data[DATA_AMCREST]['cameras']
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST]['cameras']:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context,
|
||||
entity_id=entity_id,
|
||||
permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call):
|
||||
args = []
|
||||
for arg in CAMERA_SERVICES[call.service][2]:
|
||||
args.append(call.data[arg])
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
service_signal(call.service, entity_id),
|
||||
*args
|
||||
)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler, params[0])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
def __init__(self, api, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
self.api = api
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
|
|
|
@ -5,38 +5,39 @@ import logging
|
|||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_MOTION)
|
||||
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
|
||||
from . import DATA_AMCREST, BINARY_SENSORS
|
||||
|
||||
from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a binary sensor for an Amcrest IP Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
binary_sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_binary_sensors = []
|
||||
for sensor_type in binary_sensors:
|
||||
amcrest_binary_sensors.append(
|
||||
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_devices(amcrest_binary_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestBinarySensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestBinarySensor(BinarySensorDevice):
|
||||
"""Binary sensor for Amcrest camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._camera = camera
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
|
@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
|||
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
|
||||
|
||||
try:
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._state = self._api.is_motion_detected
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not update %s binary sensor due to error: %s',
|
||||
|
|
|
@ -2,18 +2,72 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT
|
||||
from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST
|
||||
from .helpers import service_signal
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STREAM_SOURCE_LIST = [
|
||||
'snapshot',
|
||||
'mjpeg',
|
||||
'rtsp',
|
||||
]
|
||||
|
||||
_SRV_EN_REC = 'enable_recording'
|
||||
_SRV_DS_REC = 'disable_recording'
|
||||
_SRV_EN_AUD = 'enable_audio'
|
||||
_SRV_DS_AUD = 'disable_audio'
|
||||
_SRV_EN_MOT_REC = 'enable_motion_recording'
|
||||
_SRV_DS_MOT_REC = 'disable_motion_recording'
|
||||
_SRV_GOTO = 'goto_preset'
|
||||
_SRV_CBW = 'set_color_bw'
|
||||
_SRV_TOUR_ON = 'start_tour'
|
||||
_SRV_TOUR_OFF = 'stop_tour'
|
||||
|
||||
_ATTR_PRESET = 'preset'
|
||||
_ATTR_COLOR_BW = 'color_bw'
|
||||
|
||||
_CBW_COLOR = 'color'
|
||||
_CBW_AUTO = 'auto'
|
||||
_CBW_BW = 'bw'
|
||||
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
|
||||
|
||||
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
|
||||
})
|
||||
|
||||
CAMERA_SERVICES = {
|
||||
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
|
||||
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
|
||||
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
|
||||
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
|
||||
_SRV_EN_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
|
||||
_SRV_DS_MOT_REC: (
|
||||
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
|
||||
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
|
||||
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
|
||||
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
|
||||
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
|
||||
}
|
||||
|
||||
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
|
@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities([
|
||||
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, amcrest):
|
||||
def __init__(self, name, device, ffmpeg):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._api = device.api
|
||||
self._ffmpeg = ffmpeg
|
||||
self._ffmpeg_arguments = device.ffmpeg_arguments
|
||||
self._stream_source = device.stream_source
|
||||
self._resolution = device.resolution
|
||||
self._token = self._auth = device.authentication
|
||||
self._is_recording = False
|
||||
self._motion_detection_enabled = None
|
||||
self._model = None
|
||||
self._audio_enabled = None
|
||||
self._motion_recording_enabled = None
|
||||
self._color_bw = None
|
||||
self._snapshot_lock = asyncio.Lock()
|
||||
self._unsub_dispatcher = []
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
@ -56,7 +115,7 @@ class AmcrestCam(Camera):
|
|||
try:
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = await self.hass.async_add_executor_job(
|
||||
self._camera.snapshot, self._resolution)
|
||||
self._api.snapshot, self._resolution)
|
||||
return response.data
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
|
@ -67,15 +126,16 @@ class AmcrestCam(Camera):
|
|||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Return an MJPEG stream."""
|
||||
# The snapshot implementation is handled by the parent class
|
||||
if self._stream_source == STREAM_SOURCE_LIST['snapshot']:
|
||||
if self._stream_source == 'snapshot':
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
if self._stream_source == 'mjpeg':
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
||||
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
streaming_url, auth=self._token,
|
||||
timeout=CAMERA_WEB_SESSION_TIMEOUT)
|
||||
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
|
@ -83,7 +143,7 @@ class AmcrestCam(Camera):
|
|||
# streaming via ffmpeg
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
|
||||
streaming_url = self._api.rtsp_url(typeno=self._resolution)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
await stream.open_camera(
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
@ -103,6 +163,19 @@ class AmcrestCam(Camera):
|
|||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the Amcrest-specific camera state attributes."""
|
||||
attr = {}
|
||||
if self._audio_enabled is not None:
|
||||
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
|
||||
if self._motion_recording_enabled is not None:
|
||||
attr['motion_recording'] = _BOOL_TO_STATE.get(
|
||||
self._motion_recording_enabled)
|
||||
if self._color_bw is not None:
|
||||
attr[_ATTR_COLOR_BW] = self._color_bw
|
||||
return attr
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
|
@ -120,6 +193,11 @@ class AmcrestCam(Camera):
|
|||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_detection_enabled
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
|
@ -128,7 +206,7 @@ class AmcrestCam(Camera):
|
|||
@property
|
||||
def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._camera.rtsp_url(typeno=self._resolution)
|
||||
return self._api.rtsp_url(typeno=self._resolution)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -137,6 +215,21 @@ class AmcrestCam(Camera):
|
|||
|
||||
# Other Entity method overrides
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to signals and add camera to list."""
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
self._unsub_dispatcher.append(async_dispatcher_connect(
|
||||
self.hass,
|
||||
service_signal(service, self.entity_id),
|
||||
getattr(self, params[1])))
|
||||
self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove camera from list and disconnect from signals."""
|
||||
self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id)
|
||||
for unsub_dispatcher in self._unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
|
||||
def update(self):
|
||||
"""Update entity status."""
|
||||
from amcrest import AmcrestError
|
||||
|
@ -144,15 +237,21 @@ class AmcrestCam(Camera):
|
|||
_LOGGER.debug('Pulling data from %s camera', self.name)
|
||||
if self._model is None:
|
||||
try:
|
||||
self._model = self._camera.device_type.split('=')[-1].strip()
|
||||
self._model = self._api.device_type.split('=')[-1].strip()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera model due to error: %s',
|
||||
self.name, error)
|
||||
self._model = ''
|
||||
try:
|
||||
self.is_streaming = self._camera.video_enabled
|
||||
self._is_recording = self._camera.record_mode == 'Manual'
|
||||
self.is_streaming = self._api.video_enabled
|
||||
self._is_recording = self._api.record_mode == 'Manual'
|
||||
self._motion_detection_enabled = (
|
||||
self._api.is_motion_detector_on())
|
||||
self._audio_enabled = self._api.audio_enabled
|
||||
self._motion_recording_enabled = (
|
||||
self._api.is_record_on_motion_detection())
|
||||
self._color_bw = _CBW[self._api.day_night_color]
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera attributes due to error: %s',
|
||||
|
@ -168,14 +267,71 @@ class AmcrestCam(Camera):
|
|||
"""Turn on camera."""
|
||||
self._enable_video_stream(True)
|
||||
|
||||
# Utility methods
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
self._enable_motion_detection(True)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
self._enable_motion_detection(False)
|
||||
|
||||
# Additional Amcrest Camera service methods
|
||||
|
||||
async def async_enable_recording(self):
|
||||
"""Call the job and enable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, True)
|
||||
|
||||
async def async_disable_recording(self):
|
||||
"""Call the job and disable recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_recording, False)
|
||||
|
||||
async def async_enable_audio(self):
|
||||
"""Call the job and enable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, True)
|
||||
|
||||
async def async_disable_audio(self):
|
||||
"""Call the job and disable audio."""
|
||||
await self.hass.async_add_executor_job(self._enable_audio, False)
|
||||
|
||||
async def async_enable_motion_recording(self):
|
||||
"""Call the job and enable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
True)
|
||||
|
||||
async def async_disable_motion_recording(self):
|
||||
"""Call the job and disable motion recording."""
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording,
|
||||
False)
|
||||
|
||||
async def async_goto_preset(self, preset):
|
||||
"""Call the job and move camera to preset position."""
|
||||
await self.hass.async_add_executor_job(self._goto_preset, preset)
|
||||
|
||||
async def async_set_color_bw(self, color_bw):
|
||||
"""Call the job and set camera color mode."""
|
||||
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
|
||||
|
||||
async def async_start_tour(self):
|
||||
"""Call the job and start camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, True)
|
||||
|
||||
async def async_stop_tour(self):
|
||||
"""Call the job and stop camera tour."""
|
||||
await self.hass.async_add_executor_job(self._start_tour, False)
|
||||
|
||||
# Methods to send commands to Amcrest camera and handle errors
|
||||
|
||||
def _enable_video_stream(self, enable):
|
||||
"""Enable or disable camera video stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# recording on if video stream is being turned off.
|
||||
if self.is_recording and not enable:
|
||||
self._enable_recording(False)
|
||||
try:
|
||||
self._camera.video_enabled = enable
|
||||
self._api.video_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera video stream due to error: %s',
|
||||
|
@ -183,3 +339,103 @@ class AmcrestCam(Camera):
|
|||
else:
|
||||
self.is_streaming = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_recording(self, enable):
|
||||
"""Turn recording on or off."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
# Given the way the camera's state is determined by
|
||||
# is_streaming and is_recording, we can't leave
|
||||
# video stream off if recording is being turned on.
|
||||
if not self.is_streaming and enable:
|
||||
self._enable_video_stream(True)
|
||||
rec_mode = {'Automatic': 0, 'Manual': 1}
|
||||
try:
|
||||
self._api.record_mode = rec_mode[
|
||||
'Manual' if enable else 'Automatic']
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._is_recording = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_motion_detection(self, enable):
|
||||
"""Enable or disable motion detection."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_detection = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion detection due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._motion_detection_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_audio(self, enable):
|
||||
"""Enable or disable audio stream."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.audio_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera audio stream due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._audio_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _enable_motion_recording(self, enable):
|
||||
"""Enable or disable motion recording."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.motion_recording = str(enable).lower()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera motion recording due to error: %s',
|
||||
'enable' if enable else 'disable', self.name, error)
|
||||
else:
|
||||
self._motion_recording_enabled = enable
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _goto_preset(self, preset):
|
||||
"""Move camera position and zoom to preset."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.go_to_preset(
|
||||
action='start', preset_point_number=preset)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not move %s camera to preset %i due to error: %s',
|
||||
self.name, preset, error)
|
||||
|
||||
def _set_color_bw(self, cbw):
|
||||
"""Set camera color mode."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.day_night_color = _CBW.index(cbw)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not set %s camera color mode to %s due to error: %s',
|
||||
self.name, cbw, error)
|
||||
else:
|
||||
self._color_bw = cbw
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_tour(self, start):
|
||||
"""Start camera tour."""
|
||||
from amcrest import AmcrestError
|
||||
|
||||
try:
|
||||
self._api.tour(start=start)
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera tour due to error: %s',
|
||||
'start' if start else 'stop', self.name, error)
|
||||
|
|
7
homeassistant/components/amcrest/const.py
Normal file
7
homeassistant/components/amcrest/const.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""Constants for amcrest component."""
|
||||
DOMAIN = 'amcrest'
|
||||
DATA_AMCREST = DOMAIN
|
||||
|
||||
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
|
||||
CAMERA_WEB_SESSION_TIMEOUT = 10
|
||||
SENSOR_SCAN_INTERVAL_SECS = 10
|
10
homeassistant/components/amcrest/helpers.py
Normal file
10
homeassistant/components/amcrest/helpers.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""Helpers for amcrest component."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def service_signal(service, entity_id=None):
|
||||
"""Encode service and entity_id into signal."""
|
||||
signal = '{}_{}'.format(DOMAIN, service)
|
||||
if entity_id:
|
||||
signal += '_{}'.format(entity_id.replace('.', '_'))
|
||||
return signal
|
|
@ -5,11 +5,19 @@ import logging
|
|||
from homeassistant.const import CONF_NAME, CONF_SENSORS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DATA_AMCREST, SENSORS
|
||||
from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSOR_MOTION_DETECTOR = 'motion_detector'
|
||||
SENSORS = {
|
||||
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
|
@ -18,30 +26,26 @@ async def async_setup_platform(
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
sensors = discovery_info[CONF_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_sensors = []
|
||||
for sensor_type in sensors:
|
||||
amcrest_sensors.append(
|
||||
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_entities(amcrest_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSensor(Entity):
|
||||
"""A sensor implementation for Amcrest IP camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize a sensor for Amcrest camera."""
|
||||
self._attrs = {}
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._name = '{0}_{1}'.format(
|
||||
name, SENSORS.get(self._sensor_type)[0])
|
||||
self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
|
||||
self._state = None
|
||||
self._attrs = {}
|
||||
self._unit_of_measurement = SENSORS[sensor_type][1]
|
||||
self._icon = SENSORS[sensor_type][2]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -66,22 +70,22 @@ class AmcrestSensor(Entity):
|
|||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return SENSORS.get(self._sensor_type)[1]
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Pulling data from %s sensor.", self._name)
|
||||
|
||||
if self._sensor_type == 'motion_detector':
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._camera.record_mode
|
||||
self._state = self._api.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._api.record_mode
|
||||
|
||||
elif self._sensor_type == 'ptz_preset':
|
||||
self._state = self._camera.ptz_presets_count
|
||||
self._state = self._api.ptz_presets_count
|
||||
|
||||
elif self._sensor_type == 'sdcard':
|
||||
sd_used = self._camera.storage_used
|
||||
sd_total = self._camera.storage_total
|
||||
sd_used = self._api.storage_used
|
||||
sd_total = self._api.storage_total
|
||||
self._attrs['Total'] = '{0} {1}'.format(*sd_total)
|
||||
self._attrs['Used'] = '{0} {1}'.format(*sd_used)
|
||||
self._state = self._camera.storage_used_percent
|
||||
self._state = self._api.storage_used_percent
|
||||
|
|
75
homeassistant/components/amcrest/services.yaml
Normal file
75
homeassistant/components/amcrest/services.yaml
Normal file
|
@ -0,0 +1,75 @@
|
|||
enable_recording:
|
||||
description: Enable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_recording:
|
||||
description: Disable continuous recording to camera storage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_audio:
|
||||
description: Enable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_audio:
|
||||
description: Disable audio stream.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
enable_motion_recording:
|
||||
description: Enable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
disable_motion_recording:
|
||||
description: Disable recording a clip to camera storage when motion is detected.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
goto_preset:
|
||||
description: Move camera to PTZ preset.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
preset:
|
||||
description: Preset number, starting from 1.
|
||||
example: 1
|
||||
|
||||
set_color_bw:
|
||||
description: Set camera color mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
color_bw:
|
||||
description: Color mode, one of 'auto', 'color' or 'bw'.
|
||||
example: auto
|
||||
|
||||
start_tour:
|
||||
description: Start camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
||||
|
||||
stop_tour:
|
||||
description: Stop camera's PTZ tour function.
|
||||
fields:
|
||||
entity_id:
|
||||
description: "Name(s) of the cameras, or 'all' for all cameras."
|
||||
example: 'camera.house_front'
|
|
@ -1,13 +1,19 @@
|
|||
"""Support for toggling Amcrest IP camera settings."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import CONF_NAME, CONF_SWITCHES
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from . import DATA_AMCREST, SWITCHES
|
||||
from .const import DATA_AMCREST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
|
@ -16,67 +22,58 @@ async def async_setup_platform(
|
|||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
switches = discovery_info[CONF_SWITCHES]
|
||||
camera = hass.data[DATA_AMCREST][name].device
|
||||
|
||||
all_switches = []
|
||||
|
||||
for setting in switches:
|
||||
all_switches.append(AmcrestSwitch(setting, camera, name))
|
||||
|
||||
async_add_entities(all_switches, True)
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSwitch(name, device, setting)
|
||||
for setting in discovery_info[CONF_SWITCHES]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSwitch(ToggleEntity):
|
||||
"""Representation of an Amcrest IP camera switch."""
|
||||
|
||||
def __init__(self, setting, camera, name):
|
||||
def __init__(self, name, device, setting):
|
||||
"""Initialize the Amcrest switch."""
|
||||
self._name = '{} {}'.format(name, SWITCHES[setting][0])
|
||||
self._api = device.api
|
||||
self._setting = setting
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(SWITCHES[setting][0], name)
|
||||
self._state = False
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the switch."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state == STATE_ON
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn setting on."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'true'
|
||||
self._api.motion_detection = 'true'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'true'
|
||||
self._api.motion_recording = 'true'
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn setting off."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'false'
|
||||
self._api.motion_detection = 'false'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'false'
|
||||
self._api.motion_recording = 'false'
|
||||
|
||||
def update(self):
|
||||
"""Update setting state."""
|
||||
_LOGGER.debug("Polling state for setting: %s ", self._name)
|
||||
|
||||
if self._setting == 'motion_detection':
|
||||
detection = self._camera.is_motion_detector_on()
|
||||
detection = self._api.is_motion_detector_on()
|
||||
elif self._setting == 'motion_recording':
|
||||
detection = self._camera.is_record_on_motion_detection()
|
||||
detection = self._api.is_record_on_motion_detection()
|
||||
|
||||
self._state = STATE_ON if detection else STATE_OFF
|
||||
self._state = detection
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
|
Loading…
Add table
Reference in a new issue