Refactored Amcrest to use central hub component (#8184)

* Refactored Amcrest to use central hub component

* Set default streaming source to snapshot

* Simplified code by using discovery platforms

* Makes lint happy

* Update authentication method to basic

* Fixed lint issues

* Makes Amcrest hub async

*  Make Amcrest hub IO synchronous and disabled ffmpeg dependency on sensor/amcrest.

* Removed async to load component

* Organized import order

* Update amcrest.py
This commit is contained in:
Marcelo Moreira de Mello 2017-07-11 04:10:10 -04:00 committed by Pascal Vizeli
parent ac72dea09a
commit f5e24cb0bb
5 changed files with 203 additions and 145 deletions

View file

@ -11,6 +11,9 @@ omit =
homeassistant/components/alarmdecoder.py homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py homeassistant/components/*/alarmdecoder.py
homeassistant/components/amcrest.py
homeassistant/components/*/amcrest.py
homeassistant/components/apcupsd.py homeassistant/components/apcupsd.py
homeassistant/components/*/apcupsd.py homeassistant/components/*/apcupsd.py
@ -220,7 +223,6 @@ omit =
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/binary_sensor/tapsaff.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
@ -388,7 +390,6 @@ omit =
homeassistant/components/remote/itach.py homeassistant/components/remote/itach.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/amcrest.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py

View file

@ -0,0 +1,149 @@
"""
This component provides basic support for Amcrest IP cameras.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/amcrest/
"""
import logging
from datetime import timedelta
import aiohttp
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
import homeassistant.loader as loader
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.0']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_AUTHENTICATION = 'authentication'
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'snapshot'
TIMEOUT = 10
DATA_AMCREST = 'amcrest'
DOMAIN = 'amcrest'
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
RESOLUTION_LIST = {
'high': 0,
'low': 1,
}
SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = {
'basic': 'basic'
}
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
# Sensor types are defined like: Name, units, icon
SENSORS = {
'motion_detector': ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
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): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_SENSORS, default=None):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
})])
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera
amcrest_cams = config[DOMAIN]
persistent_notification = loader.get_component('persistent_notification')
for device in amcrest_cams:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
try:
camera.current_time
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
name = device.get(CONF_NAME)
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
sensors = device.get(CONF_SENSORS)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
username = device.get(CONF_USERNAME)
password = device.get(CONF_PASSWORD)
# 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
discovery.load_platform(
hass, 'camera', DOMAIN, {
'device': camera,
CONF_AUTHENTICATION: authentication,
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
CONF_NAME: name,
CONF_RESOLUTION: resolution,
CONF_STREAM_SOURCE: stream_source,
}, config)
if sensors:
discovery.load_platform(
hass, 'sensor', DOMAIN, {
'device': camera,
CONF_NAME: name,
CONF_SENSORS: sensors,
}, config)
return True

View file

@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/
import asyncio import asyncio
import logging import logging
import aiohttp from homeassistant.components.amcrest import (
import voluptuous as vol STREAM_SOURCE_LIST, TIMEOUT)
from homeassistant.components.camera import Camera
import homeassistant.loader as loader
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web, async_get_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_stream) async_aiohttp_proxy_stream)
REQUIREMENTS = ['amcrest==1.2.0'] DEPENDENCIES = ['amcrest', 'ffmpeg']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera' @asyncio.coroutine
DEFAULT_PORT = 80 def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'mjpeg'
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
RESOLUTION_LIST = {
'high': 0,
'low': 1,
}
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
CONTENT_TYPE_HEADER = 'Content-Type'
TIMEOUT = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Amcrest IP Camera.""" """Set up an Amcrest IP Camera."""
from amcrest import AmcrestCamera if discovery_info is None:
camera = AmcrestCamera( return
config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
persistent_notification = loader.get_component('persistent_notification') device = discovery_info['device']
try: authentication = discovery_info['authentication']
camera.current_time ffmpeg_arguments = discovery_info['ffmpeg_arguments']
# pylint: disable=broad-except name = discovery_info['name']
except Exception as ex: resolution = discovery_info['resolution']
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) stream_source = discovery_info['stream_source']
persistent_notification.create(
hass, 'Error: {}<br />' async_add_devices([
'You will need to restart hass after fixing.' AmcrestCam(hass,
''.format(ex), name,
title=NOTIFICATION_TITLE, device,
notification_id=NOTIFICATION_ID) authentication,
return False ffmpeg_arguments,
stream_source,
resolution)], True)
add_devices([AmcrestCam(hass, config, camera)])
return True return True
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
def __init__(self, hass, device_info, camera): def __init__(self, hass, name, camera, authentication,
ffmpeg_arguments, stream_source, resolution):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super(AmcrestCam, self).__init__()
self._name = name
self._camera = camera self._camera = camera
self._base_url = self._camera.get_base_url() self._base_url = self._camera.get_base_url()
self._name = device_info.get(CONF_NAME)
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = ffmpeg_arguments
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] self._stream_source = stream_source
self._stream_source = STREAM_SOURCE_LIST[ self._resolution = resolution
device_info.get(CONF_STREAM_SOURCE) self._token = self._auth = authentication
]
self._token = self._auth = aiohttp.BasicAuth(
device_info.get(CONF_USERNAME),
password=device_info.get(CONF_PASSWORD)
)
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image reponse from the camera."""

