mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +00:00
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:
parent
a48c0f2991
commit
34bb31f4ec
@ -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:
|
||||||
|
71
homeassistant/components/amcrest/binary_sensor.py
Normal file
71
homeassistant/components/amcrest/binary_sensor.py
Normal 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)
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user