Add amcrest binary_sensors (#22703)

* Add amcrest binary_sensors

Add binary_sensors with option motion_detected. Deprecate motion_detector sensor.

* Update per review

* Update per review

Add custom validators to make sure camera names are unique, and to issue warning if deprecated sensors option motion_detector is used.

async_setup_platform should not return a value.

* Another review update

Since there is only one type of binary_sensor, remove type test in update method.
This commit is contained in:
Phil Bruckner 2019-04-09 08:21:47 -05:00 committed by Sebastian Muszynski
parent a48c0f2991
commit 34bb31f4ec
4 changed files with 147 additions and 46 deletions

View File

@ -7,11 +7,11 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, 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 from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.3.0'] REQUIREMENTS = ['amcrest==1.3.0']
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
@ -52,9 +52,14 @@ STREAM_SOURCE_LIST = {
'rtsp': 2, 'rtsp': 2,
} }
BINARY_SENSORS = {
'motion_detected': 'Motion Detected'
}
# Sensor types are defined like: Name, units, icon # Sensor types are defined like: Name, units, icon
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSORS = { SENSORS = {
'motion_detector': ['Motion Detected', None, 'mdi:run'], SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'], 'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
} }
@ -65,8 +70,24 @@ SWITCHES = {
'motion_recording': ['Motion Recording', 'mdi:record-rec'] 'motion_recording': ['Motion Recording', 'mdi:record-rec']
} }
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ 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_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
@ -82,11 +103,16 @@ CONFIG_SCHEMA = vol.Schema({
cv.string, cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period, cv.time_period,
vol.Optional(CONF_BINARY_SENSORS):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
vol.Optional(CONF_SENSORS): vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]), vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
vol.Optional(CONF_SWITCHES): vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]), vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})]) })
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -94,20 +120,24 @@ def setup(hass, config):
"""Set up the Amcrest IP Camera component.""" """Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera, AmcrestError from amcrest import AmcrestCamera, AmcrestError
hass.data[DATA_AMCREST] = {} hass.data.setdefault(DATA_AMCREST, {})
amcrest_cams = config[DOMAIN] amcrest_cams = config[DOMAIN]
for device in amcrest_cams: for device in amcrest_cams:
name = device[CONF_NAME]
username = device[CONF_USERNAME]
password = device[CONF_PASSWORD]
try: try:
camera = AmcrestCamera(device.get(CONF_HOST), camera = AmcrestCamera(device[CONF_HOST],
device.get(CONF_PORT), device[CONF_PORT],
device.get(CONF_USERNAME), username,
device.get(CONF_PASSWORD)).camera password).camera
# pylint: disable=pointless-statement # pylint: disable=pointless-statement
camera.current_time camera.current_time
except AmcrestError as ex: 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( hass.components.persistent_notification.create(
'Error: {}<br />' 'Error: {}<br />'
'You will need to restart hass after fixing.' 'You will need to restart hass after fixing.'
@ -116,20 +146,16 @@ def setup(hass, config):
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID)
continue continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS]
name = device.get(CONF_NAME) resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]]
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] binary_sensors = device.get(CONF_BINARY_SENSORS)
sensors = device.get(CONF_SENSORS) sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES) switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]]
username = device.get(CONF_USERNAME)
password = device.get(CONF_PASSWORD)
# currently aiohttp only works with basic authentication # currently aiohttp only works with basic authentication
# only valid for mjpeg streaming # only valid for mjpeg streaming
if username is not None and password is not None: if device[CONF_AUTHENTICATION] == HTTP_BASIC_AUTHENTICATION:
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
authentication = aiohttp.BasicAuth(username, password) authentication = aiohttp.BasicAuth(username, password)
else: else:
authentication = None authentication = None
@ -143,6 +169,13 @@ def setup(hass, config):
CONF_NAME: name, CONF_NAME: name,
}, config) }, config)
if binary_sensors:
discovery.load_platform(
hass, 'binary_sensor', DOMAIN, {
CONF_NAME: name,
CONF_BINARY_SENSORS: binary_sensors
}, config)
if sensors: if sensors:
discovery.load_platform( discovery.load_platform(
hass, 'sensor', DOMAIN, { hass, 'sensor', DOMAIN, {
@ -157,7 +190,7 @@ def setup(hass, config):
CONF_SWITCHES: switches CONF_SWITCHES: switches
}, config) }, config)
return True return len(hass.data[DATA_AMCREST]) >= 1
class AmcrestDevice: class AmcrestDevice:

View File

@ -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)

View File

@ -28,8 +28,6 @@ async def async_setup_platform(hass, config, async_add_entities,
async_add_entities([AmcrestCam(hass, amcrest)], True) async_add_entities([AmcrestCam(hass, amcrest)], True)
return True
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""

View File

@ -30,7 +30,6 @@ async def async_setup_platform(
AmcrestSensor(amcrest.name, amcrest.device, sensor_type)) AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
async_add_entities(amcrest_sensors, True) async_add_entities(amcrest_sensors, True)
return True
class AmcrestSensor(Entity): class AmcrestSensor(Entity):