View file

@ -4,92 +4,50 @@ This component provides HA sensor support for Amcrest IP cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.amcrest/ https://home-assistant.io/components/sensor.amcrest/
""" """
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import voluptuous as vol from homeassistant.components.amcrest import SENSORS
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS,
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.loader as loader from homeassistant.const import STATE_UNKNOWN
from requests.exceptions import HTTPError, ConnectTimeout DEPENDENCIES = ['amcrest']
REQUIREMENTS = ['amcrest==1.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Sensor Setup'
DEFAULT_NAME = 'Amcrest'
DEFAULT_PORT = 80
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
# Sensor types are defined like: Name, units, icon
SENSOR_TYPES = {
'motion_detector': ['Motion Detected', None, 'run'],
'sdcard': ['SD Used', '%', 'sd'],
'ptz_preset': ['PTZ Preset', None, 'camera-iris'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine
vol.Required(CONF_HOST): cv.string, def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
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.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for an Amcrest IP Camera.""" """Set up a sensor for an Amcrest IP Camera."""
from amcrest import AmcrestCamera if discovery_info is None:
return
camera = AmcrestCamera( device = discovery_info['device']
config.get(CONF_HOST), config.get(CONF_PORT), name = discovery_info['name']
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera sensors = discovery_info['sensors']
persistent_notification = loader.get_component('persistent_notification') amcrest_sensors = []
try: for sensor_type in sensors:
camera.current_time amcrest_sensors.append(AmcrestSensor(name, device, sensor_type))
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
sensors.append(AmcrestSensor(config, camera, sensor_type))
add_devices(sensors, True)
async_add_devices(amcrest_sensors, True)
return True return True
class AmcrestSensor(Entity): class AmcrestSensor(Entity):
"""A sensor implementation for Amcrest IP camera.""" """A sensor implementation for Amcrest IP camera."""
def __init__(self, device_info, camera, sensor_type): def __init__(self, name, camera, sensor_type):
"""Initialize a sensor for Amcrest camera.""" """Initialize a sensor for Amcrest camera."""
super(AmcrestSensor, self).__init__()
self._attrs = {} self._attrs = {}
self._camera = camera self._camera = camera
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._name = '{0}_{1}'.format(device_info.get(CONF_NAME), self._name = '{0}_{1}'.format(name,
SENSOR_TYPES.get(self._sensor_type)[0]) SENSORS.get(self._sensor_type)[0])
self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property @property
@ -115,7 +73,7 @@ class AmcrestSensor(Entity):
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the units of measurement.""" """Return the units of measurement."""
return SENSOR_TYPES.get(self._sensor_type)[1] return SENSORS.get(self._sensor_type)[1]
def update(self): def update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""

View file

@ -60,8 +60,7 @@ aiopvapi==1.4
# homeassistant.components.alarmdecoder # homeassistant.components.alarmdecoder
alarmdecoder==0.12.1.0 alarmdecoder==0.12.1.0
# homeassistant.components.camera.amcrest # homeassistant.components.amcrest
# homeassistant.components.sensor.amcrest
amcrest==1.2.0 amcrest==1.2.0
# homeassistant.components.media_player.anthemav # homeassistant.components.media_player.anthemav