2016-10-13 15:49:58 +00:00
|
|
|
"""
|
|
|
|
Support for Synology Surveillance Station Cameras.
|
|
|
|
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/camera.synology/
|
|
|
|
"""
|
2016-10-28 04:40:10 +00:00
|
|
|
import asyncio
|
2016-10-13 15:49:58 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2016-11-11 05:04:47 +00:00
|
|
|
import aiohttp
|
2016-10-28 04:40:10 +00:00
|
|
|
from aiohttp import web
|
|
|
|
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
|
|
|
import async_timeout
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
2016-11-28 00:26:46 +00:00
|
|
|
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
2016-10-13 15:49:58 +00:00
|
|
|
from homeassistant.components.camera import (
|
|
|
|
Camera, PLATFORM_SCHEMA)
|
2016-11-28 00:26:46 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import (
|
|
|
|
async_get_clientsession, async_create_clientsession)
|
2016-10-13 15:49:58 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-10-28 04:40:10 +00:00
|
|
|
from homeassistant.util.async import run_coroutine_threadsafe
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEFAULT_NAME = 'Synology Camera'
|
|
|
|
DEFAULT_STREAM_ID = '0'
|
|
|
|
TIMEOUT = 5
|
|
|
|
CONF_CAMERA_NAME = 'camera_name'
|
|
|
|
CONF_STREAM_ID = 'stream_id'
|
|
|
|
|
|
|
|
QUERY_CGI = 'query.cgi'
|
|
|
|
QUERY_API = 'SYNO.API.Info'
|
|
|
|
AUTH_API = 'SYNO.API.Auth'
|
|
|
|
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
|
|
|
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
|
|
|
SESSION_ID = '0'
|
|
|
|
|
|
|
|
WEBAPI_PATH = '/webapi/'
|
|
|
|
AUTH_PATH = 'auth.cgi'
|
|
|
|
CAMERA_PATH = 'camera.cgi'
|
|
|
|
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
2016-10-28 04:40:10 +00:00
|
|
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
SYNO_API_URL = '{0}{1}{2}'
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
|
|
vol.Required(CONF_URL): cv.string,
|
|
|
|
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
2016-11-04 01:41:32 +00:00
|
|
|
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
2016-10-13 15:49:58 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
2016-10-13 15:49:58 +00:00
|
|
|
"""Setup a Synology IP Camera."""
|
2016-11-28 00:26:46 +00:00
|
|
|
verify_ssl = config.get(CONF_VERIFY_SSL)
|
|
|
|
websession_init = async_get_clientsession(hass, verify_ssl)
|
2016-11-07 06:17:56 +00:00
|
|
|
|
2016-10-13 15:49:58 +00:00
|
|
|
# Determine API to use for authentication
|
2016-10-28 04:40:10 +00:00
|
|
|
syno_api_url = SYNO_API_URL.format(
|
|
|
|
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
|
|
|
|
|
|
|
query_payload = {
|
|
|
|
'api': QUERY_API,
|
|
|
|
'method': 'Query',
|
|
|
|
'version': '1',
|
|
|
|
'query': 'SYNO.'
|
|
|
|
}
|
2016-11-30 21:05:58 +00:00
|
|
|
query_req = None
|
2016-10-28 04:40:10 +00:00
|
|
|
try:
|
|
|
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
2016-11-11 05:04:47 +00:00
|
|
|
query_req = yield from websession_init.get(
|
2016-10-28 04:40:10 +00:00
|
|
|
syno_api_url,
|
2016-11-11 05:04:47 +00:00
|
|
|
params=query_payload
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-11-30 21:05:58 +00:00
|
|
|
|
|
|
|
query_resp = yield from query_req.json()
|
|
|
|
auth_path = query_resp['data'][AUTH_API]['path']
|
|
|
|
camera_api = query_resp['data'][CAMERA_API]['path']
|
|
|
|
camera_path = query_resp['data'][CAMERA_API]['path']
|
|
|
|
streaming_path = query_resp['data'][STREAMING_API]['path']
|
|
|
|
|
2016-11-11 05:04:47 +00:00
|
|
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
|
|
_LOGGER.exception("Error on %s", syno_api_url)
|
2016-10-28 04:40:10 +00:00
|
|
|
return False
|
|
|
|
|
2016-11-30 21:05:58 +00:00
|
|
|
finally:
|
|
|
|
if query_req is not None:
|
|
|
|
yield from query_req.release()
|
2016-10-28 04:40:10 +00:00
|
|
|
|
2016-10-13 15:49:58 +00:00
|
|
|
# Authticate to NAS to get a session id
|
2016-10-28 04:40:10 +00:00
|
|
|
syno_auth_url = SYNO_API_URL.format(
|
|
|
|
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
|
|
|
|
|
|
|
session_id = yield from get_session_id(
|
|
|
|
hass,
|
2016-11-11 05:04:47 +00:00
|
|
|
websession_init,
|
2016-10-28 04:40:10 +00:00
|
|
|
config.get(CONF_USERNAME),
|
|
|
|
config.get(CONF_PASSWORD),
|
2016-11-11 05:04:47 +00:00
|
|
|
syno_auth_url
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-10-13 15:49:58 +00:00
|
|
|
|
2016-11-11 05:04:47 +00:00
|
|
|
# init websession
|
2016-11-28 00:26:46 +00:00
|
|
|
websession = async_create_clientsession(
|
|
|
|
hass, verify_ssl, cookies={'id': session_id})
|
2016-11-11 05:04:47 +00:00
|
|
|
|
2016-10-13 15:49:58 +00:00
|
|
|
# Use SessionID to get cameras in system
|
2016-10-28 04:40:10 +00:00
|
|
|
syno_camera_url = SYNO_API_URL.format(
|
|
|
|
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
|
|
|
|
|
|
|
camera_payload = {
|
|
|
|
'api': CAMERA_API,
|
|
|
|
'method': 'List',
|
|
|
|
'version': '1'
|
|
|
|
}
|
|
|
|
try:
|
|
|
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
2016-11-11 05:04:47 +00:00
|
|
|
camera_req = yield from websession.get(
|
2016-10-28 04:40:10 +00:00
|
|
|
syno_camera_url,
|
2016-11-11 05:04:47 +00:00
|
|
|
params=camera_payload
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-11-11 05:04:47 +00:00
|
|
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
|
|
_LOGGER.exception("Error on %s", syno_camera_url)
|
2016-10-28 04:40:10 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
camera_resp = yield from camera_req.json()
|
2016-10-13 15:49:58 +00:00
|
|
|
cameras = camera_resp['data']['cameras']
|
2016-10-28 04:40:10 +00:00
|
|
|
yield from camera_req.release()
|
|
|
|
|
|
|
|
# add cameras
|
|
|
|
devices = []
|
2016-10-13 15:49:58 +00:00
|
|
|
for camera in cameras:
|
|
|
|
if not config.get(CONF_WHITELIST):
|
|
|
|
camera_id = camera['id']
|
|
|
|
snapshot_path = camera['snapshot_path']
|
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
device = SynologyCamera(
|
2016-11-11 05:04:47 +00:00
|
|
|
hass,
|
|
|
|
websession,
|
2016-10-28 04:40:10 +00:00
|
|
|
config,
|
|
|
|
camera_id,
|
|
|
|
camera['name'],
|
|
|
|
snapshot_path,
|
|
|
|
streaming_path,
|
|
|
|
camera_path,
|
|
|
|
auth_path
|
|
|
|
)
|
|
|
|
devices.append(device)
|
|
|
|
|
2016-11-08 06:31:40 +00:00
|
|
|
yield from async_add_devices(devices)
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
@asyncio.coroutine
|
2016-11-11 05:04:47 +00:00
|
|
|
def get_session_id(hass, websession, username, password, login_url):
|
2016-10-13 15:49:58 +00:00
|
|
|
"""Get a session id."""
|
2016-10-28 04:40:10 +00:00
|
|
|
auth_payload = {
|
|
|
|
'api': AUTH_API,
|
|
|
|
'method': 'Login',
|
|
|
|
'version': '2',
|
|
|
|
'account': username,
|
|
|
|
'passwd': password,
|
|
|
|
'session': 'SurveillanceStation',
|
|
|
|
'format': 'sid'
|
|
|
|
}
|
2016-11-30 21:05:58 +00:00
|
|
|
auth_req = None
|
2016-10-28 04:40:10 +00:00
|
|
|
try:
|
|
|
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
2016-11-11 05:04:47 +00:00
|
|
|
auth_req = yield from websession.get(
|
2016-10-28 04:40:10 +00:00
|
|
|
login_url,
|
2016-11-11 05:04:47 +00:00
|
|
|
params=auth_payload
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-11-30 21:05:58 +00:00
|
|
|
auth_resp = yield from auth_req.json()
|
|
|
|
return auth_resp['data']['sid']
|
|
|
|
|
2016-11-11 05:04:47 +00:00
|
|
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
|
|
_LOGGER.exception("Error on %s", login_url)
|
2016-10-28 04:40:10 +00:00
|
|
|
return False
|
|
|
|
|
2016-11-30 21:05:58 +00:00
|
|
|
finally:
|
|
|
|
if auth_req is not None:
|
|
|
|
yield from auth_req.release()
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SynologyCamera(Camera):
|
|
|
|
"""An implementation of a Synology NAS based IP camera."""
|
|
|
|
|
2016-11-11 05:04:47 +00:00
|
|
|
def __init__(self, hass, websession, config, camera_id,
|
|
|
|
camera_name, snapshot_path, streaming_path, camera_path,
|
|
|
|
auth_path):
|
2016-10-13 15:49:58 +00:00
|
|
|
"""Initialize a Synology Surveillance Station camera."""
|
|
|
|
super().__init__()
|
2016-11-11 05:04:47 +00:00
|
|
|
self.hass = hass
|
|
|
|
self._websession = websession
|
2016-10-13 15:49:58 +00:00
|
|
|
self._name = camera_name
|
|
|
|
self._synology_url = config.get(CONF_URL)
|
|
|
|
self._camera_name = config.get(CONF_CAMERA_NAME)
|
|
|
|
self._stream_id = config.get(CONF_STREAM_ID)
|
|
|
|
self._camera_id = camera_id
|
|
|
|
self._snapshot_path = snapshot_path
|
|
|
|
self._streaming_path = streaming_path
|
|
|
|
self._camera_path = camera_path
|
|
|
|
self._auth_path = auth_path
|
|
|
|
|
|
|
|
def camera_image(self):
|
2016-10-28 04:40:10 +00:00
|
|
|
"""Return bytes of camera image."""
|
|
|
|
return run_coroutine_threadsafe(
|
|
|
|
self.async_camera_image(), self.hass.loop).result()
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def async_camera_image(self):
|
2016-10-13 15:49:58 +00:00
|
|
|
"""Return a still image response from the camera."""
|
2016-10-28 04:40:10 +00:00
|
|
|
image_url = SYNO_API_URL.format(
|
|
|
|
self._synology_url, WEBAPI_PATH, self._camera_path)
|
|
|
|
|
|
|
|
image_payload = {
|
|
|
|
'api': CAMERA_API,
|
|
|
|
'method': 'GetSnapshot',
|
|
|
|
'version': '1',
|
|
|
|
'cameraId': self._camera_id
|
|
|
|
}
|
2016-10-13 15:49:58 +00:00
|
|
|
try:
|
2016-10-28 04:40:10 +00:00
|
|
|
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
2016-11-11 05:04:47 +00:00
|
|
|
response = yield from self._websession.get(
|
2016-10-28 04:40:10 +00:00
|
|
|
image_url,
|
2016-11-11 05:04:47 +00:00
|
|
|
params=image_payload
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-11-11 05:04:47 +00:00
|
|
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
|
|
_LOGGER.exception("Error on %s", image_url)
|
2016-10-13 15:49:58 +00:00
|
|
|
return None
|
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
image = yield from response.read()
|
|
|
|
yield from response.release()
|
2016-10-13 15:49:58 +00:00
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
return image
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def handle_async_mjpeg_stream(self, request):
|
2016-10-13 15:49:58 +00:00
|
|
|
"""Return a MJPEG stream image response directly from the camera."""
|
2016-10-28 04:40:10 +00:00
|
|
|
streaming_url = SYNO_API_URL.format(
|
|
|
|
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
|
|
|
|
|
|
|
streaming_payload = {
|
|
|
|
'api': STREAMING_API,
|
|
|
|
'method': 'Stream',
|
|
|
|
'version': '1',
|
|
|
|
'cameraId': self._camera_id,
|
|
|
|
'format': 'mjpeg'
|
|
|
|
}
|
2016-11-30 21:05:58 +00:00
|
|
|
stream = None
|
|
|
|
response = None
|
2016-10-28 04:40:10 +00:00
|
|
|
try:
|
|
|
|
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
2016-11-11 05:04:47 +00:00
|
|
|
stream = yield from self._websession.get(
|
2016-10-28 04:40:10 +00:00
|
|
|
streaming_url,
|
2016-11-11 05:04:47 +00:00
|
|
|
params=streaming_payload
|
2016-10-28 04:40:10 +00:00
|
|
|
)
|
2016-11-30 21:05:58 +00:00
|
|
|
response = web.StreamResponse()
|
|
|
|
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
2016-10-28 04:40:10 +00:00
|
|
|
|
2016-11-30 21:05:58 +00:00
|
|
|
yield from response.prepare(request)
|
2016-10-28 04:40:10 +00:00
|
|
|
|
|
|
|
while True:
|
|
|
|
data = yield from stream.content.read(102400)
|
|
|
|
if not data:
|
|
|
|
break
|
|
|
|
response.write(data)
|
2016-11-30 21:05:58 +00:00
|
|
|
|
|
|
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
|
|
|
_LOGGER.exception("Error on %s", streaming_url)
|
|
|
|
raise HTTPGatewayTimeout()
|
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
finally:
|
2016-11-30 21:05:58 +00:00
|
|
|
if stream is not None:
|
|
|
|
self.hass.async_add_job(stream.release())
|
|
|
|
if response is not None:
|
|
|
|
yield from response.write_eof()
|
2016-10-13 15:49:58 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of this device."""
|
|
|
|
return self._name
|