From f5e24cb0bbaf9b18c199302687f5e96b623f8755 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 11 Jul 2017 04:10:10 -0400 Subject: [PATCH] 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 --- .coveragerc | 5 +- homeassistant/components/amcrest.py | 149 +++++++++++++++++++++ homeassistant/components/camera/amcrest.py | 109 +++++---------- homeassistant/components/sensor/amcrest.py | 82 +++--------- requirements_all.txt | 3 +- 5 files changed, 203 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/amcrest.py diff --git a/.coveragerc b/.coveragerc index 0defebb7e7a..e4d896e444b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,9 @@ omit = homeassistant/components/alarmdecoder.py homeassistant/components/*/alarmdecoder.py + homeassistant/components/amcrest.py + homeassistant/components/*/amcrest.py + homeassistant/components/apcupsd.py homeassistant/components/*/apcupsd.py @@ -220,7 +223,6 @@ omit = homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/browser.py - homeassistant/components/camera/amcrest.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py @@ -388,7 +390,6 @@ omit = homeassistant/components/remote/itach.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py - homeassistant/components/sensor/amcrest.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/arwn.py homeassistant/components/sensor/bbox.py diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py new file mode 100644 index 00000000000..8a40c790c12 --- /dev/null +++ b/homeassistant/components/amcrest.py @@ -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: {}
' + '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 diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 8f8b7e5f9f5..51b8ff13906 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/ import asyncio import logging -import aiohttp -import voluptuous as vol - -import homeassistant.loader as loader -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.amcrest import ( + STREAM_SOURCE_LIST, TIMEOUT) +from homeassistant.components.camera import Camera 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 ( async_get_clientsession, async_aiohttp_proxy_web, async_aiohttp_proxy_stream) -REQUIREMENTS = ['amcrest==1.2.0'] -DEPENDENCIES = ['ffmpeg'] +DEPENDENCIES = ['amcrest', 'ffmpeg'] _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' -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): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up an Amcrest IP Camera.""" - from amcrest import AmcrestCamera - camera = AmcrestCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera + if discovery_info is None: + return - persistent_notification = loader.get_component('persistent_notification') - try: - camera.current_time - # pylint: disable=broad-except - except Exception as ex: - _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) - persistent_notification.create( - hass, 'Error: {}
' - 'You will need to restart hass after fixing.' - ''.format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False + device = discovery_info['device'] + authentication = discovery_info['authentication'] + ffmpeg_arguments = discovery_info['ffmpeg_arguments'] + name = discovery_info['name'] + resolution = discovery_info['resolution'] + stream_source = discovery_info['stream_source'] + + async_add_devices([ + AmcrestCam(hass, + name, + device, + authentication, + ffmpeg_arguments, + stream_source, + resolution)], True) - add_devices([AmcrestCam(hass, config, camera)]) return True class AmcrestCam(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.""" super(AmcrestCam, self).__init__() + self._name = name self._camera = camera self._base_url = self._camera.get_base_url() - self._name = device_info.get(CONF_NAME) self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) - self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] - self._stream_source = STREAM_SOURCE_LIST[ - device_info.get(CONF_STREAM_SOURCE) - ] - self._token = self._auth = aiohttp.BasicAuth( - device_info.get(CONF_USERNAME), - password=device_info.get(CONF_PASSWORD) - ) + self._ffmpeg_arguments = ffmpeg_arguments + self._stream_source = stream_source + self._resolution = resolution + self._token = self._auth = authentication def camera_image(self): """Return a still image reponse from the camera.""" diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 23f7fc4dfbe..e7bf309c33a 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -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 https://home-assistant.io/components/sensor.amcrest/ """ +import asyncio from datetime import timedelta import logging -import voluptuous as vol -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.components.amcrest import SENSORS from homeassistant.helpers.entity import Entity -import homeassistant.loader as loader +from homeassistant.const import STATE_UNKNOWN -from requests.exceptions import HTTPError, ConnectTimeout - -REQUIREMENTS = ['amcrest==1.2.0'] +DEPENDENCIES = ['amcrest'] _LOGGER = logging.getLogger(__name__) -NOTIFICATION_ID = 'amcrest_notification' -NOTIFICATION_TITLE = 'Amcrest Sensor Setup' - -DEFAULT_NAME = 'Amcrest' -DEFAULT_PORT = 80 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({ - 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.Required(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up a sensor for an Amcrest IP Camera.""" - from amcrest import AmcrestCamera + if discovery_info is None: + return - camera = AmcrestCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera + device = discovery_info['device'] + name = discovery_info['name'] + sensors = discovery_info['sensors'] - persistent_notification = loader.get_component('persistent_notification') - 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: {}
' - '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) + amcrest_sensors = [] + for sensor_type in sensors: + amcrest_sensors.append(AmcrestSensor(name, device, sensor_type)) + async_add_devices(amcrest_sensors, True) return True class AmcrestSensor(Entity): """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.""" - super(AmcrestSensor, self).__init__() self._attrs = {} self._camera = camera self._sensor_type = sensor_type - self._name = '{0}_{1}'.format(device_info.get(CONF_NAME), - SENSOR_TYPES.get(self._sensor_type)[0]) - self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2]) + self._name = '{0}_{1}'.format(name, + SENSORS.get(self._sensor_type)[0]) + self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2]) self._state = STATE_UNKNOWN @property @@ -115,7 +73,7 @@ class AmcrestSensor(Entity): @property def unit_of_measurement(self): """Return the units of measurement.""" - return SENSOR_TYPES.get(self._sensor_type)[1] + return SENSORS.get(self._sensor_type)[1] def update(self): """Get the latest data and updates the state.""" diff --git a/requirements_all.txt b/requirements_all.txt index 80eb6987ce8..5d09b63df80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -60,8 +60,7 @@ aiopvapi==1.4 # homeassistant.components.alarmdecoder alarmdecoder==0.12.1.0 -# homeassistant.components.camera.amcrest -# homeassistant.components.sensor.amcrest +# homeassistant.components.amcrest amcrest==1.2.0 # homeassistant.components.media_player.anthemav