2019-04-03 15:40:03 +00:00
|
|
|
"""Support for Ubiquiti's UVC cameras."""
|
2016-02-06 01:24:44 +00:00
|
|
|
import logging
|
2020-05-11 22:50:54 +00:00
|
|
|
import re
|
2016-02-06 01:24:44 +00:00
|
|
|
|
|
|
|
import requests
|
2019-11-26 18:11:10 +00:00
|
|
|
from uvcclient import camera as uvc_camera, nvr
|
2016-09-18 21:22:32 +00:00
|
|
|
import voluptuous as vol
|
2016-02-06 01:24:44 +00:00
|
|
|
|
2020-01-03 10:30:26 +00:00
|
|
|
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
2018-12-01 10:58:59 +00:00
|
|
|
from homeassistant.const import CONF_PORT, CONF_SSL
|
2018-06-09 05:22:17 +00:00
|
|
|
from homeassistant.exceptions import PlatformNotReady
|
2019-11-26 18:11:10 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-02-06 01:24:44 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_NVR = "nvr"
|
|
|
|
CONF_KEY = "key"
|
|
|
|
CONF_PASSWORD = "password"
|
2016-09-18 21:22:32 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_PASSWORD = "ubnt"
|
2016-09-18 21:22:32 +00:00
|
|
|
DEFAULT_PORT = 7080
|
2018-12-01 10:58:59 +00:00
|
|
|
DEFAULT_SSL = False
|
2016-09-18 21:22:32 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_NVR): cv.string,
|
|
|
|
vol.Required(CONF_KEY): cv.string,
|
|
|
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
|
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
|
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
|
|
|
}
|
|
|
|
)
|
2016-09-18 21:22:32 +00:00
|
|
|
|
2016-02-06 01:24:44 +00:00
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Discover cameras on a Unifi NVR."""
|
2016-09-18 21:22:32 +00:00
|
|
|
addr = config[CONF_NVR]
|
|
|
|
key = config[CONF_KEY]
|
2017-05-10 04:54:38 +00:00
|
|
|
password = config[CONF_PASSWORD]
|
2016-09-18 21:22:32 +00:00
|
|
|
port = config[CONF_PORT]
|
2018-12-01 10:58:59 +00:00
|
|
|
ssl = config[CONF_SSL]
|
2016-02-06 01:24:44 +00:00
|
|
|
|
|
|
|
try:
|
2018-06-09 05:22:17 +00:00
|
|
|
# Exceptions may be raised in all method calls to the nvr library.
|
2018-12-01 10:58:59 +00:00
|
|
|
nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl)
|
2016-02-06 01:24:44 +00:00
|
|
|
cameras = nvrconn.index()
|
2018-06-09 05:22:17 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
identifier = "id" if nvrconn.server_version >= (3, 2, 0) else "uuid"
|
2018-06-09 05:22:17 +00:00
|
|
|
# Filter out airCam models, which are not supported in the latest
|
|
|
|
# version of UnifiVideo and which are EOL by Ubiquiti
|
|
|
|
cameras = [
|
2019-07-31 19:25:30 +00:00
|
|
|
camera
|
|
|
|
for camera in cameras
|
|
|
|
if "airCam" not in nvrconn.get_camera(camera[identifier])["model"]
|
|
|
|
]
|
2016-02-06 01:24:44 +00:00
|
|
|
except nvr.NotAuthorized:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Authorization failure while connecting to NVR")
|
2016-02-06 01:24:44 +00:00
|
|
|
return False
|
2018-06-09 05:22:17 +00:00
|
|
|
except nvr.NvrError as ex:
|
|
|
|
_LOGGER.error("NVR refuses to talk to me: %s", str(ex))
|
2020-08-28 11:50:32 +00:00
|
|
|
raise PlatformNotReady from ex
|
2016-02-06 01:24:44 +00:00
|
|
|
except requests.exceptions.ConnectionError as ex:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
2020-08-28 11:50:32 +00:00
|
|
|
raise PlatformNotReady from ex
|
2016-02-23 20:01:51 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
add_entities(
|
|
|
|
[
|
|
|
|
UnifiVideoCamera(nvrconn, camera[identifier], camera["name"], password)
|
|
|
|
for camera in cameras
|
2020-04-23 22:52:36 +00:00
|
|
|
],
|
|
|
|
True,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-02-22 22:06:06 +00:00
|
|
|
return True
|
2016-02-06 01:24:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
class UnifiVideoCamera(Camera):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""A Ubiquiti Unifi Video Camera."""
|
2016-03-07 19:29:54 +00:00
|
|
|
|
2019-11-26 18:11:10 +00:00
|
|
|
def __init__(self, camera, uuid, name, password):
|
2016-03-07 19:29:54 +00:00
|
|
|
"""Initialize an Unifi camera."""
|
2019-09-24 22:38:20 +00:00
|
|
|
super().__init__()
|
2019-11-26 18:11:10 +00:00
|
|
|
self._nvr = camera
|
2016-02-06 01:24:44 +00:00
|
|
|
self._uuid = uuid
|
|
|
|
self._name = name
|
2017-05-10 04:54:38 +00:00
|
|
|
self._password = password
|
2016-02-06 01:24:44 +00:00
|
|
|
self.is_streaming = False
|
2016-02-14 17:06:46 +00:00
|
|
|
self._connect_addr = None
|
|
|
|
self._camera = None
|
2018-01-11 23:44:23 +00:00
|
|
|
self._motion_status = False
|
2020-04-23 22:52:36 +00:00
|
|
|
self._caminfo = None
|
2016-02-06 01:24:44 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return the name of this camera."""
|
2016-02-06 01:24:44 +00:00
|
|
|
return self._name
|
|
|
|
|
2020-04-23 22:52:36 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""If this entity should be polled."""
|
|
|
|
return True
|
|
|
|
|
2020-01-03 10:30:26 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Return supported features."""
|
2020-04-23 22:52:36 +00:00
|
|
|
channels = self._caminfo["channels"]
|
2020-01-03 10:30:26 +00:00
|
|
|
for channel in channels:
|
|
|
|
if channel["isRtspEnabled"]:
|
|
|
|
return SUPPORT_STREAM
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
2016-02-06 01:24:44 +00:00
|
|
|
@property
|
|
|
|
def is_recording(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return true if the camera is recording."""
|
2020-04-23 22:52:36 +00:00
|
|
|
return self._caminfo["recordingSettings"]["fullTimeRecordEnabled"]
|
2016-02-06 01:24:44 +00:00
|
|
|
|
2018-01-11 23:44:23 +00:00
|
|
|
@property
|
|
|
|
def motion_detection_enabled(self):
|
|
|
|
"""Camera Motion Detection Status."""
|
2020-04-23 22:52:36 +00:00
|
|
|
return self._caminfo["recordingSettings"]["motionRecordEnabled"]
|
2018-01-11 23:44:23 +00:00
|
|
|
|
2021-01-10 22:23:32 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self) -> str:
|
|
|
|
"""Return a unique identifier for this client."""
|
|
|
|
return self._uuid
|
|
|
|
|
2016-02-14 17:09:54 +00:00
|
|
|
@property
|
|
|
|
def brand(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return the brand of this camera."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "Ubiquiti"
|
2016-02-14 17:09:54 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def model(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return the model of this camera."""
|
2020-04-23 22:52:36 +00:00
|
|
|
return self._caminfo["model"]
|
2016-02-14 17:09:54 +00:00
|
|
|
|
2016-02-14 17:06:46 +00:00
|
|
|
def _login(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Login to the camera."""
|
2020-04-23 22:52:36 +00:00
|
|
|
caminfo = self._caminfo
|
2016-02-14 17:06:46 +00:00
|
|
|
if self._connect_addr:
|
|
|
|
addrs = [self._connect_addr]
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
addrs = [caminfo["host"], caminfo["internalHost"]]
|
2016-02-14 17:06:46 +00:00
|
|
|
|
2016-06-05 17:09:58 +00:00
|
|
|
if self._nvr.server_version >= (3, 2, 0):
|
|
|
|
client_cls = uvc_camera.UVCCameraClientV320
|
|
|
|
else:
|
|
|
|
client_cls = uvc_camera.UVCCameraClient
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if caminfo["username"] is None:
|
|
|
|
caminfo["username"] = "ubnt"
|
2018-01-28 09:50:23 +00:00
|
|
|
|
2016-02-06 01:24:44 +00:00
|
|
|
camera = None
|
2016-02-14 17:06:46 +00:00
|
|
|
for addr in addrs:
|
2016-02-06 01:24:44 +00:00
|
|
|
try:
|
2019-07-31 19:25:30 +00:00
|
|
|
camera = client_cls(addr, caminfo["username"], self._password)
|
2016-02-14 17:06:46 +00:00
|
|
|
camera.login()
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Logged into UVC camera %(name)s via %(addr)s",
|
2020-10-06 13:02:23 +00:00
|
|
|
{"name": self._name, "addr": addr},
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-02-14 17:06:46 +00:00
|
|
|
self._connect_addr = addr
|
2016-02-22 22:06:06 +00:00
|
|
|
break
|
2020-04-04 20:09:11 +00:00
|
|
|
except OSError:
|
2016-02-06 01:24:44 +00:00
|
|
|
pass
|
2016-02-14 16:36:51 +00:00
|
|
|
except uvc_camera.CameraConnectError:
|
|
|
|
pass
|
|
|
|
except uvc_camera.CameraAuthError:
|
|
|
|
pass
|
2016-02-22 22:06:06 +00:00
|
|
|
if not self._connect_addr:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Unable to login to camera")
|
2016-02-06 01:24:44 +00:00
|
|
|
return None
|
|
|
|
|
2016-02-14 17:06:46 +00:00
|
|
|
self._camera = camera
|
2020-04-23 22:52:36 +00:00
|
|
|
self._caminfo = caminfo
|
2016-02-14 17:06:46 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
def camera_image(self):
|
2016-03-07 16:45:06 +00:00
|
|
|
"""Return the image of this camera."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-02-14 17:06:46 +00:00
|
|
|
if not self._camera:
|
|
|
|
if not self._login():
|
|
|
|
return
|
|
|
|
|
|
|
|
def _get_image(retry=True):
|
|
|
|
try:
|
|
|
|
return self._camera.get_snapshot()
|
|
|
|
except uvc_camera.CameraConnectError:
|
2017-04-30 05:04:49 +00:00
|
|
|
_LOGGER.error("Unable to contact camera")
|
2016-02-14 17:06:46 +00:00
|
|
|
except uvc_camera.CameraAuthError:
|
|
|
|
if retry:
|
|
|
|
self._login()
|
|
|
|
return _get_image(retry=False)
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Unable to log into camera, unable to get snapshot")
|
2018-07-23 08:16:05 +00:00
|
|
|
raise
|
2016-02-14 17:06:46 +00:00
|
|
|
|
|
|
|
return _get_image()
|
2018-01-11 23:44:23 +00:00
|
|
|
|
|
|
|
def set_motion_detection(self, mode):
|
|
|
|
"""Set motion detection on or off."""
|
2020-05-05 01:02:09 +00:00
|
|
|
set_mode = "motion" if mode is True else "none"
|
2018-01-11 23:44:23 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
self._nvr.set_recordmode(self._uuid, set_mode)
|
|
|
|
self._motion_status = mode
|
2019-11-26 18:11:10 +00:00
|
|
|
except nvr.NvrError as err:
|
2018-02-11 17:20:28 +00:00
|
|
|
_LOGGER.error("Unable to set recordmode to %s", set_mode)
|
2018-01-11 23:44:23 +00:00
|
|
|
_LOGGER.debug(err)
|
|
|
|
|
|
|
|
def enable_motion_detection(self):
|
|
|
|
"""Enable motion detection in camera."""
|
|
|
|
self.set_motion_detection(True)
|
|
|
|
|
|
|
|
def disable_motion_detection(self):
|
|
|
|
"""Disable motion detection in camera."""
|
|
|
|
self.set_motion_detection(False)
|
2020-01-03 10:30:26 +00:00
|
|
|
|
|
|
|
async def stream_source(self):
|
|
|
|
"""Return the source of the stream."""
|
2020-05-05 01:02:09 +00:00
|
|
|
for channel in self._caminfo["channels"]:
|
2020-01-03 10:30:26 +00:00
|
|
|
if channel["isRtspEnabled"]:
|
2020-05-11 22:50:54 +00:00
|
|
|
uri = next(
|
|
|
|
(
|
|
|
|
uri
|
|
|
|
for i, uri in enumerate(channel["rtspUris"])
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
if re.search(self._nvr._host, uri)
|
|
|
|
# pylint: enable=protected-access
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return uri
|
2020-01-03 10:30:26 +00:00
|
|
|
|
|
|
|
return None
|
2020-04-23 22:52:36 +00:00
|
|
|
|
|
|
|
def update(self):
|
|
|
|
"""Update the info."""
|
|
|
|
self._caminfo = self._nvr.get_camera(self._uuid)
|