2019-02-13 20:21:14 +00:00
|
|
|
"""Support for Amcrest IP cameras."""
|
2017-07-11 08:10:10 +00:00
|
|
|
import logging
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
import aiohttp
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
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
|
2017-07-11 08:10:10 +00:00
|
|
|
from homeassistant.const import (
|
2019-04-25 05:39:49 +00:00
|
|
|
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
|
2017-07-11 08:10:10 +00:00
|
|
|
from homeassistant.helpers import discovery
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
2019-04-25 05:39:49 +00:00
|
|
|
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
|
2017-07-11 08:10:10 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
CONF_RESOLUTION = 'resolution'
|
|
|
|
CONF_STREAM_SOURCE = 'stream_source'
|
|
|
|
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
|
|
|
|
|
|
|
DEFAULT_NAME = 'Amcrest Camera'
|
|
|
|
DEFAULT_PORT = 80
|
|
|
|
DEFAULT_RESOLUTION = 'high'
|
2019-04-03 11:46:41 +00:00
|
|
|
DEFAULT_ARGUMENTS = '-pred 1'
|
2017-07-11 08:10:10 +00:00
|
|
|
|
|
|
|
NOTIFICATION_ID = 'amcrest_notification'
|
|
|
|
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
|
|
|
|
|
|
|
RESOLUTION_LIST = {
|
|
|
|
'high': 0,
|
|
|
|
'low': 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
SCAN_INTERVAL = timedelta(seconds=10)
|
|
|
|
|
|
|
|
AUTHENTICATION_LIST = {
|
|
|
|
'basic': 'basic'
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
def _deprecated_sensor_values(sensors):
|
|
|
|
if SENSOR_MOTION_DETECTOR in sensors:
|
|
|
|
_LOGGER.warning(
|
|
|
|
"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
|
2018-03-31 21:15:25 +00:00
|
|
|
|
2019-04-09 13:21:47 +00:00
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
def _deprecated_switches(config):
|
|
|
|
if CONF_SWITCHES in config:
|
2019-04-09 13:21:47 +00:00
|
|
|
_LOGGER.warning(
|
2019-04-25 05:39:49 +00:00
|
|
|
"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
|
2019-04-09 13:21:47 +00:00
|
|
|
|
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
def _has_unique_names(devices):
|
|
|
|
names = [device[CONF_NAME] for device in devices]
|
2019-04-09 13:21:47 +00:00
|
|
|
vol.Schema(vol.Unique())(names)
|
2019-04-25 05:39:49 +00:00
|
|
|
return devices
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
2019-04-09 13:21:47 +00:00
|
|
|
|
2017-07-11 08:10:10 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
2019-04-09 13:21:47 +00:00
|
|
|
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
|
2017-07-11 08:10:10 +00:00
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
|
|
|
def setup(hass, config):
|
|
|
|
"""Set up the Amcrest IP Camera component."""
|
2019-03-14 17:56:33 +00:00
|
|
|
from amcrest import AmcrestCamera, AmcrestError
|
2017-07-11 08:10:10 +00:00
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
|
|
|
|
devices = config[DOMAIN]
|
2017-07-11 08:10:10 +00:00
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
for device in devices:
|
2019-04-09 13:21:47 +00:00
|
|
|
name = device[CONF_NAME]
|
|
|
|
username = device[CONF_USERNAME]
|
|
|
|
password = device[CONF_PASSWORD]
|
|
|
|
|
2017-07-11 08:10:10 +00:00
|
|
|
try:
|
2019-04-25 05:39:49 +00:00
|
|
|
api = AmcrestCamera(device[CONF_HOST],
|
|
|
|
device[CONF_PORT],
|
|
|
|
username,
|
|
|
|
password).camera
|
2018-03-30 22:48:31 +00:00
|
|
|
# pylint: disable=pointless-statement
|
2019-04-25 05:39:49 +00:00
|
|
|
# Test camera communications.
|
|
|
|
api.current_time
|
2017-07-11 08:10:10 +00:00
|
|
|
|
2019-03-14 17:56:33 +00:00
|
|
|
except AmcrestError as ex:
|
2019-04-09 13:21:47 +00:00
|
|
|
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
|
2017-07-16 19:39:38 +00:00
|
|
|
hass.components.persistent_notification.create(
|
|
|
|
'Error: {}<br />'
|
2017-07-11 08:10:10 +00:00
|
|
|
'You will need to restart hass after fixing.'
|
|
|
|
''.format(ex),
|
|
|
|
title=NOTIFICATION_TITLE,
|
|
|
|
notification_id=NOTIFICATION_ID)
|
2018-03-30 22:48:31 +00:00
|
|
|
continue
|
2017-07-11 08:10:10 +00:00
|
|
|
|
2019-04-09 13:21:47 +00:00
|
|
|
ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
|
|
|
|
resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]]
|
|
|
|
binary_sensors = device.get(CONF_BINARY_SENSORS)
|
2017-07-11 08:10:10 +00:00
|
|
|
sensors = device.get(CONF_SENSORS)
|
2018-03-31 21:15:25 +00:00
|
|
|
switches = device.get(CONF_SWITCHES)
|
2019-04-25 05:39:49 +00:00
|
|
|
stream_source = device[CONF_STREAM_SOURCE]
|
2017-07-11 08:10:10 +00:00
|
|
|
|
|
|
|
# currently aiohttp only works with basic authentication
|
|
|
|
# only valid for mjpeg streaming
|
2019-04-09 13:21:47 +00:00
|
|
|
if device[CONF_AUTHENTICATION] == HTTP_BASIC_AUTHENTICATION:
|
|
|
|
authentication = aiohttp.BasicAuth(username, password)
|
|
|
|
else:
|
|
|
|
authentication = None
|
2017-07-11 08:10:10 +00:00
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
|
|
|
|
api, authentication, ffmpeg_arguments, stream_source,
|
2017-11-24 00:38:53 +00:00
|
|
|
resolution)
|
|
|
|
|
2017-07-11 08:10:10 +00:00
|
|
|
discovery.load_platform(
|
2019-04-25 05:39:49 +00:00
|
|
|
hass, CAMERA, DOMAIN, {
|
2017-07-11 08:10:10 +00:00
|
|
|
CONF_NAME: name,
|
|
|
|
}, config)
|
|
|
|
|
2019-04-09 13:21:47 +00:00
|
|
|
if binary_sensors:
|
|
|
|
discovery.load_platform(
|
2019-04-25 05:39:49 +00:00
|
|
|
hass, BINARY_SENSOR, DOMAIN, {
|
2019-04-09 13:21:47 +00:00
|
|
|
CONF_NAME: name,
|
|
|
|
CONF_BINARY_SENSORS: binary_sensors
|
|
|
|
}, config)
|
|
|
|
|
2017-07-11 08:10:10 +00:00
|
|
|
if sensors:
|
|
|
|
discovery.load_platform(
|
2019-04-25 05:39:49 +00:00
|
|
|
hass, SENSOR, DOMAIN, {
|
2017-07-11 08:10:10 +00:00
|
|
|
CONF_NAME: name,
|
|
|
|
CONF_SENSORS: sensors,
|
|
|
|
}, config)
|
|
|
|
|
2018-03-31 21:15:25 +00:00
|
|
|
if switches:
|
|
|
|
discovery.load_platform(
|
2019-04-25 05:39:49 +00:00
|
|
|
hass, SWITCH, DOMAIN, {
|
2018-03-31 21:15:25 +00:00
|
|
|
CONF_NAME: name,
|
|
|
|
CONF_SWITCHES: switches
|
|
|
|
}, config)
|
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
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
|
2017-11-24 00:38:53 +00:00
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class AmcrestDevice:
|
2017-11-24 00:38:53 +00:00
|
|
|
"""Representation of a base Amcrest discovery device."""
|
|
|
|
|
2019-04-25 05:39:49 +00:00
|
|
|
def __init__(self, api, authentication, ffmpeg_arguments,
|
2017-11-24 00:38:53 +00:00
|
|
|
stream_source, resolution):
|
|
|
|
"""Initialize the entity."""
|
2019-04-25 05:39:49 +00:00
|
|
|
self.api = api
|
2017-11-24 00:38:53 +00:00
|
|
|
self.authentication = authentication
|
|
|
|
self.ffmpeg_arguments = ffmpeg_arguments
|
|
|
|
self.stream_source = stream_source
|
|
|
|
self.resolution = resolution
|