Add support for UniFi Video >= 3.2.0

Unfortunately, Ubiquiti changed their (supposedly versioned) API in
3.2.0 which causes us to have to refer to cameras by id instead of
UUID. The firmware for 3.2.x also changed the on-camera login procedures
and snapshot functionality significantly.

This bumps the requirement for uvcclient to 0.9.0, which supports the
newer API and makes the tweaks necessary to interact properly.
This commit is contained in:
Dan Smith 2016-06-05 10:09:58 -07:00
parent f4594027fd
commit 49de55e75b
3 changed files with 65 additions and 19 deletions

View File

@ -12,7 +12,7 @@ import requests
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
REQUIREMENTS = ['uvcclient==0.8'] REQUIREMENTS = ['uvcclient==0.9.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,13 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error('Unable to connect to NVR: %s', str(ex)) _LOGGER.error('Unable to connect to NVR: %s', str(ex))
return False return False
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
# Filter out airCam models, which are not supported in the latest # Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti # version of UnifiVideo and which are EOL by Ubiquiti
cameras = [camera for camera in cameras cameras = [
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']] camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
add_devices([UnifiVideoCamera(nvrconn, add_devices([UnifiVideoCamera(nvrconn,
camera['uuid'], camera[identifier],
camera['name']) camera['name'])
for camera in cameras]) for camera in cameras])
return True return True
@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
dict(name=self._name)) dict(name=self._name))
password = 'ubnt' password = 'ubnt'
if self._nvr.server_version >= (3, 2, 0):
client_cls = uvc_camera.UVCCameraClientV320
else:
client_cls = uvc_camera.UVCCameraClient
camera = None camera = None
for addr in addrs: for addr in addrs:
try: try:
camera = uvc_camera.UVCCameraClient(addr, camera = client_cls(addr,
caminfo['username'], caminfo['username'],
password) password)
camera.login() camera.login()
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s', _LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
dict(name=self._name, addr=addr)) dict(name=self._name, addr=addr))

View File

@ -375,7 +375,7 @@ unifi==1.2.5
urllib3 urllib3
# homeassistant.components.camera.uvc # homeassistant.components.camera.uvc
uvcclient==0.8 uvcclient==0.9.0
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==0.8.1 vsure==0.8.1

View File

@ -23,14 +23,14 @@ class TestUVCSetup(unittest.TestCase):
'key': 'secret', 'key': 'secret',
} }
fake_cameras = [ fake_cameras = [
{'uuid': 'one', 'name': 'Front'}, {'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'},
{'uuid': 'three', 'name': 'Old AirCam'}, {'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'},
] ]
def fake_get_camera(uuid): def fake_get_camera(uuid):
""""Create a fake camera.""" """"Create a fake camera."""
if uuid == 'three': if uuid == 'id3':
return {'model': 'airCam'} return {'model': 'airCam'}
else: else:
return {'model': 'UVC'} return {'model': 'UVC'}
@ -39,13 +39,14 @@ class TestUVCSetup(unittest.TestCase):
add_devices = mock.MagicMock() add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.side_effect = fake_get_camera mock_remote.return_value.get_camera.side_effect = fake_get_camera
mock_remote.return_value.server_version = (3, 2, 0)
self.assertTrue(uvc.setup_platform(hass, config, add_devices)) self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 123, 'secret') mock_remote.assert_called_once_with('foo', 123, 'secret')
add_devices.assert_called_once_with([ add_devices.assert_called_once_with([
mock_uvc.return_value, mock_uvc.return_value]) mock_uvc.return_value, mock_uvc.return_value])
mock_uvc.assert_has_calls([ mock_uvc.assert_has_calls([
mock.call(mock_remote.return_value, 'one', 'Front'), mock.call(mock_remote.return_value, 'id1', 'Front'),
mock.call(mock_remote.return_value, 'two', 'Back'), mock.call(mock_remote.return_value, 'id2', 'Back'),
]) ])
@mock.patch('uvcclient.nvr.UVCRemote') @mock.patch('uvcclient.nvr.UVCRemote')
@ -57,13 +58,40 @@ class TestUVCSetup(unittest.TestCase):
'key': 'secret', 'key': 'secret',
} }
fake_cameras = [ fake_cameras = [
{'uuid': 'one', 'name': 'Front'}, {'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'},
] ]
hass = mock.MagicMock() hass = mock.MagicMock()
add_devices = mock.MagicMock() add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 2, 0)
self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 7080, 'secret')
add_devices.assert_called_once_with([
mock_uvc.return_value, mock_uvc.return_value])
mock_uvc.assert_has_calls([
mock.call(mock_remote.return_value, 'id1', 'Front'),
mock.call(mock_remote.return_value, 'id2', 'Back'),
])
@mock.patch('uvcclient.nvr.UVCRemote')
@mock.patch.object(uvc, 'UnifiVideoCamera')
def test_setup_partial_config_v31x(self, mock_uvc, mock_remote):
"""Test the setup with a v3.1.x server."""
config = {
'nvr': 'foo',
'key': 'secret',
}
fake_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 1, 3)
self.assertTrue(uvc.setup_platform(hass, config, add_devices)) self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 7080, 'secret') mock_remote.assert_called_once_with('foo', 7080, 'secret')
add_devices.assert_called_once_with([ add_devices.assert_called_once_with([
@ -114,6 +142,7 @@ class TestUVC(unittest.TestCase):
'internalHost': 'host-b', 'internalHost': 'host-b',
'username': 'admin', 'username': 'admin',
} }
self.nvr.server_version = (3, 2, 0)
def test_properties(self): def test_properties(self):
""""Test the properties.""" """"Test the properties."""
@ -123,7 +152,7 @@ class TestUVC(unittest.TestCase):
self.assertEqual('UVC Fake', self.uvc.model) self.assertEqual('UVC Fake', self.uvc.model)
@mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient') @mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login(self, mock_camera, mock_store): def test_login(self, mock_camera, mock_store):
""""Test the login.""" """"Test the login."""
mock_store.return_value.get_camera_password.return_value = 'seekret' mock_store.return_value.get_camera_password.return_value = 'seekret'
@ -133,6 +162,16 @@ class TestUVC(unittest.TestCase):
@mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient') @mock.patch('uvcclient.camera.UVCCameraClient')
def test_login_v31x(self, mock_camera, mock_store):
"""Test login with v3.1.x server."""
mock_store.return_value.get_camera_password.return_value = 'seekret'
self.nvr.server_version = (3, 1, 3)
self.uvc._login()
mock_camera.assert_called_once_with('host-a', 'admin', 'seekret')
mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_no_password(self, mock_camera, mock_store): def test_login_no_password(self, mock_camera, mock_store):
""""Test the login with no password.""" """"Test the login with no password."""
mock_store.return_value.get_camera_password.return_value = None mock_store.return_value.get_camera_password.return_value = None
@ -141,7 +180,7 @@ class TestUVC(unittest.TestCase):
mock_camera.return_value.login.assert_called_once_with() mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient') @mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store):
""""Test the login tries.""" """"Test the login tries."""
responses = [0] responses = [0]
@ -165,7 +204,7 @@ class TestUVC(unittest.TestCase):
mock_camera.return_value.login.assert_called_once_with() mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient') @mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_fails_both_properly(self, mock_camera, mock_store): def test_login_fails_both_properly(self, mock_camera, mock_store):
""""Test if login fails properly.""" """"Test if login fails properly."""
mock_camera.return_value.login.side_effect = socket.error mock_camera.return_value.login.side_effect = socket.error