From 49de55e75b0b4800a1dd1e03e1410f1a01663e0c Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Sun, 5 Jun 2016 10:09:58 -0700 Subject: [PATCH] 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. --- homeassistant/components/camera/uvc.py | 21 ++++++--- requirements_all.txt | 2 +- tests/components/camera/test_uvc.py | 61 +++++++++++++++++++++----- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 0aaf147e4fb..b75d1480b09 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -12,7 +12,7 @@ import requests from homeassistant.components.camera import DOMAIN, Camera from homeassistant.helpers import validate_config -REQUIREMENTS = ['uvcclient==0.8'] +REQUIREMENTS = ['uvcclient==0.9.0'] _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)) return False + identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid' # Filter out airCam models, which are not supported in the latest # version of UnifiVideo and which are EOL by Ubiquiti - cameras = [camera for camera in cameras - if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']] + cameras = [ + camera for camera in cameras + if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']] add_devices([UnifiVideoCamera(nvrconn, - camera['uuid'], + camera[identifier], camera['name']) for camera in cameras]) return True @@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera): dict(name=self._name)) password = 'ubnt' + if self._nvr.server_version >= (3, 2, 0): + client_cls = uvc_camera.UVCCameraClientV320 + else: + client_cls = uvc_camera.UVCCameraClient + camera = None for addr in addrs: try: - camera = uvc_camera.UVCCameraClient(addr, - caminfo['username'], - password) + camera = client_cls(addr, + caminfo['username'], + password) camera.login() _LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s', dict(name=self._name, addr=addr)) diff --git a/requirements_all.txt b/requirements_all.txt index ccd4ac83970..8999eb19e1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -375,7 +375,7 @@ unifi==1.2.5 urllib3 # homeassistant.components.camera.uvc -uvcclient==0.8 +uvcclient==0.9.0 # homeassistant.components.verisure vsure==0.8.1 diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index ccc50523589..7c88c61850f 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -23,14 +23,14 @@ class TestUVCSetup(unittest.TestCase): 'key': 'secret', } fake_cameras = [ - {'uuid': 'one', 'name': 'Front'}, - {'uuid': 'two', 'name': 'Back'}, - {'uuid': 'three', 'name': 'Old AirCam'}, + {'uuid': 'one', 'name': 'Front', 'id': 'id1'}, + {'uuid': 'two', 'name': 'Back', 'id': 'id2'}, + {'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'}, ] def fake_get_camera(uuid): """"Create a fake camera.""" - if uuid == 'three': + if uuid == 'id3': return {'model': 'airCam'} else: return {'model': 'UVC'} @@ -39,13 +39,14 @@ class TestUVCSetup(unittest.TestCase): add_devices = mock.MagicMock() mock_remote.return_value.index.return_value = fake_cameras 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)) mock_remote.assert_called_once_with('foo', 123, '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, 'one', 'Front'), - mock.call(mock_remote.return_value, 'two', 'Back'), + mock.call(mock_remote.return_value, 'id1', 'Front'), + mock.call(mock_remote.return_value, 'id2', 'Back'), ]) @mock.patch('uvcclient.nvr.UVCRemote') @@ -57,13 +58,40 @@ class TestUVCSetup(unittest.TestCase): 'key': 'secret', } fake_cameras = [ - {'uuid': 'one', 'name': 'Front'}, - {'uuid': 'two', 'name': 'Back'}, + {'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, 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)) mock_remote.assert_called_once_with('foo', 7080, 'secret') add_devices.assert_called_once_with([ @@ -114,6 +142,7 @@ class TestUVC(unittest.TestCase): 'internalHost': 'host-b', 'username': 'admin', } + self.nvr.server_version = (3, 2, 0) def test_properties(self): """"Test the properties.""" @@ -123,7 +152,7 @@ class TestUVC(unittest.TestCase): self.assertEqual('UVC Fake', self.uvc.model) @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): """"Test the login.""" 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.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): """"Test the login with no password.""" 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.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): """"Test the login tries.""" responses = [0] @@ -165,7 +204,7 @@ class TestUVC(unittest.TestCase): mock_camera.return_value.login.assert_called_once_with() @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): """"Test if login fails properly.""" mock_camera.return_value.login.side_effect = socket.error