diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index ff34ca6f5c7..a4c020efcdf 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -7,11 +7,11 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) + CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, + HTTP_BASIC_AUTHENTICATION) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv - REQUIREMENTS = ['amcrest==1.3.0'] DEPENDENCIES = ['ffmpeg'] @@ -52,9 +52,14 @@ STREAM_SOURCE_LIST = { 'rtsp': 2, } +BINARY_SENSORS = { + 'motion_detected': 'Motion Detected' +} + # Sensor types are defined like: Name, units, icon +SENSOR_MOTION_DETECTOR = 'motion_detector' SENSORS = { - 'motion_detector': ['Motion Detected', None, 'mdi:run'], + SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'], 'sdcard': ['SD Used', '%', 'mdi:sd'], 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], } @@ -65,28 +70,49 @@ SWITCHES = { 'motion_recording': ['Motion Recording', 'mdi:record-rec'] } + +def _deprecated_sensors(value): + if SENSOR_MOTION_DETECTOR in value: + _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 + + +def _has_unique_names(value): + names = [camera[CONF_NAME] for camera in value] + vol.Schema(vol.Unique())(names) + return value + + +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)]), +}) + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [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_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)]), - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - })]) + DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names) }, extra=vol.ALLOW_EXTRA) @@ -94,20 +120,24 @@ def setup(hass, config): """Set up the Amcrest IP Camera component.""" from amcrest import AmcrestCamera, AmcrestError - hass.data[DATA_AMCREST] = {} + hass.data.setdefault(DATA_AMCREST, {}) amcrest_cams = config[DOMAIN] for device in amcrest_cams: + name = device[CONF_NAME] + username = device[CONF_USERNAME] + password = device[CONF_PASSWORD] + try: - camera = AmcrestCamera(device.get(CONF_HOST), - device.get(CONF_PORT), - device.get(CONF_USERNAME), - device.get(CONF_PASSWORD)).camera + camera = AmcrestCamera(device[CONF_HOST], + device[CONF_PORT], + username, + password).camera # pylint: disable=pointless-statement camera.current_time except AmcrestError as ex: - _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) + _LOGGER.error("Unable to connect to %s camera: %s", name, str(ex)) hass.components.persistent_notification.create( 'Error: {}
' 'You will need to restart hass after fixing.' @@ -116,23 +146,19 @@ def setup(hass, config): notification_id=NOTIFICATION_ID) continue - ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) - name = device.get(CONF_NAME) - resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] + ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS] + resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] + binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) switches = device.get(CONF_SWITCHES) - stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] - - username = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD) + stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]] # currently aiohttp only works with basic authentication # only valid for mjpeg streaming - if username is not None and password is not None: - if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION: - authentication = aiohttp.BasicAuth(username, password) - else: - authentication = None + if device[CONF_AUTHENTICATION] == HTTP_BASIC_AUTHENTICATION: + authentication = aiohttp.BasicAuth(username, password) + else: + authentication = None hass.data[DATA_AMCREST][name] = AmcrestDevice( camera, name, authentication, ffmpeg_arguments, stream_source, @@ -143,6 +169,13 @@ def setup(hass, config): CONF_NAME: name, }, config) + if binary_sensors: + discovery.load_platform( + hass, 'binary_sensor', DOMAIN, { + CONF_NAME: name, + CONF_BINARY_SENSORS: binary_sensors + }, config) + if sensors: discovery.load_platform( hass, 'sensor', DOMAIN, { @@ -157,7 +190,7 @@ def setup(hass, config): CONF_SWITCHES: switches }, config) - return True + return len(hass.data[DATA_AMCREST]) >= 1 class AmcrestDevice: diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py new file mode 100644 index 00000000000..113918ed041 --- /dev/null +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -0,0 +1,71 @@ +"""Suppoort for Amcrest IP camera binary sensors.""" +from datetime import timedelta +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 + +DEPENDENCIES = ['amcrest'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=5) + + +async def async_setup_platform(hass, config, async_add_devices, + 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) + + +class AmcrestBinarySensor(BinarySensorDevice): + """Binary sensor for Amcrest camera.""" + + def __init__(self, name, camera, sensor_type): + """Initialize entity.""" + self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type]) + self._camera = camera + self._sensor_type = sensor_type + self._state = None + + @property + def name(self): + """Return entity name.""" + return self._name + + @property + def is_on(self): + """Return if entity is on.""" + return self._state + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_MOTION + + def update(self): + """Update entity.""" + from amcrest import AmcrestError + + _LOGGER.debug('Pulling data from %s binary sensor', self._name) + + try: + self._state = self._camera.is_motion_detected + except AmcrestError as error: + _LOGGER.error( + 'Could not update %s binary sensor due to error: %s', + self.name, error) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 853d5404dab..f361c4e0183 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -28,8 +28,6 @@ async def async_setup_platform(hass, config, async_add_entities, async_add_entities([AmcrestCam(hass, amcrest)], True) - return True - class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 68bc86da94c..119520e6a03 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -30,7 +30,6 @@ async def async_setup_platform( AmcrestSensor(amcrest.name, amcrest.device, sensor_type)) async_add_entities(amcrest_sensors, True) - return True class AmcrestSensor(Entity):